github.com/go-swagger/go-swagger@v0.31.0/codescan/application.go (about) 1 package codescan 2 3 import ( 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 19 const pkgLoadMode = packages.NeedName | packages.NeedFiles | packages.NeedImports | packages.NeedDeps | packages.NeedTypes | packages.NeedSyntax | packages.NeedTypesInfo 20 21 func 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 30 var Debug = safeConvert(os.Getenv("DEBUG")) 31 32 type node uint32 33 34 const ( 35 metaNode node = 1 << iota 36 routeNode 37 operationNode 38 modelNode 39 parametersNode 40 responseNode 41 ) 42 43 // Options for the scanner 44 type 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 57 type scanCtx struct { 58 pkgs []*packages.Package 59 app *typeIndex 60 } 61 62 func 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 71 func 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 80 func 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 108 type 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 120 func (d *entityDecl) Names() (name, goName string) { 121 goName = d.Ident.Name 122 name = goName 123 if d.Comments == nil { 124 return 125 } 126 127 DECLS: 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 143 func (d *entityDecl) ResponseNames() (name, goName string) { 144 goName = d.Ident.Name 145 name = goName 146 if d.Comments == nil { 147 return 148 } 149 150 DECLS: 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 166 func (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 190 func (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 209 func (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 228 func (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 247 func (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 269 comments := ts.Doc // type ( /* doc */ Foo struct{} ) 270 if comments == nil { 271 comments = gd.Doc // /* doc */ type ( Foo struct{} ) 272 } 273 274 decl := &entityDecl{ 275 Comments: comments, 276 Type: nt, 277 Ident: ts.Name, 278 Spec: ts, 279 File: file, 280 Pkg: pkg, 281 } 282 return decl, true 283 } 284 } 285 } 286 } 287 } 288 return nil, false 289 } 290 291 func (s *scanCtx) FindModel(pkgPath, name string) (*entityDecl, bool) { 292 for _, cand := range s.app.Models { 293 ct := cand.Type.Obj() 294 if ct.Name() == name && ct.Pkg().Path() == pkgPath { 295 return cand, true 296 } 297 } 298 if decl, found := s.FindDecl(pkgPath, name); found { 299 s.app.ExtraModels[decl.Ident] = decl 300 return decl, true 301 } 302 return nil, false 303 } 304 305 func (s *scanCtx) PkgForPath(pkgPath string) (*packages.Package, bool) { 306 v, ok := s.app.AllPackages[pkgPath] 307 return v, ok 308 } 309 310 func (s *scanCtx) DeclForType(t types.Type) (*entityDecl, bool) { 311 switch tpe := t.(type) { 312 case *types.Pointer: 313 return s.DeclForType(tpe.Elem()) 314 case *types.Named: 315 return s.FindDecl(tpe.Obj().Pkg().Path(), tpe.Obj().Name()) 316 317 default: 318 log.Printf("unknown type to find the package for [%T]: %s", t, t.String()) 319 return nil, false 320 } 321 } 322 323 func (s *scanCtx) PkgForType(t types.Type) (*packages.Package, bool) { 324 switch tpe := t.(type) { 325 // case *types.Basic: 326 // case *types.Struct: 327 // case *types.Pointer: 328 // case *types.Interface: 329 // case *types.Array: 330 // case *types.Slice: 331 // case *types.Map: 332 case *types.Named: 333 v, ok := s.app.AllPackages[tpe.Obj().Pkg().Path()] 334 return v, ok 335 default: 336 log.Printf("unknown type to find the package for [%T]: %s", t, t.String()) 337 return nil, false 338 } 339 } 340 341 func (s *scanCtx) FindComments(pkg *packages.Package, name string) (*ast.CommentGroup, bool) { 342 for _, f := range pkg.Syntax { 343 for _, d := range f.Decls { 344 gd, ok := d.(*ast.GenDecl) 345 if !ok { 346 continue 347 } 348 349 for _, s := range gd.Specs { 350 if ts, ok := s.(*ast.TypeSpec); ok { 351 if ts.Name.Name == name { 352 return gd.Doc, true 353 } 354 } 355 } 356 } 357 } 358 return nil, false 359 } 360 361 func (s *scanCtx) FindEnumValues(pkg *packages.Package, enumName string) (list []interface{}, descList []string, _ bool) { 362 for _, f := range pkg.Syntax { 363 for _, d := range f.Decls { 364 gd, ok := d.(*ast.GenDecl) 365 if !ok { 366 continue 367 } 368 369 if gd.Tok != token.CONST { 370 continue 371 } 372 373 for _, s := range gd.Specs { 374 if vs, ok := s.(*ast.ValueSpec); ok { 375 if vsIdent, ok := vs.Type.(*ast.Ident); ok { 376 if vsIdent.Name == enumName { 377 if len(vs.Values) > 0 { 378 if bl, ok := vs.Values[0].(*ast.BasicLit); ok { 379 blValue := getEnumBasicLitValue(bl) 380 list = append(list, blValue) 381 382 // build the enum description 383 var ( 384 desc = &strings.Builder{} 385 namesLen = len(vs.Names) 386 ) 387 desc.WriteString(fmt.Sprintf("%v ", blValue)) 388 for i, name := range vs.Names { 389 desc.WriteString(name.Name) 390 if i < namesLen-1 { 391 desc.WriteString(" ") 392 } 393 } 394 if vs.Doc != nil { 395 docListLen := len(vs.Doc.List) 396 if docListLen > 0 { 397 desc.WriteString(" ") 398 } 399 for i, doc := range vs.Doc.List { 400 if doc.Text != "" { 401 text := strings.TrimPrefix(doc.Text, "//") 402 desc.WriteString(text) 403 if i < docListLen-1 { 404 desc.WriteString(" ") 405 } 406 } 407 } 408 } 409 descList = append(descList, desc.String()) 410 } 411 } 412 } 413 } 414 } 415 } 416 } 417 } 418 return list, descList, true 419 } 420 421 func newTypeIndex(pkgs []*packages.Package, excludeDeps bool, includeTags, excludeTags map[string]bool, includePkgs, excludePkgs []string) (*typeIndex, error) { 422 ac := &typeIndex{ 423 AllPackages: make(map[string]*packages.Package), 424 Models: make(map[*ast.Ident]*entityDecl), 425 ExtraModels: make(map[*ast.Ident]*entityDecl), 426 excludeDeps: excludeDeps, 427 includeTags: includeTags, 428 excludeTags: excludeTags, 429 includePkgs: includePkgs, 430 excludePkgs: excludePkgs, 431 } 432 if err := ac.build(pkgs); err != nil { 433 return nil, err 434 } 435 return ac, nil 436 } 437 438 type typeIndex struct { 439 AllPackages map[string]*packages.Package 440 Models map[*ast.Ident]*entityDecl 441 ExtraModels map[*ast.Ident]*entityDecl 442 Meta []metaSection 443 Routes []parsedPathContent 444 Operations []parsedPathContent 445 Parameters []*entityDecl 446 Responses []*entityDecl 447 excludeDeps bool 448 includeTags map[string]bool 449 excludeTags map[string]bool 450 includePkgs []string 451 excludePkgs []string 452 } 453 454 func (a *typeIndex) build(pkgs []*packages.Package) error { 455 for _, pkg := range pkgs { 456 if _, known := a.AllPackages[pkg.PkgPath]; known { 457 continue 458 } 459 a.AllPackages[pkg.PkgPath] = pkg 460 if err := a.processPackage(pkg); err != nil { 461 return err 462 } 463 if err := a.walkImports(pkg); err != nil { 464 return err 465 } 466 } 467 468 return nil 469 } 470 471 func (a *typeIndex) processPackage(pkg *packages.Package) error { 472 if !shouldAcceptPkg(pkg.PkgPath, a.includePkgs, a.excludePkgs) { 473 debugLog("package %s is ignored due to rules", pkg.Name) 474 return nil 475 } 476 477 for _, file := range pkg.Syntax { 478 n, err := a.detectNodes(file) 479 if err != nil { 480 return err 481 } 482 483 if n&metaNode != 0 { 484 a.Meta = append(a.Meta, metaSection{Comments: file.Doc}) 485 } 486 487 if n&operationNode != 0 { 488 for _, cmts := range file.Comments { 489 pp := parsePathAnnotation(rxOperation, cmts.List) 490 if pp.Method == "" { 491 continue // not a valid operation 492 } 493 if !shouldAcceptTag(pp.Tags, a.includeTags, a.excludeTags) { 494 debugLog("operation %s %s is ignored due to tag rules", pp.Method, pp.Path) 495 continue 496 } 497 a.Operations = append(a.Operations, pp) 498 } 499 } 500 501 if n&routeNode != 0 { 502 for _, cmts := range file.Comments { 503 pp := parsePathAnnotation(rxRoute, cmts.List) 504 if pp.Method == "" { 505 continue // not a valid operation 506 } 507 if !shouldAcceptTag(pp.Tags, a.includeTags, a.excludeTags) { 508 debugLog("operation %s %s is ignored due to tag rules", pp.Method, pp.Path) 509 continue 510 } 511 a.Routes = append(a.Routes, pp) 512 } 513 } 514 515 for _, dt := range file.Decls { 516 switch fd := dt.(type) { 517 case *ast.BadDecl: 518 continue 519 case *ast.FuncDecl: 520 if fd.Body == nil { 521 continue 522 } 523 for _, stmt := range fd.Body.List { 524 if dstm, ok := stmt.(*ast.DeclStmt); ok { 525 if gd, isGD := dstm.Decl.(*ast.GenDecl); isGD { 526 a.processDecl(pkg, file, n, gd) 527 } 528 } 529 } 530 case *ast.GenDecl: 531 a.processDecl(pkg, file, n, fd) 532 } 533 } 534 } 535 return nil 536 } 537 538 func (a *typeIndex) processDecl(pkg *packages.Package, file *ast.File, n node, gd *ast.GenDecl) { 539 for _, sp := range gd.Specs { 540 switch ts := sp.(type) { 541 case *ast.ValueSpec: 542 debugLog("saw value spec: %v", ts.Names) 543 return 544 case *ast.ImportSpec: 545 debugLog("saw import spec: %v", ts.Name) 546 return 547 case *ast.TypeSpec: 548 def, ok := pkg.TypesInfo.Defs[ts.Name] 549 if !ok { 550 debugLog("couldn't find type info for %s", ts.Name) 551 continue 552 } 553 nt, isNamed := def.Type().(*types.Named) 554 if !isNamed { 555 debugLog("%s is not a named type but a %T", ts.Name, def.Type()) 556 continue 557 } 558 559 comments := ts.Doc // type ( /* doc */ Foo struct{} ) 560 if comments == nil { 561 comments = gd.Doc // /* doc */ type ( Foo struct{} ) 562 } 563 564 decl := &entityDecl{ 565 Comments: comments, 566 Type: nt, 567 Ident: ts.Name, 568 Spec: ts, 569 File: file, 570 Pkg: pkg, 571 } 572 key := ts.Name 573 if n&modelNode != 0 && decl.HasModelAnnotation() { 574 a.Models[key] = decl 575 } 576 if n¶metersNode != 0 && decl.HasParameterAnnotation() { 577 a.Parameters = append(a.Parameters, decl) 578 } 579 if n&responseNode != 0 && decl.HasResponseAnnotation() { 580 a.Responses = append(a.Responses, decl) 581 } 582 } 583 } 584 } 585 586 func (a *typeIndex) walkImports(pkg *packages.Package) error { 587 if a.excludeDeps { 588 return nil 589 } 590 for _, v := range pkg.Imports { 591 if _, known := a.AllPackages[v.PkgPath]; known { 592 continue 593 } 594 595 a.AllPackages[v.PkgPath] = v 596 if err := a.processPackage(v); err != nil { 597 return err 598 } 599 if err := a.walkImports(v); err != nil { 600 return err 601 } 602 } 603 return nil 604 } 605 606 func (a *typeIndex) detectNodes(file *ast.File) (node, error) { 607 var n node 608 for _, comments := range file.Comments { 609 var seenStruct string 610 for _, cline := range comments.List { 611 if cline == nil { 612 continue 613 } 614 } 615 616 for _, cline := range comments.List { 617 if cline == nil { 618 continue 619 } 620 621 matches := rxSwaggerAnnotation.FindStringSubmatch(cline.Text) 622 if len(matches) < 2 { 623 continue 624 } 625 626 switch matches[1] { 627 case "route": 628 n |= routeNode 629 case "operation": 630 n |= operationNode 631 case "model": 632 n |= modelNode 633 if seenStruct == "" || seenStruct == matches[1] { 634 seenStruct = matches[1] 635 } else { 636 return 0, fmt.Errorf("classifier: already annotated as %s, can't also be %q - %s", seenStruct, matches[1], cline.Text) 637 } 638 case "meta": 639 n |= metaNode 640 case "parameters": 641 n |= parametersNode 642 if seenStruct == "" || seenStruct == matches[1] { 643 seenStruct = matches[1] 644 } else { 645 return 0, fmt.Errorf("classifier: already annotated as %s, can't also be %q - %s", seenStruct, matches[1], cline.Text) 646 } 647 case "response": 648 n |= responseNode 649 if seenStruct == "" || seenStruct == matches[1] { 650 seenStruct = matches[1] 651 } else { 652 return 0, fmt.Errorf("classifier: already annotated as %s, can't also be %q - %s", seenStruct, matches[1], cline.Text) 653 } 654 case "strfmt", "name", "discriminated", "file", "enum", "default", "alias", "type": 655 // TODO: perhaps collect these and pass along to avoid lookups later on 656 case "allOf": 657 case "ignore": 658 default: 659 return 0, fmt.Errorf("classifier: unknown swagger annotation %q", matches[1]) 660 } 661 } 662 } 663 return n, nil 664 } 665 666 func debugLog(format string, args ...interface{}) { 667 if Debug { 668 log.Printf(format, args...) 669 } 670 }