github.com/containers/podman/v2@v2.2.2-0.20210501105131-c1e07d070c4c/hack/swagger-check (about)

     1  #!/usr/bin/perl
     2  #
     3  # swagger-check - Look for inconsistencies between swagger and source code
     4  #
     5  package LibPod::SwaggerCheck;
     6  
     7  use v5.14;
     8  use strict;
     9  use warnings;
    10  
    11  use File::Find;
    12  
    13  (our $ME = $0) =~ s|.*/||;
    14  (our $VERSION = '$Revision: 1.7 $ ') =~ tr/[0-9].//cd;
    15  
    16  # For debugging, show data structures using DumpTree($var)
    17  #use Data::TreeDumper; $Data::TreeDumper::Displayaddress = 0;
    18  
    19  ###############################################################################
    20  # BEGIN user-customizable section
    21  
    22  our $Default_Dir =  'pkg/api/server';
    23  
    24  # END   user-customizable section
    25  ###############################################################################
    26  
    27  ###############################################################################
    28  # BEGIN boilerplate args checking, usage messages
    29  
    30  sub usage {
    31      print  <<"END_USAGE";
    32  Usage: $ME [OPTIONS] DIRECTORY-TO-CHECK
    33  
    34  $ME scans all .go files under the given DIRECTORY-TO-CHECK
    35  (default: $Default_Dir), looking for lines of the form 'r.Handle(...)'
    36  or 'r.HandleFunc(...)'. For each such line, we check for a preceding
    37  swagger comment line and verify that the comment line matches the
    38  declarations in the r.Handle() invocation.
    39  
    40  For example, the following would be a correctly-matching pair of lines:
    41  
    42      // swagger:operation GET /images/json compat getImages
    43      r.Handle(VersionedPath("/images/json"), s.APIHandler(compat.GetImages)).Methods(http.MethodGet)
    44  
    45  ...because http.MethodGet matches GET in the comment, the endpoint
    46  is /images/json in both cases, the APIHandler() says "compat" so
    47  that's the swagger tag, and the swagger operation name is the
    48  same as the APIHandler but with a lower-case first letter.
    49  
    50  The following is an inconsistency as reported by this script:
    51  
    52  pkg/api/server/register_info.go:
    53  -       // swagger:operation GET /info libpod libpodGetInfo
    54  +       // ................. ... ..... compat
    55          r.Handle(VersionedPath("/info"), s.APIHandler(compat.GetInfo)).Methods(http.MethodGet)
    56  
    57  ...because APIHandler() says 'compat' but the swagger comment
    58  says 'libpod'.
    59  
    60  OPTIONS:
    61  
    62    --pedantic     Compare operation names (the last part of swagger comment).
    63                   There are far too many of these inconsistencies to allow us
    64                   to enable this by default, but it still might be a useful
    65                   check in some circumstances.
    66  
    67    -v, --verbose  show verbose progress indicators
    68    -n, --dry-run  make no actual changes
    69  
    70    --help         display this message
    71    --version      display program name and version
    72  END_USAGE
    73  
    74      exit;
    75  }
    76  
    77  # Command-line options.  Note that this operates directly on @ARGV !
    78  our $pedantic;
    79  our $debug   = 0;
    80  our $force   = 0;
    81  our $verbose = 0;
    82  our $NOT     = '';              # print "blahing the blah$NOT\n" if $debug
    83  sub handle_opts {
    84      use Getopt::Long;
    85      GetOptions(
    86          'pedantic'   => \$pedantic,
    87  
    88          'debug!'     => \$debug,
    89          'dry-run|n!' => sub { $NOT = ' [NOT]' },
    90          'force'      => \$force,
    91          'verbose|v'  => \$verbose,
    92  
    93          help         => \&usage,
    94          man          => \&man,
    95          version      => sub { print "$ME version $VERSION\n"; exit 0 },
    96      ) or die "Try `$ME --help' for help\n";
    97  }
    98  
    99  # END   boilerplate args checking, usage messages
   100  ###############################################################################
   101  
   102  ############################## CODE BEGINS HERE ###############################
   103  
   104  my $exit_status = 0;
   105  
   106  # The term is "modulino".
   107  __PACKAGE__->main()                                     unless caller();
   108  
   109  # Main code.
   110  sub main {
   111      # Note that we operate directly on @ARGV, not on function parameters.
   112      # This is deliberate: it's because Getopt::Long only operates on @ARGV
   113      # and there's no clean way to make it use @_.
   114      handle_opts();                      # will set package globals
   115  
   116      # Fetch command-line arguments.  Barf if too many.
   117      my $dir = shift(@ARGV) || $Default_Dir;
   118      die "$ME: Too many arguments; try $ME --help\n"                 if @ARGV;
   119  
   120      # Find and act upon all matching files
   121      find { wanted => sub { finder(@_) }, no_chdir => 1 }, $dir;
   122  
   123      exit $exit_status;
   124  }
   125  
   126  
   127  ############
   128  #  finder  #  File::Find action - looks for 'r.Handle' or 'r.HandleFunc'
   129  ############
   130  sub finder {
   131      my $path = $File::Find::name;
   132      return if     $path =~ m|/\.|;              # skip dotfiles
   133      return unless $path =~ /\.go$/;             # Only want .go files
   134  
   135      print $path, "\n"                           if $debug;
   136  
   137      # Read each .go file. Keep a running tally of all '// comment' lines;
   138      # if we see a 'r.Handle()' or 'r.HandleFunc()' line, pass it + comments
   139      # to analysis function.
   140      open my $in, '<', $path
   141          or die "$ME: Cannot read $path: $!\n";
   142      my @comments;
   143      while (my $line = <$in>) {
   144          if ($line =~ m!^\s*//!) {
   145              push @comments, $line;
   146          }
   147          else {
   148              # Not a comment line. If it's an r.Handle*() one, process it.
   149              if ($line =~ m!^\s*r\.Handle(Func)?\(!) {
   150                  handle_handle($path, $line, @comments)
   151                      or $exit_status = 1;
   152              }
   153  
   154              # Reset comments
   155              @comments = ();
   156          }
   157      }
   158      close $in;
   159  }
   160  
   161  
   162  ###################
   163  #  handle_handle  #  Cross-check a 'r.Handle*' declaration against swagger
   164  ###################
   165  #
   166  # Returns false if swagger comment is inconsistent with function call,
   167  # true if it matches or if there simply isn't a swagger comment.
   168  #
   169  sub handle_handle {
   170      my $path     = shift;               # for error messages only
   171      my $line     = shift;               # in: the r.Handle* line
   172      my @comments = @_;                  # in: preceding comment lines
   173  
   174      # Preserve the original line, so we can show it in comments
   175      my $line_orig = $line;
   176  
   177      # Strip off the 'r.Handle*(' and leading whitespace; preserve the latter
   178      $line =~ s!^(\s*)r\.Handle(Func)?\(!!
   179          or die "$ME: INTERNAL ERROR! Got '$line'!\n";
   180      my $indent = $1;
   181  
   182      # Some have VersionedPath, some don't. Doesn't seem to make a difference
   183      # in terms of swagger, so let's just ignore it.
   184      $line =~ s!^VersionedPath\(([^\)]+)\)!$1!;
   185      $line =~ m!^"(/[^"]+)",!
   186          or die "$ME: $path:$.: Cannot grok '$line'\n";
   187      my $endpoint = $1;
   188  
   189      # FIXME: in older code, '{name:..*}' meant 'nameOrID'. As of 2020-02
   190      # it looks like most of the '{name:..*}' entries are gone, except for one.
   191  ###FIXME-obsolete?    $endpoint =~ s|\{name:\.\.\*\}|{nameOrID}|;
   192  
   193      # e.g. /auth, /containers/*/rename, /distribution, /monitor, /plugins
   194      return 1 if $line =~ /\.UnsupportedHandler/;
   195  
   196      #
   197      # Determine the HTTP METHOD (GET, POST, DELETE, HEAD)
   198      #
   199      my $method;
   200      if ($line =~ /generic.VersionHandler/) {
   201          $method = 'GET';
   202      }
   203      elsif ($line =~ m!\.Methods\((.*)\)!) {
   204          my $x = $1;
   205  
   206          if ($x =~ /Method(Post|Get|Delete|Head)/) {
   207              $method = uc $1;
   208          }
   209          elsif ($x =~ /\"(HEAD|GET|POST)"/) {
   210              $method = $1;
   211          }
   212          else {
   213              die "$ME: $path:$.: Cannot grok $x\n";
   214          }
   215      }
   216      else {
   217          warn "$ME: $path:$.: No Methods in '$line'\n";
   218          return 1;
   219      }
   220  
   221      #
   222      # Determine the SWAGGER TAG. Assume 'compat' unless we see libpod; but
   223      # this can be overruled (see special case below)
   224      #
   225      my $tag = ($endpoint =~ /(libpod)/ ? $1 : 'compat');
   226  
   227      #
   228      # Determine the OPERATION. *** NOTE: This is mostly useless! ***
   229      # In an ideal world the swagger comment would match actual function call;
   230      # in reality there are over thirty mismatches. Use --pedantic to see.
   231      #
   232      my $operation = '';
   233      if ($line =~ /(generic|handlers|compat)\.(\w+)/) {
   234          $operation = lcfirst $2;
   235          if ($endpoint =~ m!/libpod/! && $operation !~ /^libpod/) {
   236              $operation = 'libpod' . ucfirst $operation;
   237          }
   238      }
   239      elsif ($line =~ /(libpod)\.(\w+)/) {
   240          $operation = "$1$2";
   241      }
   242  
   243      # Special case: the following endpoints all get a custom tag
   244      if ($endpoint =~ m!/(pods|manifests)/!) {
   245          $tag = $1;
   246          $operation =~ s/^libpod//;
   247          $operation = lcfirst $operation;
   248      }
   249  
   250      # Special case: anything related to 'events' gets a system tag
   251      if ($endpoint =~ m!/events!) {
   252          $tag = 'system';
   253      }
   254  
   255      # Special case: /changes is libpod even though it says compat
   256      if ($endpoint =~ m!/changes!) {
   257          $tag = 'libpod';
   258      }
   259  
   260      state $previous_path;                # Previous path name, to avoid dups
   261  
   262      #
   263      # Compare actual swagger comment to what we expect based on Handle call.
   264      #
   265      my $expect = " // swagger:operation $method $endpoint $tag $operation ";
   266      my @actual = grep { /swagger:operation/ } @comments;
   267  
   268      return 1 if !@actual;         # No swagger comment in file; oh well
   269  
   270      my $actual = $actual[0];
   271  
   272      # By default, don't compare the operation: there are far too many
   273      # mismatches here.
   274      if (! $pedantic) {
   275          $actual =~ s/\s+\S+\s*$//;
   276          $expect =~ s/\s+\S+\s*$//;
   277      }
   278  
   279      # (Ignore whitespace discrepancies)
   280      (my $a_trimmed = $actual) =~ s/\s+/ /g;
   281  
   282      return 1 if $a_trimmed eq $expect;
   283  
   284      # Mismatch. Display it. Start with filename, if different from previous
   285      print "\n";
   286      if (!$previous_path || $previous_path ne $path) {
   287          print $path, ":\n";
   288      }
   289      $previous_path = $path;
   290  
   291      # Show the actual line, prefixed with '-' ...
   292      print "- $actual[0]";
   293      # ...then our generated ones, but use '...' as a way to ignore matches
   294      print "+ $indent//";
   295      my @actual_split = split ' ', $actual;
   296      my @expect_split = split ' ', $expect;
   297      for my $i (1 .. $#actual_split) {
   298          print " ";
   299          if ($actual_split[$i] eq ($expect_split[$i]||'')) {
   300              print "." x length($actual_split[$i]);
   301          }
   302          else {
   303              # Show the difference. Use terminal highlights if available.
   304              print "\e[1;37m"            if -t *STDOUT;
   305              print $expect_split[$i];
   306              print "\e[m"                if -t *STDOUT;
   307          }
   308      }
   309      print "\n";
   310  
   311      # Show the r.Handle* code line itself
   312      print "  ", $line_orig;
   313  
   314      return;
   315  }
   316  
   317  1;