Skip to content

Commit a2f1482

Browse files
mvdancueckoo
authored andcommitted
internal/encoding/gotypes: parse type options as Go expressions
We were naively cutting strings to find Go named type selectors such as `go/constant.Kind` so we could import the package and refer to it by name correctly in the resulting Go type expression. This, however, quickly broke down with more complex type expressions such as `[]go/constant.Kind`. Moreover, such a string is not a valid Go expression, so we came up with a solution in parseTypeExpr which allows quoting import paths to avoid syntactic issues. Fixes #3841. Signed-off-by: Daniel Martí <[email protected]> Change-Id: Ib5db43daa038b689a6bccd99b6170435bc6bf5d5 Dispatch-Trailer: {"type":"trybot","CL":1214345,"patchset":2,"ref":"refs/changes/45/1214345/2","targetBranch":"master"}
1 parent 3ba376f commit a2f1482

File tree

3 files changed

+96
-29
lines changed

3 files changed

+96
-29
lines changed

cmd/cue/cmd/exp.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,10 @@ given that Go does not allow declaring nested types.
7474
7575
#Bar: {
7676
@go(BetterBarTypeName)
77-
renamed: int @go(BetterFieldName)
78-
retyped: string @go(,type=foo.com/bar.NamedString)
77+
renamed: int @go(BetterFieldName)
78+
79+
retypedLocal: [...string] @go(,type=[]LocalType)
80+
retypedImport: [...string] @go(,type=[]"foo.com/bar".ImportedType)
7981
}
8082
8183
The attribute "@go(-)" can be used to ignore a definition or field, for example:

cmd/cue/cmd/testdata/script/exp_gengotypes.txtar

+11-8
Original file line numberDiff line numberDiff line change
@@ -188,10 +188,10 @@ fail.both."42_LinkedList".types.LinkedList.next: conflicting values "x" and {ite
188188
./root/types.cue:43:14
189189
fail.both.notList: conflicting values [1,2,3] and {embedded2?:int} (mismatched types list and struct):
190190
./cuetest/all.cue:5:24
191-
./root/root.cue:121:2
191+
./root/root.cue:122:2
192192
fail.both.notString: conflicting values "not_a_struct" and {embedded2?:int} (mismatched types string and struct):
193193
./cuetest/all.cue:4:24
194-
./root/root.cue:121:2
194+
./root/root.cue:122:2
195195
fail.cue."11_Int8".types.Int8: invalid value 99999 (out of bound <=127):
196196
./cuetest/all.cue:61:30
197197
fail.cue."12_Int8".types.Int8: invalid value -99999 (out of bound >=-128):
@@ -400,7 +400,7 @@ _#overridenNeverGenerate: string
400400

401401
nestedStructAttrNillable: {
402402
optionalStruct?: #emptyStruct
403-
optionalStructAttrType?: #emptyStruct @go(,type=EmptyStruct)
403+
optionalStructAttrType?: #emptyStruct @go(,type=EmptyStruct) // overrides optional=nillable
404404
optionalStructAttrZero?: #emptyStruct @go(,optional=zero)
405405
} @go(,optional=nillable)
406406

@@ -415,10 +415,11 @@ _#overridenNeverGenerate: string
415415

416416
attrName?: int @go(AttrChangedName)
417417
attrNameNested?: int @go(AttrChangedNameNested)
418-
attrType?: _#overridenNeverGenerate @go(,type=go/constant.Kind)
418+
attrType?: _#overridenNeverGenerate @go(,type="go/constant".Kind)
419419
// For compatibility with `cue get go`, an unnamed second value is also a type.
420-
attrTypeCompat?: _#overridenNeverGenerate @go(,go/token.Token)
421-
attrTypeNested?: {one?: two?: three?: int} @go(,type=map[any]any)
420+
attrTypeCompat?: _#overridenNeverGenerate @go(,"go/token".Token)
421+
attrTypeNested?: {one?: two?: three?: int} @go(,type=map[any]any)
422+
attrTypeComplex?: _#overridenNeverGenerate @go(,type=*[]*"go/constant".Kind)
422423

423424
attrIgnoreNeverGenerate?: int @go(-)
424425

@@ -501,9 +502,9 @@ _#unusedHiddenStruct: neverGenerate?: int
501502

502503
field?: int
503504
}
504-
#attrType: _#overridenNeverGenerate @go(,type=go/constant.Kind)
505+
#attrType: _#overridenNeverGenerate @go(,type="go/constant".Kind)
505506
#attrTypeEmbed: {
506-
@go(,type=go/constant.Kind) // obeyed even when the type has a different kind
507+
@go(,type="go/constant".Kind) // obeyed even when the type has a different kind
507508

