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