Skip to content

Commit 44ce6a0

Browse files
committed
feat: add non-fatal warning system for code generation diagnostics
Add a warning infrastructure that mirrors the existing error reporting system but is non-fatal. The first use case is warning about pointers as map values, which is supported but not recommended per Kubernetes API conventions. - Add Package.Warnings field and AddWarning method to pkg/loader - Add PrintWarnings and VisitPackages helper functions - Add Runtime.PrintWarnings method to pkg/genall - Call PrintWarnings after successful generation in controller-gen - Emit warning when pointers are used as map values in CRD schemas - Add tests for pointer map value warning and non-warning cases
1 parent e045852 commit 44ce6a0

5 files changed

Lines changed: 167 additions & 0 deletions

File tree

cmd/controller-gen/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,8 @@ func main() {
188188
// don't obscure the actual error with a bunch of usage
189189
return noUsageError{fmt.Errorf("not all generators ran successfully")}
190190
}
191+
192+
rt.PrintWarnings()
191193
return nil
192194
},
193195
SilenceUsage: true, // silence the usage, then print it out ourselves if it wasn't suppressed

pkg/crd/schema.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,7 @@ func mapToSchema(ctx *schemaContext, mapType *ast.MapType) *apiextensionsv1.JSON
414414
case *ast.ArrayType:
415415
valSchema = arrayToSchema(ctx.ForInfo(&markers.TypeInfo{}), val)
416416
case *ast.StarExpr:
417+
ctx.pkg.AddWarning(loader.ErrFromNode(fmt.Errorf("using pointers as map values is not recommended per Kubernetes API conventions"), mapType.Value))
417418
valSchema = typeToSchema(ctx.ForInfo(&markers.TypeInfo{}), val)
418419
case *ast.MapType:
419420
valSchema = typeToSchema(ctx.ForInfo(&markers.TypeInfo{}), val)

pkg/crd/schema_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,80 @@ func (m *testapplyFirstMarker) ApplyToSchema(*crdmarkers.SchemaContext, *apiexte
176176
m.callback()
177177
return nil
178178
}
179+
180+
func Test_Schema_PointerMapValue_Warning(t *testing.T) {
181+
g := gomega.NewWithT(t)
182+
183+
moduleName := "sigs.k8s.io/controller-tools/pkg/crd"
184+
modules := []pkgstest.Module{
185+
{
186+
Name: moduleName,
187+
Files: map[string]any{
188+
"test.go": `package crd
189+
type Test map[string]*string
190+
`,
191+
},
192+
},
193+
}
194+
195+
pkgs, exported, err := testloader.LoadFakeRoots(pkgstest.Modules, modules, moduleName)
196+
if exported != nil {
197+
t.Cleanup(exported.Cleanup)
198+
}
199+
if err != nil {
200+
t.Fatalf("unable to load fake package: %s", err)
201+
}
202+
if len(pkgs) != 1 {
203+
t.Fatal("expected to parse only one package")
204+
}
205+
206+
pkg := pkgs[0]
207+
pkg.NeedTypesInfo()
208+
failIfErrors(t, pkg.Errors)
209+
210+
schemaContext := newSchemaContext(pkg, nil, true, false).ForInfo(&markers.TypeInfo{})
211+
definedType := pkg.Syntax[0].Decls[0].(*ast.GenDecl).Specs[0].(*ast.TypeSpec).Type
212+
typeToSchema(schemaContext, definedType)
213+
214+
g.Expect(pkg.Errors).To(gomega.BeEmpty(), "pointer map values should not produce errors")
215+
g.Expect(pkg.Warnings).To(gomega.HaveLen(1), "expected exactly one warning")
216+
g.Expect(pkg.Warnings[0].Msg).To(gomega.ContainSubstring("pointers as map values"))
217+
}
218+
219+
func Test_Schema_NonPointerMapValue_NoWarning(t *testing.T) {
220+
g := gomega.NewWithT(t)
221+
222+
moduleName := "sigs.k8s.io/controller-tools/pkg/crd"
223+
modules := []pkgstest.Module{
224+
{
225+
Name: moduleName,
226+
Files: map[string]any{
227+
"test.go": `package crd
228+
type Test map[string]string
229+
`,
230+
},
231+
},
232+
}
233+
234+
pkgs, exported, err := testloader.LoadFakeRoots(pkgstest.Modules, modules, moduleName)
235+
if exported != nil {
236+
t.Cleanup(exported.Cleanup)
237+
}
238+
if err != nil {
239+
t.Fatalf("unable to load fake package: %s", err)
240+
}
241+
if len(pkgs) != 1 {
242+
t.Fatal("expected to parse only one package")
243+
}
244+
245+
pkg := pkgs[0]
246+
pkg.NeedTypesInfo()
247+
failIfErrors(t, pkg.Errors)
248+
249+
schemaContext := newSchemaContext(pkg, nil, true, false).ForInfo(&markers.TypeInfo{})
250+
definedType := pkg.Syntax[0].Decls[0].(*ast.GenDecl).Specs[0].(*ast.TypeSpec).Type
251+
typeToSchema(schemaContext, definedType)
252+
253+
g.Expect(pkg.Errors).To(gomega.BeEmpty())
254+
g.Expect(pkg.Warnings).To(gomega.BeEmpty(), "non-pointer map values should not produce warnings")
255+
}

