Skip to content

Commit f2e0b7e

Browse files
committed
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 Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/1214345 TryBot-Result: CUEcueckoo <[email protected]> Reviewed-by: Roger Peppe <[email protected]> Unity-Result: CUE porcuepine <[email protected]>
1 parent d0f10ea commit f2e0b7e

File tree

3 files changed

+124
-29
lines changed

3 files changed

+124
-29
lines changed

cmd/cue/cmd/exp.go

Lines changed: 4 additions & 2 deletions
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

Lines changed: 16 additions & 8 deletions
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:127: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:127: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,16 @@ _#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)
423+
// See the TODO around semicolons being inserted on newlines.
424+
// attrTypeStruct?: _#overridenNeverGenerate @go(,type=struct{
425+
// AttrTypeStructOne int
426+
// AttrTypeStructTwo string
427+
// })
422428

423429
attrIgnoreNeverGenerate?: int @go(-)
424430

@@ -501,9 +507,9 @@ _#unusedHiddenStruct: neverGenerate?: int
501507

502508
field?: int
503509
}
504-
#attrType: _#overridenNeverGenerate @go(,type=go/constant.Kind)
510+
#attrType: _#overridenNeverGenerate @go(,type="go/constant".Kind)
505511
#attrTypeEmbed: {
506-
@go(,type=go/constant.Kind) // obeyed even when the type has a different kind
512+
@go(,type="go/constant".Kind) // obeyed even when the type has a different kind
507513

