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