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