Skip to content

Commit 11527bf

Browse files
authored
Harden codegen contracts and stabilize examples
Make code generation fail fast on internal invariant violations, keep generators read-only after explicit normalization, and anchor generated examples to design identities.\n\nValidated with full Goa tests, lint, downstream regeneration comparisons, and PR CI.
1 parent c3df31f commit 11527bf

293 files changed

Lines changed: 11882 additions & 5302 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

cmd/goa/gen.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ func (g *Generator) Write(_ bool) error {
134134
codegen.SimpleImport("fmt"),
135135
codegen.SimpleImport("os"),
136136
codegen.SimpleImport("path/filepath"),
137+
codegen.SimpleImport("runtime/debug"),
137138
codegen.SimpleImport("sort"),
138139
codegen.SimpleImport("strconv"),
139140
codegen.SimpleImport("strings"),
@@ -366,7 +367,7 @@ const mainT = `func main() {
366367
{{- end }}
367368
368369
startGenerate := time.Now()
369-
outputs, err := generator.Generate(*out, {{ printf "%q" .Command }}, *debug)
370+
outputs, err := generate(*out, {{ printf "%q" .Command }}, *debug)
370371
if err != nil {
371372
fail(err.Error())
372373
}
@@ -377,6 +378,20 @@ const mainT = `func main() {
377378
fmt.Println(strings.Join(outputs, "\n"))
378379
}
379380
381+
// generate runs code generation and converts panics into a bug report
382+
// request: Goa generators panic on internal invariant violations, and this
383+
// recover is the single boundary turning them into actionable output. Design
384+
// errors never reach this point; eval.RunDSL reports them before generation.
385+
func generate(out, cmd string, dbg bool) ([]string, error) {
386+
defer func() {
387+
if r := recover(); r != nil {
388+
fmt.Fprintf(os.Stderr, "panic: %v\n\n%s\n", r, debug.Stack())
389+
fail("This is a bug in Goa, please report it at https://github.com/goadesign/goa/issues and include the stack trace above together with the design that triggered it.\n")
390+
}
391+
}()
392+
return generator.Generate(out, cmd, dbg)
393+
}
394+
380395
func fail(msg string, vals ...any) {
381396
fmt.Fprintf(os.Stderr, msg, vals...)
382397
os.Exit(1)

codegen/cli/cli.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,35 @@ type (
119119
CheckErr bool
120120
}
121121

122+
// FlagArgData describes a payload initialization argument from which a
123+
// command-line flag and the code that loads the flag value into the
124+
// corresponding payload builder field are generated.
125+
FlagArgData struct {
126+
// Name is the argument variable name used to derive the flag name and
127+
// the name of the local variable holding the flag value.
128+
Name string
129+
// TypeName is the argument Go type name.
130+
TypeName string
131+
// TypeRef is the reference to the argument type.
132+
TypeRef string
133+
// FieldName is the name of the payload field initialized with the
134+
// argument value if any.
135+
FieldName string
136+
// Description is the flag help text.
137+
Description string
138+
// Required is true if the flag is required.
139+
Required bool
140+
// Example is an example value for the flag.
141+
Example any
142+
// DefaultValue is the default value of the argument if any.
143+
DefaultValue any
144+
// Validate contains the validation code for the argument value if any.
145+
Validate string
146+
// OmitField if true generates the flag without a corresponding payload
147+
// builder field.
148+
OmitField bool
149+
}
150+
122151
// FieldData contains the data needed to generate the code that initializes a
123152
// field in the method payload type.
124153
FieldData struct {
@@ -245,6 +274,98 @@ func BuildSubcommandData(data *service.Data, m *service.MethodData, buildFunctio
245274
return sub
246275
}
247276

277+
// EndpointParserFile returns the file that implements the command line parser
278+
// that builds the client endpoint and payload necessary to perform a request.
279+
// The parse section renders the transport-specific ParseEndpoint function.
280+
func EndpointParserFile(
281+
path, title string,
282+
specs []*codegen.ImportSpec,
283+
data []*CommandData,
284+
parseSection *codegen.SectionTemplate,
285+
) *codegen.File {
286+
sections := make([]*codegen.SectionTemplate, 0, 4+len(data))
287+
sections = append(sections,
288+
codegen.Header(title, "cli", specs),
289+
UsageCommands(data),
290+
UsageExamples(data),
291+
parseSection,
292+
)
293+
for _, cmd := range data {
294+
sections = append(sections, CommandUsage(cmd))
295+
}
296+
return &codegen.File{Path: path, SectionTemplates: sections}
297+
}
298+
299+
// MakeFlags returns the flag data generated from the given payload
300+
// initialization arguments along with the data for the function that builds
301+
// the method payload from the corresponding flag values. payload and
302+
// payloadRef describe the method payload type, pinit - if not nil - describes
303+
// the payload constructor invoked by the build function.
304+
func MakeFlags(
305+
svcn string,
306+
m *service.MethodData,
307+
args []*FlagArgData,
308+
payload expr.DataType,
309+
payloadRef string,
310+
pinit *PayloadInitData,
311+
) ([]*FlagData, *BuildFunctionData) {
312+
var (
313+
fdata = make([]*FieldData, 0, len(args)) // preallocate
314+
flags = make([]*FlagData, len(args))
315+
params = make([]string, len(args))
316+
check bool
317+
)
318+
for i, arg := range args {
319+
f := NewFlagData(svcn, m.Name, arg.Name, arg.TypeName, arg.Description, arg.Required, arg.Example, arg.DefaultValue)
320+
flags[i] = f
321+
params[i] = f.FullName
322+
if arg.OmitField {
323+
continue
324+
}
325+
code, chek := FieldLoadCode(f, arg.Name, arg.TypeName, arg.Validate, arg.DefaultValue, payload, payloadRef)
326+
check = check || chek
327+
tn := arg.TypeRef
328+
if f.Type == "JSON" {
329+
// We need to declare the variable without
330+
// a pointer to be able to unmarshal the JSON
331+
// using its address.
332+
tn = arg.TypeName
333+
}
334+
fdata = append(fdata, &FieldData{
335+
Name: arg.Name,
336+
VarName: arg.Name,
337+
TypeRef: tn,
338+
Init: code,
339+
})
340+
}
341+
342+
return flags, &BuildFunctionData{
343+
Name: "Build" + m.VarName + "Payload",
344+
ActualParams: params,
345+
FormalParams: params,
346+
ServiceName: svcn,
347+
MethodName: m.Name,
348+
ResultType: payloadRef,
349+
Fields: fdata,
350+
PayloadInit: pinit,
351+
CheckErr: check,
352+
}
353+
}
354+
355+
// PayloadBuildersFile returns the file that contains the payload constructors
356+
// that use the command flag values as arguments.
357+
func PayloadBuildersFile(path, title string, specs []*codegen.ImportSpec, data *CommandData) *codegen.File {
358+
sections := []*codegen.SectionTemplate{
359+
codegen.Header(title, "client", specs),
360+
}
361+
for _, sub := range data.Subcommands {
362+
if sub.BuildFunction != nil {
363+
sections = append(sections, PayloadBuilderSection(sub.BuildFunction))
364+
}
365+
}
366+
return &codegen.File{Path: path, SectionTemplates: sections}
367+
}
368+
248369
// UsageCommands builds a section template that generates a help text showing
249370
// the list of allowed commands and sub-commands.
250371
func UsageCommands(data []*CommandData) *codegen.SectionTemplate {

codegen/example/example_server.go

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,24 @@ func ServerFiles(genpkg string, root *expr.RootExpr, services *service.ServicesD
2323
return fw
2424
}
2525

26+
// APIPkg returns a unique package name for the example API implementation
27+
// package derived from the API name. The name is registered with the given
28+
// scope so subsequent calls return distinct names.
29+
func APIPkg(root *expr.RootExpr, scope *codegen.NameScope) string {
30+
return scope.Unique(strings.ToLower(codegen.Goify(root.API.Name, false)), "api")
31+
}
32+
33+
// RootPath returns the Go import path of the project root computed from the
34+
// generated code package import path genpkg. It returns "." if genpkg has no
35+
// parent path.
36+
func RootPath(genpkg string) string {
37+
// genpkg is created by path.Join so the separator is / regardless of operating system
38+
if idx := strings.LastIndex(genpkg, "/"); idx > 0 {
39+
return genpkg[:idx]
40+
}
41+
return "."
42+
}
43+
2644
// exampleSvrMain returns the default main function for the given server
2745
// expression.
2846
func exampleSvrMain(genpkg string, root *expr.RootExpr, svr *expr.ServerExpr, services *service.ServicesData) *codegen.File {
@@ -62,19 +80,8 @@ func exampleSvrMain(genpkg string, root *expr.RootExpr, svr *expr.ServerExpr, se
6280
}
6381
interPkg := scope.Unique("interceptors", "ex")
6482

65-
var (
66-
rootPath string
67-
apiPkg string
68-
)
69-
{
70-
// genpkg is created by path.Join so the separator is / regardless of operating system
71-
idx := strings.LastIndex(genpkg, string("/"))
72-
rootPath = "."
73-
if idx > 0 {
74-
rootPath = genpkg[:idx]
75-
}
76-
apiPkg = scope.Unique(strings.ToLower(codegen.Goify(root.API.Name, false)), "api")
77-
}
83+
rootPath := RootPath(genpkg)
84+
apiPkg := APIPkg(root, scope)
7885
specs = append(specs, &codegen.ImportSpec{Path: rootPath, Name: apiPkg})
7986
if hasInterceptors {
8087
specs = append(specs, &codegen.ImportSpec{Path: path.Join(rootPath, "interceptors"), Name: interPkg})

codegen/generator/example.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,11 @@ func Example(genpkg string, roots []eval.Root) ([]*codegen.File, error) {
6060

6161
// JSON-RPC
6262
if len(r.API.JSONRPC.Services) > 0 {
63-
jsonrpcServices := httpcodegen.NewServicesData(services, &r.API.JSONRPC.HTTPExpr)
63+
jsonrpcServices := httpcodegen.NewJSONRPCServicesData(services, &r.API.JSONRPC.HTTPExpr)
6464
if fs := jsonrpccodegen.ExampleServerFiles(genpkg, jsonrpcServices, files); len(fs) > 0 {
6565
files = append(files, fs...)
6666
}
67-
if fs := jsonrpccodegen.ExampleCLIFiles(genpkg, jsonrpcServices); len(fs) > 0 {
67+
if fs := httpcodegen.ExampleCLIFiles(genpkg, jsonrpcServices); len(fs) > 0 {
6868
files = append(files, fs...)
6969
}
7070
}

codegen/generator/generate.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"goa.design/goa/v3/codegen"
1313
"goa.design/goa/v3/eval"
14+
"goa.design/goa/v3/expr"
1415
"golang.org/x/tools/go/packages"
1516
)
1617

@@ -100,13 +101,22 @@ func Generate(dir, cmd string, debug bool) (outputs []string, err1 error) {
100101
}
101102
}
102103

103-
// 4. Run the code pre generation plugins.
104+
// 4. Run the code pre generation plugins then normalize the design
105+
// roots. NormalizeRoot is the only sanctioned design mutation past eval
106+
// finalization; it runs after the prepare plugins so plugin contributed
107+
// endpoints are normalized too and before the generators so they all
108+
// observe the same read-only design tree.
104109
{
105110
start := time.Now()
106111
err := codegen.RunPluginsPrepare(cmd, genpkg, roots)
107112
if err != nil {
108113
return nil, err
109114
}
115+
for _, root := range roots {
116+
if r, ok := root.(*expr.RootExpr); ok {
117+
codegen.NormalizeRoot(r)
118+
}
119+
}
110120
if debug {
111121
fmt.Fprintf(os.Stderr, "[TIMING] [generate] Stage 4: Run pre-generation plugins took %v\n", time.Since(start))
112122
}

0 commit comments

Comments
 (0)