@@ -10,26 +10,27 @@ import (
1010 "go/types"
1111 "log"
1212 "os"
13+ "regexp"
1314 "strings"
1415
1516 "github.com/go-openapi/spec"
16- "github.com/go-openapi/swag"
17+ "github.com/go-openapi/swag/conv "
1718
1819 "golang.org/x/tools/go/packages"
1920)
2021
2122const pkgLoadMode = packages .NeedName | packages .NeedFiles | packages .NeedImports | packages .NeedDeps | packages .NeedTypes | packages .NeedSyntax | packages .NeedTypesInfo
2223
2324func safeConvert (str string ) bool {
24- b , err := swag .ConvertBool (str )
25+ b , err := conv .ConvertBool (str )
2526 if err != nil {
2627 return false
2728 }
2829 return b
2930}
3031
3132// Debug is true when process is run with DEBUG=1 env var.
32- var Debug = safeConvert (os .Getenv ("DEBUG" ))
33+ var Debug = safeConvert (os .Getenv ("DEBUG" )) //nolint:gochecknoglobals // package-level configuration from environment
3334
3435type node uint32
3536
@@ -286,49 +287,51 @@ func (d *entityDecl) HasParameterAnnotation() bool {
286287}
287288
288289func (s * scanCtx ) FindDecl (pkgPath , name string ) (* entityDecl , bool ) {
289- if pkg , ok := s .app .AllPackages [pkgPath ]; ok {
290- for _ , file := range pkg .Syntax {
291- for _ , d := range file .Decls {
292- gd , ok := d .(* ast.GenDecl )
290+ pkg , ok := s .app .AllPackages [pkgPath ]
291+ if ! ok {
292+ return nil , false
293+ }
294+
295+ for _ , file := range pkg .Syntax {
296+ for _ , d := range file .Decls {
297+ gd , ok := d .(* ast.GenDecl )
298+ if ! ok {
299+ continue
300+ }
301+
302+ for _ , sp := range gd .Specs {
303+ ts , ok := sp .(* ast.TypeSpec )
304+ if ! ok || ts .Name .Name != name {
305+ continue
306+ }
307+
308+ def , ok := pkg .TypesInfo .Defs [ts .Name ]
293309 if ! ok {
310+ debugLogf ("couldn't find type info for %s" , ts .Name )
294311 continue
295312 }
296313
297- for _ , sp := range gd .Specs {
298- if ts , ok := sp .(* ast.TypeSpec ); ok && ts .Name .Name == name {
299- def , ok := pkg .TypesInfo .Defs [ts .Name ]
300- if ! ok {
301- debugLogf ("couldn't find type info for %s" , ts .Name )
302-
303- continue
304- }
305-
306- nt , isNamed := def .Type ().(* types.Named )
307- at , isAliased := def .Type ().(* types.Alias )
308- if ! isNamed && ! isAliased {
309- debugLogf ("%s is not a named or an aliased type but a %T" , ts .Name , def .Type ())
310-
311- continue
312- }
313-
314- comments := ts .Doc // type ( /* doc */ Foo struct{} )
315- if comments == nil {
316- comments = gd .Doc // /* doc */ type ( Foo struct{} )
317- }
318-
319- decl := & entityDecl {
320- Comments : comments ,
321- Type : nt ,
322- Alias : at ,
323- Ident : ts .Name ,
324- Spec : ts ,
325- File : file ,
326- Pkg : pkg ,
327- }
328-
329- return decl , true
330- }
314+ nt , isNamed := def .Type ().(* types.Named )
315+ at , isAliased := def .Type ().(* types.Alias )
316+ if ! isNamed && ! isAliased {
317+ debugLogf ("%s is not a named or an aliased type but a %T" , ts .Name , def .Type ())
318+ continue
331319 }
320+
321+ comments := ts .Doc // type ( /* doc */ Foo struct{} )
322+ if comments == nil {
323+ comments = gd .Doc // /* doc */ type ( Foo struct{} )
324+ }
325+
326+ return & entityDecl {
327+ Comments : comments ,
328+ Type : nt ,
329+ Alias : at ,
330+ Ident : ts .Name ,
331+ Spec : ts ,
332+ File : file ,
333+ Pkg : pkg ,
334+ }, true
332335 }
333336 }
334337 }
@@ -612,64 +615,72 @@ func (a *typeIndex) processPackage(pkg *packages.Package) error {
612615 }
613616
614617 for _ , file := range pkg .Syntax {
615- n , err := a .detectNodes (file )
616- if err != nil {
618+ if err := a .processFile (pkg , file ); err != nil {
617619 return err
618620 }
621+ }
619622
620- if n & metaNode != 0 {
621- a .Meta = append (a .Meta , metaSection {Comments : file .Doc })
622- }
623+ return nil
624+ }
623625
624- if n & operationNode != 0 {
625- for _ , cmts := range file .Comments {
626- pp := parsePathAnnotation (rxOperation , cmts .List )
627- if pp .Method == "" {
628- continue // not a valid operation
629- }
630- if ! shouldAcceptTag (pp .Tags , a .includeTags , a .excludeTags ) {
631- debugLogf ("operation %s %s is ignored due to tag rules" , pp .Method , pp .Path )
632- continue
633- }
634- a .Operations = append (a .Operations , pp )
635- }
636- }
626+ func (a * typeIndex ) processFile (pkg * packages.Package , file * ast.File ) error {
627+ n , err := a .detectNodes (file )
628+ if err != nil {
629+ return err
630+ }
637631
638- if n & routeNode != 0 {
639- for _ , cmts := range file .Comments {
640- pp := parsePathAnnotation (rxRoute , cmts .List )
641- if pp .Method == "" {
642- continue // not a valid operation
643- }
644- if ! shouldAcceptTag (pp .Tags , a .includeTags , a .excludeTags ) {
645- debugLogf ("operation %s %s is ignored due to tag rules" , pp .Method , pp .Path )
646- continue
647- }
648- a .Routes = append (a .Routes , pp )
649- }
632+ if n & metaNode != 0 {
633+ a .Meta = append (a .Meta , metaSection {Comments : file .Doc })
634+ }
635+
636+ if n & operationNode != 0 {
637+ a .Operations = a .collectPathAnnotations (rxOperation , file .Comments , a .Operations )
638+ }
639+
640+ if n & routeNode != 0 {
641+ a .Routes = a .collectPathAnnotations (rxRoute , file .Comments , a .Routes )
642+ }
643+
644+ a .processFileDecls (pkg , file , n )
645+
646+ return nil
647+ }
648+
649+ func (a * typeIndex ) collectPathAnnotations (rx * regexp.Regexp , comments []* ast.CommentGroup , dst []parsedPathContent ) []parsedPathContent {
650+ for _ , cmts := range comments {
651+ pp := parsePathAnnotation (rx , cmts .List )
652+ if pp .Method == "" {
653+ continue
650654 }
655+ if ! shouldAcceptTag (pp .Tags , a .includeTags , a .excludeTags ) {
656+ debugLogf ("operation %s %s is ignored due to tag rules" , pp .Method , pp .Path )
657+ continue
658+ }
659+ dst = append (dst , pp )
660+ }
661+ return dst
662+ }
651663
652- for _ , dt := range file .Decls {
653- switch fd := dt .(type ) {
654- case * ast.BadDecl :
664+ func (a * typeIndex ) processFileDecls (pkg * packages.Package , file * ast.File , n node ) {
665+ for _ , dt := range file .Decls {
666+ switch fd := dt .(type ) {
667+ case * ast.BadDecl :
668+ continue
669+ case * ast.FuncDecl :
670+ if fd .Body == nil {
655671 continue
656- case * ast.FuncDecl :
657- if fd .Body == nil {
658- continue
659- }
660- for _ , stmt := range fd .Body .List {
661- if dstm , ok := stmt .(* ast.DeclStmt ); ok {
662- if gd , isGD := dstm .Decl .(* ast.GenDecl ); isGD {
663- a .processDecl (pkg , file , n , gd )
664- }
672+ }
673+ for _ , stmt := range fd .Body .List {
674+ if dstm , ok := stmt .(* ast.DeclStmt ); ok {
675+ if gd , isGD := dstm .Decl .(* ast.GenDecl ); isGD {
676+ a .processDecl (pkg , file , n , gd )
665677 }
666678 }
667- case * ast.GenDecl :
668- a .processDecl (pkg , file , n , fd )
669679 }
680+ case * ast.GenDecl :
681+ a .processDecl (pkg , file , n , fd )
670682 }
671683 }
672- return nil
673684}
674685
675686func (a * typeIndex ) processDecl (pkg * packages.Package , file * ast.File , n node , gd * ast.GenDecl ) {
@@ -748,10 +759,23 @@ func (a *typeIndex) walkImports(pkg *packages.Package) error {
748759 return nil
749760}
750761
762+ func checkStructConflict (seenStruct * string , annotation string , text string ) error {
763+ if * seenStruct != "" && * seenStruct != annotation {
764+ return fmt .Errorf ("classifier: already annotated as %s, can't also be %q - %s: %w" , * seenStruct , annotation , text , ErrCodeScan )
765+ }
766+ * seenStruct = annotation
767+ return nil
768+ }
769+
770+ // detectNodes scans all comment groups in a file and returns a bitmask of
771+ // detected swagger annotation types. Node types like route, operation, and
772+ // meta accumulate freely across comment groups. Struct-level annotations
773+ // (model, parameters, response) are mutually exclusive within a single
774+ // comment group — mixing them is an error.
751775func (a * typeIndex ) detectNodes (file * ast.File ) (node , error ) {
752776 var n node
753777 for _ , comments := range file .Comments {
754- var seenStruct string
778+ var seenStruct string // tracks the struct annotation for this comment group
755779 for _ , cline := range comments .List {
756780 if cline == nil {
757781 continue
@@ -764,7 +788,7 @@ func (a *typeIndex) detectNodes(file *ast.File) (node, error) {
764788 }
765789
766790 matches := rxSwaggerAnnotation .FindStringSubmatch (cline .Text )
767- if len (matches ) < 2 {
791+ if len (matches ) < minAnnotationMatch {
768792 continue
769793 }
770794
@@ -775,41 +799,36 @@ func (a *typeIndex) detectNodes(file *ast.File) (node, error) {
775799 n |= operationNode
776800 case "model" :
777801 n |= modelNode
778- if seenStruct == "" || seenStruct == matches [1 ] {
779- seenStruct = matches [1 ]
780- } else {
781- return 0 , fmt .Errorf ("classifier: already annotated as %s, can't also be %q - %s" , seenStruct , matches [1 ], cline .Text )
802+ if err := checkStructConflict (& seenStruct , matches [1 ], cline .Text ); err != nil {
803+ return 0 , err
782804 }
783805 case "meta" :
784806 n |= metaNode
785807 case "parameters" :
786808 n |= parametersNode
787- if seenStruct == "" || seenStruct == matches [1 ] {
788- seenStruct = matches [1 ]
789- } else {
790- return 0 , fmt .Errorf ("classifier: already annotated as %s, can't also be %q - %s" , seenStruct , matches [1 ], cline .Text )
809+ if err := checkStructConflict (& seenStruct , matches [1 ], cline .Text ); err != nil {
810+ return 0 , err
791811 }
792812 case "response" :
793813 n |= responseNode
794- if seenStruct == "" || seenStruct == matches [1 ] {
795- seenStruct = matches [1 ]
796- } else {
797- return 0 , fmt .Errorf ("classifier: already annotated as %s, can't also be %q - %s" , seenStruct , matches [1 ], cline .Text )
814+ if err := checkStructConflict (& seenStruct , matches [1 ], cline .Text ); err != nil {
815+ return 0 , err
798816 }
799- case "strfmt" , "name" , "discriminated" , "file" , "enum" , "default" , "alias" , "type" :
817+ case "strfmt" , paramNameKey , "discriminated" , "file" , "enum" , "default" , "alias" , "type" :
800818 // TODO: perhaps collect these and pass along to avoid lookups later on
801819 case "allOf" :
802820 case "ignore" :
803821 default :
804- return 0 , fmt .Errorf ("classifier: unknown swagger annotation %q" , matches [1 ])
822+ return 0 , fmt .Errorf ("classifier: unknown swagger annotation %q: %w " , matches [1 ], ErrCodeScan )
805823 }
806824 }
807825 }
826+
808827 return n , nil
809828}
810829
811830func debugLogf (format string , args ... any ) {
812831 if Debug {
813- _ = log .Output (2 , fmt .Sprintf (format , args ... ))
832+ _ = log .Output (logCallerDepth , fmt .Sprintf (format , args ... ))
814833 }
815834}
0 commit comments