pkg/genall/genall.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,3 +281,8 @@ func (r *Runtime) Run() bool {
281281
// skip TypeErrors -- they're probably just from partial typechecking in crd-gen
282282
return loader.PrintErrors(r.Roots, packages.TypeError) || hadErrs
283283
}
284+
285+
// PrintWarnings prints any non-fatal warnings accumulated during generation.
286+
func (r *Runtime) PrintWarnings() {
287+
loader.PrintWarnings(r.Roots)
288+
}

pkg/loader/loader.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,41 @@ func PrintErrors(pkgs []*Package, filterKinds ...packages.ErrorKind) bool {
6363
return hadErrors
6464
}
6565

66+
// PrintWarnings prints warnings associated with all packages
67+
// in the given package graph, starting at the given root
68+
// packages and traversing through all imports. It will
69+
// return true if any warnings were printed.
70+
func PrintWarnings(pkgs []*Package) bool {
71+
hadWarnings := false
72+
VisitPackages(pkgs, func(pkg *Package) {
73+
for _, warn := range pkg.Warnings {
74+
hadWarnings = true
75+
fmt.Fprintln(os.Stderr, "Warning: ", warn)
76+
}
77+
})
78+
return hadWarnings
79+
}
80+
81+
// VisitPackages visits all packages reachable from the given root packages,
82+
// calling the given visitor for each unique package exactly once.
83+
func VisitPackages(pkgs []*Package, visitor func(*Package)) {
84+
visited := make(map[*Package]struct{})
85+
var visit func(pkg *Package)
86+
visit = func(pkg *Package) {
87+
if _, done := visited[pkg]; done {
88+
return
89+
}
90+
visited[pkg] = struct{}{}
91+
visitor(pkg)
92+
for _, imported := range pkg.Imports() {
93+
visit(imported)
94+
}
95+
}
96+
for _, pkg := range pkgs {
97+
visit(pkg)
98+
}
99+
}
100+
66101
// Package is a single, unique Go package that can be
67102
// lazily parsed and type-checked. Packages should not
68103
// be constructed directly -- instead, use LoadRoots.
@@ -72,6 +107,10 @@ func PrintErrors(pkgs []*Package, filterKinds ...packages.ErrorKind) bool {
72107
type Package struct {
73108
*packages.Package
74109

110+
// Warnings stores non-fatal warnings encountered during code generation.
111+
// Unlike errors, warnings do not cause generation to fail.
112+
Warnings []packages.Error
113+
75114
imports map[string]*Package
76115

77116
loader *loader
@@ -177,6 +216,49 @@ func (p *Package) AddError(err error) {
177216
}
178217
}
179218

219+
// AddWarning adds a non-fatal warning to the warnings associated with the
220+
// given package. It accepts the same error types as AddError.
221+
func (p *Package) AddWarning(err error) {
222+
switch typedErr := err.(type) {
223+
case *os.PathError:
224+
p.Warnings = append(p.Warnings, packages.Error{
225+
Pos: typedErr.Path + ":1",
226+
Msg: typedErr.Err.Error(),
227+
Kind: packages.ParseError,
228+
})
229+
case scanner.ErrorList:
230+
for _, subErr := range typedErr {
231+
p.Warnings = append(p.Warnings, packages.Error{
232+
Pos: subErr.Pos.String(),
233+
Msg: subErr.Msg,
234+
Kind: packages.ParseError,
235+
})
236+
}
237+
case types.Error:
238+
p.Warnings = append(p.Warnings, packages.Error{
239+
Pos: typedErr.Fset.Position(typedErr.Pos).String(),
240+
Msg: typedErr.Msg,
241+
Kind: packages.TypeError,
242+
})
243+
case ErrList:
244+
for _, subErr := range typedErr {
245+
p.AddWarning(subErr)
246+
}
247+
case PositionedError:
248+
p.Warnings = append(p.Warnings, packages.Error{
249+
Pos: p.loader.cfg.Fset.Position(typedErr.Pos).String(),
250+
Msg: typedErr.Error(),
251+
Kind: packages.UnknownError,
252+
})
253+
default:
254+
p.Warnings = append(p.Warnings, packages.Error{
255+
Pos: p.ID + ":-",
256+
Msg: err.Error(),
257+
Kind: packages.UnknownError,
258+
})
259+
}
260+
}
261+
180262
// loader loads packages and their imports. Loaded packages will have
181263
// type size, imports, and exports file information populated. Additional
182264
// information, like ASTs and type-checking information, can be accessed

0 commit comments

Comments
 (0)