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