@@ -5,6 +5,8 @@ package schemagen // import "go.opentelemetry.io/collector/cmd/builder/internal/
55
66import (
77 "fmt"
8+ "go/ast"
9+ "go/token"
810 "go/types"
911
1012 "golang.org/x/tools/go/packages"
@@ -55,33 +57,163 @@ func (pa *PackageAnalyzer) LoadPackage(importPath string) (*packages.Package, er
5557}
5658
5759// FindConfigType locates the Config struct in a component package.
58- // It looks for a type named "Config" that is a struct.
60+ // It first searches for compile-time check patterns like:
61+ //
62+ // var _ component.Config = (*ConfigType)(nil)
63+ //
64+ // If no such pattern is found, it falls back to looking for a type named "Config".
5965func (pa * PackageAnalyzer ) FindConfigType (pkg * packages.Package ) (* types.Named , error ) {
6066 if pkg .Types == nil {
6167 return nil , fmt .Errorf ("package %s has no type information" , pkg .PkgPath )
6268 }
6369
70+ // Search AST for compile-time check pattern
71+ configTypeName := pa .findConfigTypeFromAST (pkg )
72+ if configTypeName != "" {
73+ return pa .findConfigTypeByName (pkg , configTypeName )
74+ }
75+
76+ // Fallback to looking for "Config" type by name
77+ return pa .findConfigTypeByName (pkg , "Config" )
78+ }
79+
80+ // findConfigTypeByName looks up a type by name in the package scope.
81+ func (pa * PackageAnalyzer ) findConfigTypeByName (pkg * packages.Package , typeName string ) (* types.Named , error ) {
6482 scope := pkg .Types .Scope ()
6583
66- // Look for "Config" type
67- obj := scope .Lookup ("Config" )
84+ obj := scope .Lookup (typeName )
6885 if obj == nil {
69- return nil , fmt .Errorf ("no Config type found in package %s" , pkg .PkgPath )
86+ return nil , fmt .Errorf ("no %s type found in package %s" , typeName , pkg .PkgPath )
7087 }
7188
7289 named , ok := obj .Type ().(* types.Named )
7390 if ! ok {
74- return nil , fmt .Errorf ("config in package %s is not a named type" , pkg .PkgPath )
91+ return nil , fmt .Errorf ("%s in package %s is not a named type" , typeName , pkg .PkgPath )
7592 }
7693
7794 // Verify it's a struct
7895 if _ , ok := named .Underlying ().(* types.Struct ); ! ok {
79- return nil , fmt .Errorf ("config in package %s is not a struct" , pkg .PkgPath )
96+ return nil , fmt .Errorf ("%s in package %s is not a struct" , typeName , pkg .PkgPath )
8097 }
8198
8299 return named , nil
83100}
84101
102+ // findConfigTypeFromAST searches for compile-time check patterns in the package AST.
103+ // It looks for patterns like: var _ component.Config = (*ConfigType)(nil)
104+ func (pa * PackageAnalyzer ) findConfigTypeFromAST (pkg * packages.Package ) string {
105+ for _ , file := range pkg .Syntax {
106+ if typeName := pa .findConfigTypeInFile (file ); typeName != "" {
107+ return typeName
108+ }
109+ }
110+ return ""
111+ }
112+
113+ // findConfigTypeInFile searches a single file for the compile-time check pattern.
114+ func (pa * PackageAnalyzer ) findConfigTypeInFile (file * ast.File ) string {
115+ for _ , decl := range file .Decls {
116+ genDecl , ok := decl .(* ast.GenDecl )
117+ if ! ok || genDecl .Tok != token .VAR {
118+ continue
119+ }
120+
121+ for _ , spec := range genDecl .Specs {
122+ valueSpec , ok := spec .(* ast.ValueSpec )
123+ if ! ok {
124+ continue
125+ }
126+
127+ if typeName := pa .extractConfigTypeFromValueSpec (valueSpec ); typeName != "" {
128+ return typeName
129+ }
130+ }
131+ }
132+ return ""
133+ }
134+
135+ // extractConfigTypeFromValueSpec checks if a ValueSpec matches the pattern:
136+ // var _ component.Config = (*ConfigType)(nil)
137+ func (pa * PackageAnalyzer ) extractConfigTypeFromValueSpec (spec * ast.ValueSpec ) string {
138+ // Must have exactly one name and it must be blank identifier
139+ if len (spec .Names ) != 1 || spec .Names [0 ].Name != "_" {
140+ return ""
141+ }
142+
143+ // Check if the type is component.Config
144+ if ! pa .isComponentConfigType (spec .Type ) {
145+ return ""
146+ }
147+
148+ // Must have exactly one value
149+ if len (spec .Values ) != 1 {
150+ return ""
151+ }
152+
153+ // Extract type name from (*TypeName)(nil)
154+ return pa .extractTypeFromConversionExpr (spec .Values [0 ])
155+ }
156+
157+ // isComponentConfigType checks if the type expression represents component.Config.
158+ func (pa * PackageAnalyzer ) isComponentConfigType (typeExpr ast.Expr ) bool {
159+ sel , ok := typeExpr .(* ast.SelectorExpr )
160+ if ! ok {
161+ return false
162+ }
163+
164+ // Check for "Config" selector
165+ if sel .Sel .Name != "Config" {
166+ return false
167+ }
168+
169+ // Check for "component" package identifier
170+ ident , ok := sel .X .(* ast.Ident )
171+ if ! ok {
172+ return false
173+ }
174+
175+ return ident .Name == "component"
176+ }
177+
178+ // extractTypeFromConversionExpr extracts the type name from a (*TypeName)(nil) expression.
179+ func (pa * PackageAnalyzer ) extractTypeFromConversionExpr (expr ast.Expr ) string {
180+ // The expression should be a call expression: (*TypeName)(nil)
181+ callExpr , ok := expr .(* ast.CallExpr )
182+ if ! ok {
183+ return ""
184+ }
185+
186+ // Should have exactly one argument (nil)
187+ if len (callExpr .Args ) != 1 {
188+ return ""
189+ }
190+
191+ // Verify the argument is nil
192+ ident , ok := callExpr .Args [0 ].(* ast.Ident )
193+ if ! ok || ident .Name != "nil" {
194+ return ""
195+ }
196+
197+ // Fun should be a parenthesized star expression: (*TypeName)
198+ parenExpr , ok := callExpr .Fun .(* ast.ParenExpr )
199+ if ! ok {
200+ return ""
201+ }
202+
203+ starExpr , ok := parenExpr .X .(* ast.StarExpr )
204+ if ! ok {
205+ return ""
206+ }
207+
208+ // Extract the type name
209+ typeIdent , ok := starExpr .X .(* ast.Ident )
210+ if ! ok {
211+ return ""
212+ }
213+
214+ return typeIdent .Name
215+ }
216+
85217// GetStructFromNamed extracts the underlying struct type from a named type.
86218func GetStructFromNamed (named * types.Named ) (* types.Struct , bool ) {
87219 st , ok := named .Underlying ().(* types.Struct )
0 commit comments