From 03c81fc9df745d1f6cac44aeb02f3fb737d75561 Mon Sep 17 00:00:00 2001 From: Steve Hancock Date: Sat, 30 Dec 2023 14:12:30 -0800 Subject: [PATCH] add option --want-call-parens=s, see git #128 --- bin/perltidy | 51 +++++++++ dev-bin/perltidy_random_setup.pl | 4 +- lib/Perl/Tidy.pm | 8 +- lib/Perl/Tidy/Formatter.pm | 180 +++++++++++++++++++++++++++++++ 4 files changed, 239 insertions(+), 4 deletions(-) diff --git a/bin/perltidy b/bin/perltidy index b04dccc6..aae89b24 100755 --- a/bin/perltidy +++ b/bin/perltidy @@ -5602,6 +5602,57 @@ can be used to skip warning checks for a list of variables. For example, will skip all warnings for variables C<$self> and C<$class>. +=item B + +Perl allows function call arguments to be placed within parentheses, but it is +not generally required. Styles vary, but often the arguments of user-defined +functions are placed within parens, and the arguments of many builtin function +calls are often written without parens. + +The flag B<--want-call-parens=s>, or B<-wcp=s>, can be used to to produce +a warning message if the parens, or lack of parens, for a selected function +does not match a desired style. This information can help a programmer to +maintain a certain style. + +The string argument B is a list of functions which perltidy should check. +The function names may builtin keywords or user-defined subs. +Perltidy will scan the text for the listed function names, looks for a +subsequent opening paren, and warn of any discrepancies with the request. + +Functions which should enclose their arguments in parens are listed first, +separated by spaces or commas. Then, if there are functions which should not +have parens, an exclamation mark, B, should be added followed by the list of +those functions. + +For example + + perltidy -wcp='open close ! print' somefile.pl + +means that the builtin functions C and C should have parens around +their call args but C should not. Spaces are only needed to separate +words, so this could also be written + + perltidy -wcp='open close!print' somefile.pl + +or + + perltidy -wcp='open,close!print' somefile.pl + +A symbol B<+> may be placed in the string B to end the scope of a preceding +B. So this example could also be written + + perltidy -wcp='open ! print + close' somefile.pl + +If the symbol B<&> is entered instead of a function name, it means all user defined subs not in the list. So for example + + perltidy -wcp='&' + +means that calls to all user-defined subs in the file being processed +should have their call arguments enclosed in parens. + +When adding or removing parentheses, it is important to pay attention to +operator precedence issues. + =item B The first $VERSION line of a file which might be eval'd by MakeMaker diff --git a/dev-bin/perltidy_random_setup.pl b/dev-bin/perltidy_random_setup.pl index ab104d49..12c06c47 100755 --- a/dev-bin/perltidy_random_setup.pl +++ b/dev-bin/perltidy_random_setup.pl @@ -902,6 +902,8 @@ EOM 'space-signature-paren' => [ 0, 2 ], 'break-after-labels' => [ 0, 2 ], + 'want-call-parens' => [ '&', 'open!close' ], + 'want-trailing-commas' => [ '0', '*', 'm', 'b', 'h', 'i', ' ' ], 'one-line-block-exclusion-list' => [ 'sort', 'map', 'grep', 'eval', '*', 'zzyzx' ], @@ -1084,7 +1086,7 @@ EOM if ( $count > 10 ) { $count = 10 } } foreach my $i ( 1 .. $count ) { - my $index = int( rand($imax) + 0.5 ); + my $index = $imax > 0 ? int( rand($imax) + 0.5 ) : 0; if ( $i > 1 ) { $string .= ' ' } $string .= $rrange->[$index]; } diff --git a/lib/Perl/Tidy.pm b/lib/Perl/Tidy.pm index 4125ac70..d891f34c 100644 --- a/lib/Perl/Tidy.pm +++ b/lib/Perl/Tidy.pm @@ -3,7 +3,7 @@ # # perltidy - a perl script indenter and formatter # -# Copyright (c) 2000-2023 by Steve Hancock +# Copyright (c) 2000-2024 by Steve Hancock # Distributed under the GPL license agreement; see file COPYING # # This program is free software; you can redistribute it and/or modify @@ -2104,8 +2104,9 @@ sub process_all_files { if ( $in_place_modify && !-w $input_file ) { my $backup_method = $rOpts->{'backup-method'}; if ( defined($backup_method) && $backup_method eq 'copy' ) { - Warn -"skipping file '$input_file' for -b option: file reported as non-writable\n"; + Warn( +"skipping file '$input_file' for -b option: file reported as non-writable\n" + ); next; } } @@ -3704,6 +3705,7 @@ sub generate_options { $add_option->( 'pass-version-line', 'pvl', '!' ); $add_option->( 'warn-variable-types', 'wvt', '=s' ); $add_option->( 'warn-variable-exclusion-list', 'wvxl', '=s' ); + $add_option->( 'want-call-parens', 'wcp', '=s' ); ######################################## $category = 13; # Debugging diff --git a/lib/Perl/Tidy/Formatter.pm b/lib/Perl/Tidy/Formatter.pm index 5da85f1f..02ccc76b 100644 --- a/lib/Perl/Tidy/Formatter.pm +++ b/lib/Perl/Tidy/Formatter.pm @@ -376,6 +376,9 @@ my ( # INITIALIZER: sub initialize_trailing_comma_rules %trailing_comma_rules, + # INITIALIZER: sub initialize_call_paren_style + %call_paren_style, + # regex patterns for text identification. # Most can be configured by user parameters. # Most are initialized in a sub make_**_pattern during configuration. @@ -1427,6 +1430,8 @@ sub check_options { initialize_missing_else_comment(); + initialize_call_paren_style(); + make_bli_pattern(); make_bl_pattern(); @@ -6425,6 +6430,8 @@ EOM if ( $rOpts->{'warn-variable-types'} && $self->[_logger_object_] ); + $self->scan_call_parens(); + $self->examine_vertical_tightness_flags(); $self->set_excluded_lp_containers(); @@ -9695,6 +9702,179 @@ EOM return; } ## end sub warn_variable_types +sub initialize_call_paren_style { + + # parse the flag --want-call-parens=string + + # Rules: + # - we scan from left to right, so if there are multiple entries for + # a word, the last entry is used + # - starting mode is WANT PARENS + # - '!' switches to DO NOT WANT PARENS mode + # - '+' resets to WANT PARENS mode (if needed for complex lists) + # - '&' is default for user-defined subs + # - '*' is reserved as possible FUTURE default for SELECTED keywords + # - ',' same as space + + # To create an input string: + # - first list all words which SHOULD have parens + # - then, if some do not get parens, add a '!' + # - then list all words which SHOULD NOT have parens; + # - Enter '&' instead of a word to indicate a default for sub calls + + # Examples: + # wcp='&' - means all sub calls should have parens + # wcp='open close' - 'open' and 'close' have parens + # wcp='! open close' - 'open' and 'close' do NOT have parens + # wcp='& ! myfun ' - all subs have perens except 'myfun' + + %call_paren_style = (); + my $opt = 'want-call-parens'; + my $str = $rOpts->{$opt}; + return unless $str; + + my $want_parens = 1; + my $err_msg; + while ( + $str =~ m{ + \G( + (\s+) # whitespace - this must come before \W + | (\W) # or single-character, non-whitespace punct + | (\d+) # or sequence of digits - must come before \w + | (\w+) # or words not starting with a digit + ) + }gcx + ) + { + # skip blanks + if ( defined($2) ) { next } + + if ( defined($3) ) { + if ( $3 eq '!' ) { $want_parens = 0; } + elsif ( $3 eq '+' ) { $want_parens = 1; } + elsif ( $3 eq '*' ) { $call_paren_style{$3} = $want_parens } + elsif ( $3 eq '&' ) { $call_paren_style{$3} = $want_parens } + elsif ( $3 eq ',' ) { next } + else { $err_msg = "Unexpected symbol '$3'"; last } + } + elsif ( defined($4) ) { + $err_msg = "Unexpected digit '$4'"; + last; + } + elsif ( defined($5) ) { + if ( defined( $call_paren_style{$5} ) ) { + Warn("--$opt has multiple entries for '$5', last is used\n"); + } + $call_paren_style{$5} = $want_parens; + } + else { + ## should never get here + $err_msg = "Unexpected token '$1'"; + last; + } + } + if ($err_msg) { + Perl::Tidy::Die("Error parsing --$opt: $err_msg\n"); + } + return; +} ## end sub initialize_call_paren_style + +sub scan_call_parens { + my ($self) = @_; + + # Perform a scan requesed by --want-call-parens + # We search for selected functions or keywords and for a following paren. + # A warning is issued if the paren existence is not what is wanted + # according to the setting --want-call-parens. + + # This routine does not attempt to add or remove parens, it merely + # issues a warning so that the user can make a change if desired. + # It is risky to add or delete parens automatically; see git #128. + + my $wcp_name = 'want-call-parens'; + my $rOpts_warn_call_parens = $rOpts->{$wcp_name}; + return unless ($rOpts_warn_call_parens); + + # if no hash, user may have just entered -wcp='!' + return unless (%call_paren_style); + + my $rwarnings = []; + + #--------------------- + # Loop over all tokens + #--------------------- + my $rLL = $self->[_rLL_]; + foreach my $KK ( 0 .. @{$rLL} - 1 ) { + my $type = $rLL->[$KK]->[_TYPE_]; + next if ( $type eq 'b' || $type eq '#' ); + + # 'k'=builtin keyword, 'U'=user defined sub, 'w'=unknown bareword + if ( $type eq 'k' || $type eq 'U' || $type eq 'w' ) { + + # Are we looking for this word? + my $token = $rLL->[$KK]->[_TOKEN_]; + my $want_paren = $call_paren_style{$token}; + + # Only user-defined subs (type 'U') have defaults. + if ( !defined($want_paren) ) { + $want_paren = + $type eq 'k' ? undef + : $type eq 'U' ? $call_paren_style{'&'} + : undef; + } + next unless defined($want_paren); + + # This is a selected word. Look for a '(' at the next token. + my $Kn = $self->K_next_code($KK); + next unless defined($Kn); + + my $token_Kn = $rLL->[$Kn]->[_TOKEN_]; + if ( $token_Kn eq '=>' ) { next } + elsif ( $token_Kn eq '(' ) { next if ($want_paren) } + else { next if ( !$want_paren ) } + + # This disagrees with the wanted style; issue a warning. + my $note = $want_paren ? "no call parens" : "has call parens"; + my $rwarning = { + token => $token, + token_next => $token_Kn, + want => $want_paren, + note => $note, + line_number => $rLL->[$KK]->[_LINE_INDEX_] + 1, + KK => $KK, + Kn => $Kn, + }; + push @{$rwarnings}, $rwarning; + } + } + + # Report any warnings + if ( @{$rwarnings} ) { + my $message = "Begin scan for --$wcp_name\n"; + $message .= <{token}; + my $token_next = $item->{token_next}; + my $note = $item->{note}; + my $lno = $item->{line_number}; + + # trim long tokens for the output line + if ( length($token_next) > 23 ) { + $token_next = substr( $token_next, 0, 20 ) . '...'; + } + $message .= "$lno:$token $token_next: $note\n"; + } + $message .= "End scan for --$wcp_name\n"; + + # Note that this is sent in a single call to warning() in order + # to avoid triggering a stop on large warning count + warning($message); + } + return; +} ## end sub scan_call_parens + sub find_non_indenting_braces { my ( $self, $rix_side_comments ) = @_; -- 2.39.5