@@ -17,15 +17,21 @@ package gotypes
17
17
import (
18
18
"bytes"
19
19
"fmt"
20
+ goast "go/ast"
20
21
goformat "go/format"
22
+ goparser "go/parser"
23
+ gotoken "go/token"
21
24
"maps"
22
25
"os"
23
26
"path/filepath"
24
27
"slices"
28
+ "strconv"
25
29
"strings"
26
30
"unicode"
27
31
"unicode/utf8"
28
32
33
+ goastutil "golang.org/x/tools/go/ast/astutil"
34
+
29
35
"cuelang.org/go/cue"
30
36
"cuelang.org/go/cue/ast"
31
37
"cuelang.org/go/cue/build"
@@ -61,7 +67,7 @@ func Generate(ctx *cue.Context, insts ...*build.Instance) error {
61
67
g .pkg = inst
62
68
g .emitDefs = nil
63
69
g .pkgRoot = instVal
64
- g .importedAs = make (map [string ]string )
70
+ g .importCuePkgAsGoPkg = make (map [string ]string )
65
71
66
72
iter , err := instVal .Fields (cue .Definitions (true ))
67
73
if err != nil {
@@ -83,7 +89,7 @@ func Generate(ctx *cue.Context, insts ...*build.Instance) error {
83
89
// TODO: we should refuse to generate for packages which are not
84
90
// part of the main module, as they may be inside the read-only module cache.
85
91
for _ , imp := range inst .Imports {
86
- if ! instDone [imp ] && g .importedAs [imp .ImportPath ] != "" {
92
+ if ! instDone [imp ] && g .importCuePkgAsGoPkg [imp .ImportPath ] != "" {
87
93
insts = append (insts , imp )
88
94
}
89
95
}
@@ -100,11 +106,11 @@ func Generate(ctx *cue.Context, insts ...*build.Instance) error {
100
106
goPkgNamesDoneByDir [inst .Dir ] = goPkgName
101
107
}
102
108
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 {
106
112
printf ("import (\n " )
107
- for _ , path := range imported {
113
+ for _ , path := range importedGo {
108
114
printf ("\t %q\n " , path )
109
115
}
110
116
printf (")\n " )
@@ -195,12 +201,12 @@ type generator struct {
195
201
// emitDefs records paths for the definitions we should emit as Go types.
196
202
emitDefs []cue.Path
197
203
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.
199
205
// This is collected as we emit types, given that some CUE fields and types are omitted
200
206
// and we don't want to end up with unused Go imports.
201
207
//
202
208
// 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
204
210
205
211
// pkgRoot is the root value of the CUE package, necessary to tell if a referenced value
206
212
// belongs to the current package or not.
@@ -225,6 +231,8 @@ type generatedDef struct {
225
231
inProgress bool
226
232
227
233
// 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.
228
236
src []byte
229
237
}
230
238
@@ -289,16 +297,35 @@ func (g *generator) emitType(val cue.Value, optional bool, optionalStg optionalS
289
297
}
290
298
}
291
299
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 ())
302
329
return nil
303
330
}
304
331
switch {
@@ -449,6 +476,41 @@ func (g *generator) emitType(val cue.Value, optional bool, optionalStg optionalS
449
476
return nil
450
477
}
451
478
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
+
452
514
func cutLast (s , sep string ) (before , after string , found bool ) {
453
515
if i := strings .LastIndex (s , sep ); i >= 0 {
454
516
return s [:i ], s [i + len (sep ):], true
@@ -584,7 +646,7 @@ func (g *generator) emitTypeReference(val cue.Value) bool {
584
646
// we need to ensure that package is imported.
585
647
// Otherwise, we need to ensure that the referenced local definition is generated.
586
648
if root != g .pkgRoot {
587
- g .importedAs [inst .ImportPath ] = unqualifiedPath
649
+ g .importCuePkgAsGoPkg [inst .ImportPath ] = unqualifiedPath
588
650
} else {
589
651
g .genDef (path , cue .Dereference (val ))
590
652
}
0 commit comments