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&parametersNode != 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  }