508509
neverGenerate?: int
509510
}
@@ -741,6 +742,8 @@ type Root struct {
741742

742743
AttrTypeNested map[any]any `json:"attrTypeNested,omitempty"`
743744

745+
AttrTypeComplex *[]*constant.Kind `json:"attrTypeComplex,omitempty"`
746+
744747
// doc is a great field.
745748
//
746749
// It deserves multiple paragraphs of documentation

internal/encoding/gotypes/generate.go

+81-19
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,21 @@ package gotypes
1717
import (
1818
"bytes"
1919
"fmt"
20+
goast "go/ast"
2021
goformat "go/format"
22+
goparser "go/parser"
23+
gotoken "go/token"
2124
"maps"
2225
"os"
2326
"path/filepath"
2427
"slices"
28+
"strconv"
2529
"strings"
2630
"unicode"
2731
"unicode/utf8"
2832

33+
goastutil "golang.org/x/tools/go/ast/astutil"
34+
2935
"cuelang.org/go/cue"
3036
"cuelang.org/go/cue/ast"
3137
"cuelang.org/go/cue/build"
@@ -61,7 +67,7 @@ func Generate(ctx *cue.Context, insts ...*build.Instance) error {
6167
g.pkg = inst
6268
g.emitDefs = nil
6369
g.pkgRoot = instVal
64-
g.importedAs = make(map[string]string)
70+
g.importCuePkgAsGoPkg = make(map[string]string)
6571

6672
iter, err := instVal.Fields(cue.Definitions(true))
6773
if err != nil {
@@ -83,7 +89,7 @@ func Generate(ctx *cue.Context, insts ...*build.Instance) error {
8389
// TODO: we should refuse to generate for packages which are not
8490
// part of the main module, as they may be inside the read-only module cache.
8591
for _, imp := range inst.Imports {
86-
if !instDone[imp] && g.importedAs[imp.ImportPath] != "" {
92+
if !instDone[imp] && g.importCuePkgAsGoPkg[imp.ImportPath] != "" {
8793
insts = append(insts, imp)
8894
}
8995
}
@@ -100,11 +106,11 @@ func Generate(ctx *cue.Context, insts ...*build.Instance) error {
100106
goPkgNamesDoneByDir[inst.Dir] = goPkgName
101107
}
102108
printf("package %s\n\n", goPkgName)
103-
imported := slices.Sorted(maps.Values(g.importedAs))
104-
imported = slices.Compact(imported)
105-
if len(imported) > 0 {
109+
importedGo := slices.Sorted(maps.Values(g.importCuePkgAsGoPkg))
110+
importedGo = slices.Compact(importedGo)
111+
if len(importedGo) > 0 {
106112
printf("import (\n")
107-
for _, path := range imported {
113+
for _, path := range importedGo {
108114
printf("\t%q\n", path)
109115
}
110116
printf(")\n")
@@ -195,12 +201,12 @@ type generator struct {
195201
// emitDefs records paths for the definitions we should emit as Go types.
196202
emitDefs []cue.Path
197203

198-
// importedAs records which CUE packages need to be imported as which Go packages in the generated Go package.
204+
// importCuePkgAsGoPkg records which CUE packages need to be imported as which Go packages in the generated Go package.
199205
// This is collected as we emit types, given that some CUE fields and types are omitted
200206
// and we don't want to end up with unused Go imports.
201207
//
202208
// The keys are full CUE import paths; the values are their resulting Go import paths.
203-
importedAs map[string]string
209+
importCuePkgAsGoPkg map[string]string
204210

205211
// pkgRoot is the root value of the CUE package, necessary to tell if a referenced value
206212
// belongs to the current package or not.
@@ -225,6 +231,8 @@ type generatedDef struct {
225231
inProgress bool
226232

227233
// src is the generated Go type expression source.
234+
// We generate types as plaintext Go source rather than [goast.Expr]
235+
// as the latter makes it very hard to use empty lines and comment placement correctly.
228236
src []byte
229237
}
230238

@@ -289,16 +297,35 @@ func (g *generator) emitType(val cue.Value, optional bool, optionalStg optionalS
289297
}
290298
}
291299
if attrType != "" {
292-
pkgPath, _, ok := cutLast(attrType, ".")
293-
if ok {
294-
// For "type=foo.Name", we need to ensure that "foo" is imported.
295-
g.importedAs[pkgPath] = pkgPath
296-
// For "type=foo/bar.Name", the selector is just "bar.Name".
297-
// Note that this doesn't support Go packages whose name does not match
298-
// the last element of their import path. That seems OK for now.
299-
_, attrType, _ = cutLast(attrType, "/")
300-
}
301-
g.def.printf("%s", attrType)
300+
expr, importedByName, err := parseTypeExpr(attrType)
301+
if err != nil {
302+
return fmt.Errorf("cannot parse @go type expression: %w", err)
303+
}
304+
for _, pkgPath := range importedByName {
305+
g.importCuePkgAsGoPkg[pkgPath] = pkgPath
306+
}
307+
// Collect any remaining imports from selectors on unquoted single-element std packages
308+
// such as `@go(,type=io.Reader)`.
309+
expr = goastutil.Apply(expr, func(c *goastutil.Cursor) bool {
310+
if sel, _ := c.Node().(*goast.SelectorExpr); sel != nil {
311+
if imp, _ := sel.X.(*goast.Ident); imp != nil {
312+
if importedByName[imp.Name] != "" {
313+
// `@go(,type="go/constant".Kind)` ends up being parsed as the Go expression `constant.Kind`;
314+
// via importedByName we can tell that "constant" is already provided via "go/constant".
315+
return true
316+
}
317+
g.importCuePkgAsGoPkg[imp.Name] = imp.Name
318+
}
319+
}
320+
return true
321+
}, nil).(goast.Expr)
322+
var buf bytes.Buffer
323+
// We emit in plaintext, so format the parsed Go expression and print it out.
324+
// Note that fileset positions don't matter, as we parsed a single line of Go.
325+
if err := goformat.Node(&buf, gotoken.NewFileSet(), expr); err != nil {
326+
return err
327+
}
328+
g.def.printf("%s", buf.Bytes())
302329
return nil
303330
}
304331
switch {
@@ -449,6 +476,41 @@ func (g *generator) emitType(val cue.Value, optional bool, optionalStg optionalS
449476
return nil
450477
}
451478

479+
// parseTypeExpr extends [goparser.ParseExpr] to allow selecting from full import paths.
480+
// `[]go/constant.Kind` is not a valid Go expression, and `[]constant.Kind` is valid
481+
// but doesn't specify a full import path, so it's ambiguous.
482+
// Accept `[]"go/constant".Kind` with a pre-processing step to find quoted strings,
483+
// record them as imports, and rewrite the Go expression to be in terms of the imported package.
484+
func parseTypeExpr(src string) (goast.Expr, map[string]string, error) {
485+
var goSrc strings.Builder
486+
importedByName := make(map[string]string)
487+
// Note that we assume all quoted strings will be Go import paths,
488+
// because quoted strings are otherwise extremely rare in Go type expressions.
489+
for len(src) > 0 {
490+
start := strings.IndexByte(src, '"')
491+
if start < 0 {
492+
goSrc.WriteString(src)
493+
break
494+
}
495+
quoted, err := strconv.QuotedPrefix(src[start:])
496+
if err != nil {
497+
return nil, nil, err
498+
}
499+
imp, err := strconv.Unquote(quoted)
500+
if err != nil {
501+
panic(err) // should never happen
502+
}
503+
// We assume the package name is the last path component.
504+
_, impName, _ := cutLast(imp, "/")
505+
importedByName[impName] = imp
506+
goSrc.WriteString(src[:start])
507+
goSrc.WriteString(impName)
508+
src = src[start+len(quoted):]
509+
}
510+
expr, err := goparser.ParseExpr(goSrc.String())
511+
return expr, importedByName, err
512+
}
513+
452514
func cutLast(s, sep string) (before, after string, found bool) {
453515
if i := strings.LastIndex(s, sep); i >= 0 {
454516
return s[:i], s[i+len(sep):], true
@@ -584,7 +646,7 @@ func (g *generator) emitTypeReference(val cue.Value) bool {
584646
// we need to ensure that package is imported.
585647
// Otherwise, we need to ensure that the referenced local definition is generated.
586648
if root != g.pkgRoot {
587-
g.importedAs[inst.ImportPath] = unqualifiedPath
649+
g.importCuePkgAsGoPkg[inst.ImportPath] = unqualifiedPath
588650
} else {
589651
g.genDef(path, cue.Dereference(val))
590652
}

0 commit comments

Comments
 (0)