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