508514
neverGenerate?: int
509515
}
@@ -741,6 +747,8 @@ type Root struct {
741747

742748
AttrTypeNested map[any]any `json:"attrTypeNested,omitempty"`
743749

750+
AttrTypeComplex *[]*constant.Kind `json:"attrTypeComplex,omitempty"`
751+
744752
// doc is a great field.
745753
//
746754
// It deserves multiple paragraphs of documentation

internal/encoding/gotypes/generate.go

Lines changed: 104 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,22 @@ package gotypes
1717
import (
1818
"bytes"
1919
"fmt"
20+
goast "go/ast"
2021
goformat "go/format"
22+
goparser "go/parser"
23+
goscanner "go/scanner"
24+
gotoken "go/token"
2125
"maps"
2226
"os"
2327
"path/filepath"
2428
"slices"
29+
"strconv"
2530
"strings"
2631
"unicode"
2732
"unicode/utf8"
2833

34+
goastutil "golang.org/x/tools/go/ast/astutil"
35+
2936
"cuelang.org/go/cue"
3037
"cuelang.org/go/cue/ast"
3138
"cuelang.org/go/cue/build"
@@ -61,7 +68,7 @@ func Generate(ctx *cue.Context, insts ...*build.Instance) error {
6168
g.pkg = inst
6269
g.emitDefs = nil
6370
g.pkgRoot = instVal
64-
g.importedAs = make(map[string]string)
71+
g.importCuePkgAsGoPkg = make(map[string]string)
6572

6673
iter, err := instVal.Fields(cue.Definitions(true))
6774
if err != nil {
@@ -83,7 +90,7 @@ func Generate(ctx *cue.Context, insts ...*build.Instance) error {
8390
// TODO: we should refuse to generate for packages which are not
8491
// part of the main module, as they may be inside the read-only module cache.
8592
for _, imp := range inst.Imports {
86-
if !instDone[imp] && g.importedAs[imp.ImportPath] != "" {
93+
if !instDone[imp] && g.importCuePkgAsGoPkg[imp.ImportPath] != "" {
8794
insts = append(insts, imp)
8895
}
8996
}
@@ -100,11 +107,11 @@ func Generate(ctx *cue.Context, insts ...*build.Instance) error {
100107
goPkgNamesDoneByDir[inst.Dir] = goPkgName
101108
}
102109
printf("package %s\n\n", goPkgName)
103-
imported := slices.Sorted(maps.Values(g.importedAs))
104-
imported = slices.Compact(imported)
105-
if len(imported) > 0 {
110+
importedGo := slices.Sorted(maps.Values(g.importCuePkgAsGoPkg))
111+
importedGo = slices.Compact(importedGo)
112+
if len(importedGo) > 0 {
106113
printf("import (\n")
107-
for _, path := range imported {
114+
for _, path := range importedGo {
108115
printf("\t%q\n", path)
109116
}
110117
printf(")\n")
@@ -195,12 +202,12 @@ type generator struct {
195202
// emitDefs records paths for the definitions we should emit as Go types.
196203
emitDefs []cue.Path
197204

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

205212
// pkgRoot is the root value of the CUE package, necessary to tell if a referenced value
206213
// belongs to the current package or not.
@@ -225,6 +232,8 @@ type generatedDef struct {
225232
inProgress bool
226233

227234
// src is the generated Go type expression source.
235+
// We generate types as plaintext Go source rather than [goast.Expr]
236+
// as the latter makes it very hard to use empty lines and comment placement correctly.
228237
src []byte
229238
}
230239

@@ -289,16 +298,35 @@ func (g *generator) emitType(val cue.Value, optional bool, optionalStg optionalS
289298
}
290299
}
291300
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)
301+
fset := gotoken.NewFileSet()
302+
expr, importedByName, err := parseTypeExpr(fset, attrType)
303+
if err != nil {
304+
return fmt.Errorf("cannot parse @go type expression: %w", err)
305+
}
306+
for _, pkgPath := range importedByName {
307+
g.importCuePkgAsGoPkg[pkgPath] = pkgPath
308+
}
309+
// Collect any remaining imports from selectors on unquoted single-element std packages
310+
// such as `@go(,type=io.Reader)`.
311+
expr = goastutil.Apply(expr, func(c *goastutil.Cursor) bool {
312+
if sel, _ := c.Node().(*goast.SelectorExpr); sel != nil {
313+
if imp, _ := sel.X.(*goast.Ident); imp != nil {
314+
if importedByName[imp.Name] != "" {
315+
// `@go(,type="go/constant".Kind)` ends up being parsed as the Go expression `constant.Kind`;
316+
// via importedByName we can tell that "constant" is already provided via "go/constant".
317+
return true
318+
}
319+
g.importCuePkgAsGoPkg[imp.Name] = imp.Name
320+
}
321+
}
322+
return true
323+
}, nil).(goast.Expr)
324+
var buf bytes.Buffer
325+
// We emit in plaintext, so format the parsed Go expression and print it out.
326+
if err := goformat.Node(&buf, fset, expr); err != nil {
327+
return err
328+
}
329+
g.def.printf("%s", buf.Bytes())
302330
return nil
303331
}
304332
switch {
@@ -449,6 +477,63 @@ func (g *generator) emitType(val cue.Value, optional bool, optionalStg optionalS
449477
return nil
450478
}
451479

480+
// parseTypeExpr extends [goparser.ParseExpr] to allow selecting from full import paths.
481+
// `[]go/constant.Kind` is not a valid Go expression, and `[]constant.Kind` is valid
482+
// but doesn't specify a full import path, so it's ambiguous.
483+
//
484+
// Accept `[]"go/constant".Kind` with a pre-processing step to find quoted strings,
485+
// record them as imports keyed by package name in the returned map,
486+
// and rewrite the Go expression to be in terms of the imported package.
487+
// Note that a pre-processing step is necessary as ParseExpr rejects this custom syntax.
488+
func parseTypeExpr(fset *gotoken.FileSet, src string) (goast.Expr, map[string]string, error) {
489+
var goSrc strings.Builder
490+
importedByName := make(map[string]string)
491+
492+
var scan goscanner.Scanner
493+
scan.Init(fset.AddFile("", fset.Base(), len(src)), []byte(src), nil, 0)
494+
lastStringLit := ""
495+
for {
496+
_, tok, lit := scan.Scan()
497+
if tok == gotoken.EOF {
498+
break
499+
}
500+
if lastStringLit != "" {
501+
if tok == gotoken.PERIOD {
502+
imp, err := strconv.Unquote(lastStringLit)
503+
if err != nil {
504+
panic(err) // should never happen
505+
}
506+
// We assume the package name is the last path component.
507+
// TODO: consider how we might support renaming imports,
508+
// so that importing both foo.com/x and bar.com/x is possible.
509+
_, impName, _ := cutLast(imp, "/")
510+
importedByName[impName] = imp
511+
goSrc.WriteString(impName)
512+
} else {
513+
goSrc.WriteString(lastStringLit)
514+
}
515+
lastStringLit = ""
516+
}
517+
switch tok {
518+
case gotoken.STRING:
519+
lastStringLit = lit
520+
case gotoken.IDENT, gotoken.INT, gotoken.FLOAT, gotoken.IMAG, gotoken.CHAR:
521+
goSrc.WriteString(lit)
522+
case gotoken.SEMICOLON:
523+
// TODO: How can we support multi-line types such as structs?
524+
// Note that EOF inserts a semicolon, which breaks goparser.ParseExpr.
525+
if lit == "\n" {
526+
break // inserted semicolon at EOF
527+
}
528+
fallthrough
529+
default:
530+
goSrc.WriteString(tok.String())
531+
}
532+
}
533+
expr, err := goparser.ParseExpr(goSrc.String())
534+
return expr, importedByName, err
535+
}
536+
452537
func cutLast(s, sep string) (before, after string, found bool) {
453538
if i := strings.LastIndex(s, sep); i >= 0 {
454539
return s[:i], s[i+len(sep):], true
@@ -584,7 +669,7 @@ func (g *generator) emitTypeReference(val cue.Value) bool {
584669
// we need to ensure that package is imported.
585670
// Otherwise, we need to ensure that the referenced local definition is generated.
586671
if root != g.pkgRoot {
587-
g.importedAs[inst.ImportPath] = unqualifiedPath
672+
g.importCuePkgAsGoPkg[inst.ImportPath] = unqualifiedPath
588673
} else {
589674
g.genDef(path, cue.Dereference(val))
590675
}

0 commit comments

Comments
 (0)