1package codescan 2 3import ( 4 "fmt" 5 "go/ast" 6 "go/token" 7 "go/types" 8 "log" 9 "os" 10 "strings" 11 12 "github.com/go-openapi/swag" 13 14 "golang.org/x/tools/go/packages" 15 16 "github.com/go-openapi/spec" 17) 18 19const pkgLoadMode = packages.NeedName | packages.NeedFiles | packages.NeedImports | packages.NeedDeps | packages.NeedTypes | packages.NeedSyntax | packages.NeedTypesInfo 20 21func safeConvert(str string) bool { 22 b, err := swag.ConvertBool(str) 23 if err != nil { 24 return false 25 } 26 return b 27} 28 29// Debug is true when process is run with DEBUG=1 env var 30var Debug = safeConvert(os.Getenv("DEBUG")) 31 32type node uint32 33 34const ( 35 metaNode node = 1 << iota 36 routeNode 37 operationNode 38 modelNode 39 parametersNode 40 responseNode 41) 42 43// Options for the scanner 44type Options struct { 45 Packages []string 46 InputSpec *spec.Swagger 47 ScanModels bool 48 WorkDir string 49 BuildTags string 50 ExcludeDeps bool 51 Include []string 52 Exclude []string 53 IncludeTags []string 54 ExcludeTags []string 55} 56 57type scanCtx struct { 58 pkgs []*packages.Package 59 app *typeIndex 60} 61 62func sliceToSet(names []string) map[string]bool { 63 result := make(map[string]bool) 64 for _, v := range names { 65 result[v] = true 66 } 67 return result 68} 69 70// Run the scanner to produce a spec with the options provided 71func Run(opts *Options) (*spec.Swagger, error) { 72 sc, err := newScanCtx(opts) 73 if err != nil { 74 return nil, err 75 } 76 sb := newSpecBuilder(opts.InputSpec, sc, opts.ScanModels) 77 return sb.Build() 78} 79 80func newScanCtx(opts *Options) (*scanCtx, error) { 81 cfg := &packages.Config{ 82 Dir: opts.WorkDir, 83 Mode: pkgLoadMode, 84 Tests: false, 85 } 86 if opts.BuildTags != "" { 87 cfg.BuildFlags = []string{"-tags", opts.BuildTags} 88 } 89 90 pkgs, err := packages.Load(cfg, opts.Packages...) 91 if err != nil { 92 return nil, err 93 } 94 95 app, err := newTypeIndex(pkgs, opts.ExcludeDeps, 96 sliceToSet(opts.IncludeTags), sliceToSet(opts.ExcludeTags), 97 opts.Include, opts.Exclude) 98 if err != nil { 99 return nil, err 100 } 101 102 return &scanCtx{ 103 pkgs: pkgs, 104 app: app, 105 }, nil 106} 107 108type entityDecl struct { 109 Comments *ast.CommentGroup 110 Type *types.Named 111 Ident *ast.Ident 112 Spec *ast.TypeSpec 113 File *ast.File 114 Pkg *packages.Package 115 hasModelAnnotation bool 116 hasResponseAnnotation bool 117 hasParameterAnnotation bool 118} 119 120func (d *entityDecl) Names() (name, goName string) { 121 goName = d.Ident.Name 122 name = goName 123 if d.Comments == nil { 124 return 125 } 126 127DECLS: 128 for _, cmt := range d.Comments.List { 129 for _, ln := range strings.Split(cmt.Text, "\n") { 130 matches := rxModelOverride.FindStringSubmatch(ln) 131 if len(matches) > 0 { 132 d.hasModelAnnotation = true 133 } 134 if len(matches) > 1 && len(matches[1]) > 0 { 135 name = matches[1] 136 break DECLS 137 } 138 } 139 } 140 return 141} 142 143func (d *entityDecl) ResponseNames() (name, goName string) { 144 goName = d.Ident.Name 145 name = goName 146 if d.Comments == nil { 147 return 148 } 149 150DECLS: 151 for _, cmt := range d.Comments.List { 152 for _, ln := range strings.Split(cmt.Text, "\n") { 153 matches := rxResponseOverride.FindStringSubmatch(ln) 154 if len(matches) > 0 { 155 d.hasResponseAnnotation = true 156 } 157 if len(matches) > 1 && len(matches[1]) > 0 { 158 name = matches[1] 159 break DECLS 160 } 161 } 162 } 163 return 164} 165 166func (d *entityDecl) OperationIDS() (result []string) { 167 if d == nil || d.Comments == nil { 168 return nil 169 } 170 171 for _, cmt := range d.Comments.List { 172 for _, ln := range strings.Split(cmt.Text, "\n") { 173 matches := rxParametersOverride.FindStringSubmatch(ln) 174 if len(matches) > 0 { 175 d.hasParameterAnnotation = true 176 } 177 if len(matches) > 1 && len(matches[1]) > 0 { 178 for _, pt := range strings.Split(matches[1], " ") { 179 tr := strings.TrimSpace(pt) 180 if len(tr) > 0 { 181 result = append(result, tr) 182 } 183 } 184 } 185 } 186 } 187 return 188} 189 190func (d *entityDecl) HasModelAnnotation() bool { 191 if d.hasModelAnnotation { 192 return true 193 } 194 if d.Comments == nil { 195 return false 196 } 197 for _, cmt := range d.Comments.List { 198 for _, ln := range strings.Split(cmt.Text, "\n") { 199 matches := rxModelOverride.FindStringSubmatch(ln) 200 if len(matches) > 0 { 201 d.hasModelAnnotation = true 202 return true 203 } 204 } 205 } 206 return false 207} 208 209func (d *entityDecl) HasResponseAnnotation() bool { 210 if d.hasResponseAnnotation { 211 return true 212 } 213 if d.Comments == nil { 214 return false 215 } 216 for _, cmt := range d.Comments.List { 217 for _, ln := range strings.Split(cmt.Text, "\n") { 218 matches := rxResponseOverride.FindStringSubmatch(ln) 219 if len(matches) > 0 { 220 d.hasResponseAnnotation = true 221 return true 222 } 223 } 224 } 225 return false 226} 227 228func (d *entityDecl) HasParameterAnnotation() bool { 229 if d.hasParameterAnnotation { 230 return true 231 } 232 if d.Comments == nil { 233 return false 234 } 235 for _, cmt := range d.Comments.List { 236 for _, ln := range strings.Split(cmt.Text, "\n") { 237 matches := rxParametersOverride.FindStringSubmatch(ln) 238 if len(matches) > 0 { 239 d.hasParameterAnnotation = true 240 return true 241 } 242 } 243 } 244 return false 245} 246 247func (s *scanCtx) FindDecl(pkgPath, name string) (*entityDecl, bool) { 248 if pkg, ok := s.app.AllPackages[pkgPath]; ok { 249 for _, file := range pkg.Syntax { 250 for _, d := range file.Decls { 251 gd, ok := d.(*ast.GenDecl) 252 if !ok { 253 continue 254 } 255 256 for _, sp := range gd.Specs { 257 if ts, ok := sp.(*ast.TypeSpec); ok && ts.Name.Name == name { 258 def, ok := pkg.TypesInfo.Defs[ts.Name] 259 if !ok { 260 debugLog("couldn't find type info for %s", ts.Name) 261 continue 262 } 263 nt, isNamed := def.Type().(*types.Named) 264 if !isNamed { 265 debugLog("%s is not a named type but a %T", ts.Name, def.Type()) 266 continue 267 } 268 decl := &entityDecl{ 269 Comments: gd.Doc, 270 Type: nt, 271 Ident: ts.Name, 272 Spec: ts, 273 File: file, 274 Pkg: pkg, 275 } 276 return decl, true 277 } 278 279 } 280 } 281 } 282 } 283 return nil, false 284} 285 286func (s *scanCtx) FindModel(pkgPath, name string) (*entityDecl, bool) { 287 for _, cand := range s.app.Models { 288 ct := cand.Type.Obj() 289 if ct.Name() == name && ct.Pkg().Path() == pkgPath { 290 return cand, true 291 } 292 } 293 if decl, found := s.FindDecl(pkgPath, name); found { 294 s.app.ExtraModels[decl.Ident] = decl 295 return decl, true 296 } 297 return nil, false 298} 299 300func (s *scanCtx) PkgForPath(pkgPath string) (*packages.Package, bool) { 301 v, ok := s.app.AllPackages[pkgPath] 302 return v, ok 303} 304 305func (s *scanCtx) DeclForType(t types.Type) (*entityDecl, bool) { 306 switch tpe := t.(type) { 307 case *types.Pointer: 308 return s.DeclForType(tpe.Elem()) 309 case *types.Named: 310 return s.FindDecl(tpe.Obj().Pkg().Path(), tpe.Obj().Name()) 311 312 default: 313 log.Printf("unknown type to find the package for [%T]: %s", t, t.String()) 314 return nil, false 315 } 316} 317 318func (s *scanCtx) PkgForType(t types.Type) (*packages.Package, bool) { 319 switch tpe := t.(type) { 320 // case *types.Basic: 321 // case *types.Struct: 322 // case *types.Pointer: 323 // case *types.Interface: 324 // case *types.Array: 325 // case *types.Slice: 326 // case *types.Map: 327 case *types.Named: 328 v, ok := s.app.AllPackages[tpe.Obj().Pkg().Path()] 329 return v, ok 330 default: 331 log.Printf("unknown type to find the package for [%T]: %s", t, t.String()) 332 return nil, false 333 } 334} 335 336func (s *scanCtx) FindComments(pkg *packages.Package, name string) (*ast.CommentGroup, bool) { 337 for _, f := range pkg.Syntax { 338 for _, d := range f.Decls { 339 gd, ok := d.(*ast.GenDecl) 340 if !ok { 341 continue 342 } 343 344 for _, s := range gd.Specs { 345 if ts, ok := s.(*ast.TypeSpec); ok { 346 if ts.Name.Name == name { 347 return gd.Doc, true 348 } 349 } 350 } 351 } 352 } 353 return nil, false 354} 355 356func (s *scanCtx) FindEnumValues(pkg *packages.Package, enumName string) (list []interface{}, _ bool) { 357 for _, f := range pkg.Syntax { 358 for _, d := range f.Decls { 359 gd, ok := d.(*ast.GenDecl) 360 if !ok { 361 continue 362 } 363 364 if gd.Tok != token.CONST { 365 continue 366 } 367 368 for _, s := range gd.Specs { 369 if vs, ok := s.(*ast.ValueSpec); ok { 370 if vsIdent, ok := vs.Type.(*ast.Ident); ok { 371 if vsIdent.Name == enumName { 372 if len(vs.Values) > 0 { 373 if bl, ok := vs.Values[0].(*ast.BasicLit); ok { 374 list = append(list, getEnumBasicLitValue(bl)) 375 } 376 } 377 } 378 } 379 } 380 } 381 } 382 } 383 return list, true 384} 385 386func newTypeIndex(pkgs []*packages.Package, 387 excludeDeps bool, includeTags, excludeTags map[string]bool, 388 includePkgs, excludePkgs []string) (*typeIndex, error) { 389 390 ac := &typeIndex{ 391 AllPackages: make(map[string]*packages.Package), 392 Models: make(map[*ast.Ident]*entityDecl), 393 ExtraModels: make(map[*ast.Ident]*entityDecl), 394 excludeDeps: excludeDeps, 395 includeTags: includeTags, 396 excludeTags: excludeTags, 397 includePkgs: includePkgs, 398 excludePkgs: excludePkgs, 399 } 400 if err := ac.build(pkgs); err != nil { 401 return nil, err 402 } 403 return ac, nil 404} 405 406type typeIndex struct { 407 AllPackages map[string]*packages.Package 408 Models map[*ast.Ident]*entityDecl 409 ExtraModels map[*ast.Ident]*entityDecl 410 Meta []metaSection 411 Routes []parsedPathContent 412 Operations []parsedPathContent 413 Parameters []*entityDecl 414 Responses []*entityDecl 415 excludeDeps bool 416 includeTags map[string]bool 417 excludeTags map[string]bool 418 includePkgs []string 419 excludePkgs []string 420} 421 422func (a *typeIndex) build(pkgs []*packages.Package) error { 423 for _, pkg := range pkgs { 424 if _, known := a.AllPackages[pkg.PkgPath]; known { 425 continue 426 } 427 a.AllPackages[pkg.PkgPath] = pkg 428 if err := a.processPackage(pkg); err != nil { 429 return err 430 } 431 if err := a.walkImports(pkg); err != nil { 432 return err 433 } 434 } 435 436 return nil 437} 438 439func (a *typeIndex) processPackage(pkg *packages.Package) error { 440 if !shouldAcceptPkg(pkg.PkgPath, a.includePkgs, a.excludePkgs) { 441 debugLog("package %s is ignored due to rules", pkg.Name) 442 return nil 443 } 444 445 for _, file := range pkg.Syntax { 446 n, err := a.detectNodes(file) 447 if err != nil { 448 return err 449 } 450 451 if n&metaNode != 0 { 452 a.Meta = append(a.Meta, metaSection{Comments: file.Doc}) 453 } 454 455 if n&operationNode != 0 { 456 for _, cmts := range file.Comments { 457 pp := parsePathAnnotation(rxOperation, cmts.List) 458 if pp.Method == "" { 459 continue // not a valid operation 460 } 461 if !shouldAcceptTag(pp.Tags, a.includeTags, a.excludeTags) { 462 debugLog("operation %s %s is ignored due to tag rules", pp.Method, pp.Path) 463 continue 464 } 465 a.Operations = append(a.Operations, pp) 466 } 467 } 468 469 if n&routeNode != 0 { 470 for _, cmts := range file.Comments { 471 pp := parsePathAnnotation(rxRoute, cmts.List) 472 if pp.Method == "" { 473 continue // not a valid operation 474 } 475 if !shouldAcceptTag(pp.Tags, a.includeTags, a.excludeTags) { 476 debugLog("operation %s %s is ignored due to tag rules", pp.Method, pp.Path) 477 continue 478 } 479 a.Routes = append(a.Routes, pp) 480 } 481 } 482 483 for _, dt := range file.Decls { 484 switch fd := dt.(type) { 485 case *ast.BadDecl: 486 continue 487 case *ast.FuncDecl: 488 if fd.Body == nil { 489 continue 490 } 491 for _, stmt := range fd.Body.List { 492 if dstm, ok := stmt.(*ast.DeclStmt); ok { 493 if gd, isGD := dstm.Decl.(*ast.GenDecl); isGD { 494 a.processDecl(pkg, file, n, gd) 495 } 496 } 497 } 498 case *ast.GenDecl: 499 a.processDecl(pkg, file, n, fd) 500 } 501 } 502 } 503 return nil 504} 505 506func (a *typeIndex) processDecl(pkg *packages.Package, file *ast.File, n node, gd *ast.GenDecl) { 507 for _, sp := range gd.Specs { 508 switch ts := sp.(type) { 509 case *ast.ValueSpec: 510 debugLog("saw value spec: %v", ts.Names) 511 return 512 case *ast.ImportSpec: 513 debugLog("saw import spec: %v", ts.Name) 514 return 515 case *ast.TypeSpec: 516 def, ok := pkg.TypesInfo.Defs[ts.Name] 517 if !ok { 518 debugLog("couldn't find type info for %s", ts.Name) 519 continue 520 } 521 nt, isNamed := def.Type().(*types.Named) 522 if !isNamed { 523 debugLog("%s is not a named type but a %T", ts.Name, def.Type()) 524 continue 525 } 526 decl := &entityDecl{ 527 Comments: gd.Doc, 528 Type: nt, 529 Ident: ts.Name, 530 Spec: ts, 531 File: file, 532 Pkg: pkg, 533 } 534 key := ts.Name 535 if n&modelNode != 0 && decl.HasModelAnnotation() { 536 a.Models[key] = decl 537 } 538 if n¶metersNode != 0 && decl.HasParameterAnnotation() { 539 a.Parameters = append(a.Parameters, decl) 540 } 541 if n&responseNode != 0 && decl.HasResponseAnnotation() { 542 a.Responses = append(a.Responses, decl) 543 } 544 } 545 } 546} 547 548func (a *typeIndex) walkImports(pkg *packages.Package) error { 549 if a.excludeDeps { 550 return nil 551 } 552 for _, v := range pkg.Imports { 553 if _, known := a.AllPackages[v.PkgPath]; known { 554 continue 555 } 556 557 a.AllPackages[v.PkgPath] = v 558 if err := a.processPackage(v); err != nil { 559 return err 560 } 561 if err := a.walkImports(v); err != nil { 562 return err 563 } 564 } 565 return nil 566} 567 568func (a *typeIndex) detectNodes(file *ast.File) (node, error) { 569 var n node 570 for _, comments := range file.Comments { 571 var seenStruct string 572 for _, cline := range comments.List { 573 if cline == nil { 574 continue 575 } 576 } 577 578 for _, cline := range comments.List { 579 if cline == nil { 580 continue 581 } 582 583 matches := rxSwaggerAnnotation.FindStringSubmatch(cline.Text) 584 if len(matches) < 2 { 585 continue 586 } 587 588 switch matches[1] { 589 case "route": 590 n |= routeNode 591 case "operation": 592 n |= operationNode 593 case "model": 594 n |= modelNode 595 if seenStruct == "" || seenStruct == matches[1] { 596 seenStruct = matches[1] 597 } else { 598 return 0, fmt.Errorf("classifier: already annotated as %s, can't also be %q - %s", seenStruct, matches[1], cline.Text) 599 } 600 case "meta": 601 n |= metaNode 602 case "parameters": 603 n |= parametersNode 604 if seenStruct == "" || seenStruct == matches[1] { 605 seenStruct = matches[1] 606 } else { 607 return 0, fmt.Errorf("classifier: already annotated as %s, can't also be %q - %s", seenStruct, matches[1], cline.Text) 608 } 609 case "response": 610 n |= responseNode 611 if seenStruct == "" || seenStruct == matches[1] { 612 seenStruct = matches[1] 613 } else { 614 return 0, fmt.Errorf("classifier: already annotated as %s, can't also be %q - %s", seenStruct, matches[1], cline.Text) 615 } 616 case "strfmt", "name", "discriminated", "file", "enum", "default", "alias", "type": 617 // TODO: perhaps collect these and pass along to avoid lookups later on 618 case "allOf": 619 case "ignore": 620 default: 621 return 0, fmt.Errorf("classifier: unknown swagger annotation %q", matches[1]) 622 } 623 } 624 } 625 return n, nil 626} 627 628func debugLog(format string, args ...interface{}) { 629 if Debug { 630 log.Printf(format, args...) 631 } 632} 633