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