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