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;