Skip to content

Commit d7ca401

Browse files
committed
Make it more robust
Signed-off-by: Pavol Loffay <[email protected]>
1 parent 9e5edeb commit d7ca401

File tree

5 files changed

+164
-19
lines changed

5 files changed

+164
-19
lines changed

cmd/builder/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ require (
2626
github.com/davecgh/go-spew v1.1.1 // indirect
2727
github.com/fsnotify/fsnotify v1.9.0 // indirect
2828
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
29-
github.com/hashicorp/go-version v1.7.0 // indirect
29+
github.com/hashicorp/go-version v1.8.0 // indirect
3030
github.com/inconshreveable/mousetrap v1.1.0 // indirect
3131
github.com/json-iterator/go v1.1.12 // indirect
3232
github.com/knadh/koanf/maps v0.1.2 // indirect

cmd/builder/go.sum

Lines changed: 2 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/builder/internal/schemagen/analyzer.go

Lines changed: 138 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ package schemagen // import "go.opentelemetry.io/collector/cmd/builder/internal/
55

66
import (
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".
5965
func (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.
86218
func GetStructFromNamed(named *types.Named) (*types.Struct, bool) {
87219
st, ok := named.Underlying().(*types.Struct)

cmd/builder/internal/schemagen/generator_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,20 @@ func TestPackageAnalyzer_NewPackageAnalyzer(t *testing.T) {
372372
assert.Equal(t, tempDir, analyzer.cfg.Dir)
373373
}
374374

375+
func TestPackageAnalyzer_FindConfigType_WithCompileTimeCheck(t *testing.T) {
376+
cwd, err := os.Getwd()
377+
require.NoError(t, err)
378+
379+
// testcomponent uses "MySettings" (not "Config") to verify AST-based detection
380+
analyzer := NewPackageAnalyzer(filepath.Join(cwd, "..", ".."))
381+
pkg, err := analyzer.LoadPackage("go.opentelemetry.io/collector/cmd/builder/internal/schemagen/testdata/testcomponent")
382+
require.NoError(t, err)
383+
384+
configType, err := analyzer.FindConfigType(pkg)
385+
require.NoError(t, err)
386+
assert.Equal(t, "MySettings", configType.Obj().Name())
387+
}
388+
375389
func TestBasicTypeToSchema(t *testing.T) {
376390
tests := []struct {
377391
name string

cmd/builder/internal/schemagen/testdata/testcomponent/config.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,21 @@
44
// Package testcomponent provides a test configuration for schema generation testing.
55
// It covers all basic Go types, special types (time.Duration), nested structs,
66
// slices, maps, and deprecation detection.
7+
// The config type is intentionally named "MySettings" (not "Config") to test
8+
// that schema generation detects config types via compile-time checks.
79
package testcomponent // import "go.opentelemetry.io/collector/cmd/builder/internal/schemagen/testdata/testcomponent"
810

911
import (
1012
"time"
13+
14+
"go.opentelemetry.io/collector/component"
1115
)
1216

13-
// Config is the configuration for the test component.
14-
type Config struct {
17+
// Compile-time check that MySettings implements component.Config.
18+
var _ component.Config = (*MySettings)(nil)
19+
20+
// MySettings is the configuration for the test component.
21+
type MySettings struct {
1522
// Name is the name of the component.
1623
Name string `mapstructure:"name"`
1724

0 commit comments

Comments
 (0)