cuelang.org/go@v0.13.0/internal/encoding/gotypes/generate.go (about)

     1  // Copyright 2024 CUE Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package gotypes
    16  
    17  import (
    18  	"bytes"
    19  	"fmt"
    20  	goast "go/ast"
    21  	goformat "go/format"
    22  	goparser "go/parser"
    23  	goscanner "go/scanner"
    24  	gotoken "go/token"
    25  	"maps"
    26  	"os"
    27  	"path/filepath"
    28  	"slices"
    29  	"strconv"
    30  	"strings"
    31  	"unicode"
    32  	"unicode/utf8"
    33  
    34  	goastutil "golang.org/x/tools/go/ast/astutil"
    35  
    36  	"cuelang.org/go/cue"
    37  	"cuelang.org/go/cue/ast"
    38  	"cuelang.org/go/cue/build"
    39  )
    40  
    41  // Generate produces Go type definitions from exported CUE definitions.
    42  // See the help text for `cue help exp gengotypes`.
    43  func Generate(ctx *cue.Context, insts ...*build.Instance) error {
    44  	// record which package instances have already been generated
    45  	instDone := make(map[*build.Instance]bool)
    46  
    47  	goPkgNamesDoneByDir := make(map[string]string)
    48  
    49  	g := generator{generatedTypes: make(map[qualifiedPath]*generatedDef)}
    50  
    51  	// ensure we don't modify the parameter slice
    52  	insts = slices.Clip(insts)
    53  	for len(insts) > 0 { // we append imports to this list
    54  		inst := insts[0]
    55  		insts = insts[1:]
    56  		if err := inst.Err; err != nil {
    57  			return err
    58  		}
    59  		if instDone[inst] {
    60  			continue
    61  		}
    62  		instDone[inst] = true
    63  
    64  		instVal := ctx.BuildInstance(inst)
    65  		if err := instVal.Validate(); err != nil {
    66  			return err
    67  		}
    68  		g.pkg = inst
    69  		g.emitDefs = nil
    70  		g.pkgRoot = instVal
    71  		g.importCuePkgAsGoPkg = make(map[string]string)
    72  
    73  		iter, err := instVal.Fields(cue.Definitions(true))
    74  		if err != nil {
    75  			return err
    76  		}
    77  		// TODO: support ignoring an entire package via a @go(-) package attribute.
    78  		// TODO: support ignoring an entire file via a @go(-) file attribute above a package clause.
    79  		for iter.Next() {
    80  			sel := iter.Selector()
    81  			if !sel.IsDefinition() {
    82  				continue
    83  			}
    84  			path := cue.MakePath(sel)
    85  			if _, err := g.genDef(path, iter.Value()); err != nil {
    86  				return err
    87  			}
    88  		}
    89  
    90  		// TODO: we should refuse to generate for packages which are not
    91  		// part of the main module, as they may be inside the read-only module cache.
    92  		for _, imp := range inst.Imports {
    93  			if !instDone[imp] && g.importCuePkgAsGoPkg[imp.ImportPath] != "" {
    94  				insts = append(insts, imp)
    95  			}
    96  		}
    97  
    98  		var buf []byte
    99  		printf := func(format string, args ...any) {
   100  			buf = fmt.Appendf(buf, format, args...)
   101  		}
   102  		printf("// Code generated by \"cue exp gengotypes\"; DO NOT EDIT.\n\n")
   103  		goPkgName := goPkgNameForInstance(inst, instVal)
   104  		if prev, ok := goPkgNamesDoneByDir[inst.Dir]; ok && prev != goPkgName {
   105  			return fmt.Errorf("cannot generate two Go packages in one directory; %s and %s", prev, goPkgName)
   106  		} else {
   107  			goPkgNamesDoneByDir[inst.Dir] = goPkgName
   108  		}
   109  		printf("package %s\n\n", goPkgName)
   110  		importedGo := slices.Sorted(maps.Values(g.importCuePkgAsGoPkg))
   111  		importedGo = slices.Compact(importedGo)
   112  		if len(importedGo) > 0 {
   113  			printf("import (\n")
   114  			for _, path := range importedGo {
   115  				printf("\t%q\n", path)
   116  			}
   117  			printf(")\n")
   118  		}
   119  		for _, path := range g.emitDefs {
   120  			qpath := g.qualifiedPath(path)
   121  
   122  			val := instVal.LookupPath(path)
   123  			goName := goNameFromPath(path, true)
   124  			if goName == "" {
   125  				return fmt.Errorf("unexpected path in emitDefs: %q", qpath)
   126  			}
   127  			goAttr := goValueAttr(val)
   128  			if s, _ := goAttr.String(0); s != "" {
   129  				if s == "-" {
   130  					continue
   131  				}
   132  				goName = s
   133  			}
   134  
   135  			emitDocs(printf, goName, val.Doc())
   136  			printf("type %s ", goName)
   137  
   138  			// As we grab the generated source, do some sanity checks too.
   139  			gen, ok := g.generatedTypes[qpath]
   140  			if !ok {
   141  				return fmt.Errorf("expected type in generatedTypes: %q", qpath)
   142  			}
   143  			if gen.inProgress {
   144  				return fmt.Errorf("unexpected in-progress type in generatedTypes: %q", qpath)
   145  			}
   146  			if len(gen.src) == 0 {
   147  				return fmt.Errorf("unexpected empty type in generatedTypes: %q", qpath)
   148  			}
   149  			buf = append(buf, gen.src...)
   150  			printf("\n\n")
   151  		}
   152  
   153  		// The generated file is named after the CUE package, not the generated Go package,
   154  		// as we can have multiple CUE packages in one directory all generating to one Go package.
   155  		// To keep the filename short for common cases, if we are generating a CUE package
   156  		// whose package name is implied from its import path, omit the package name element.
   157  		basename := "cue_types_gen.go"
   158  		ip := ast.ParseImportPath(inst.ImportPath)
   159  		ip1 := ip
   160  		ip1.Qualifier = ""
   161  		ip1.ExplicitQualifier = false
   162  		ip1 = ast.ParseImportPath(ip1.String())
   163  		if ip.Qualifier != ip1.Qualifier {
   164  			basename = fmt.Sprintf("cue_types_%s_gen.go", inst.PkgName)
   165  		}
   166  		outpath := filepath.Join(inst.Dir, basename)
   167  
   168  		formatted, err := goformat.Source(buf)
   169  		if err != nil {
   170  			// Showing the generated Go code helps debug where the syntax error is.
   171  			// This should only occur if our code generator is buggy.
   172  			lines := bytes.Split(buf, []byte("\n"))
   173  			var withLineNums []byte
   174  			for i, line := range lines {
   175  				withLineNums = fmt.Appendf(withLineNums, "% 4d: %s\n", i+1, line)
   176  			}
   177  			fmt.Fprintf(os.Stderr, "-- %s --\n%s\n--\n", filepath.ToSlash(outpath), withLineNums)
   178  			return err
   179  		}
   180  		if err := os.WriteFile(outpath, formatted, 0o666); err != nil {
   181  			return err
   182  		}
   183  	}
   184  	return nil
   185  }
   186  
   187  // generator holds the state for generating Go code for one CUE package instance.
   188  type generator struct {
   189  	// Fields for the entire invocation, to track information about referenced definitions.
   190  
   191  	// generatedTypes records CUE definitions which we have analyzed and translated
   192  	// to Go type expressions.
   193  	//
   194  	// Analyzing types before we start emitting is useful so that, for instance,
   195  	// a Go field can skip using a pointer to a Go type if the type is already nilable.
   196  	generatedTypes map[qualifiedPath]*generatedDef
   197  
   198  	// Fields for each package instance.
   199  
   200  	pkg *build.Instance
   201  
   202  	// emitDefs records paths for the definitions we should emit as Go types.
   203  	emitDefs []cue.Path
   204  
   205  	// importCuePkgAsGoPkg records which CUE packages need to be imported as which Go packages in the generated Go package.
   206  	// This is collected as we emit types, given that some CUE fields and types are omitted
   207  	// and we don't want to end up with unused Go imports.
   208  	//
   209  	// The keys are full CUE import paths; the values are their resulting Go import paths.
   210  	importCuePkgAsGoPkg map[string]string
   211  
   212  	// pkgRoot is the root value of the CUE package, necessary to tell if a referenced value
   213  	// belongs to the current package or not.
   214  	pkgRoot cue.Value
   215  
   216  	// Fields for each definition.
   217  
   218  	// def tracks the generation state for a single CUE definition.
   219  	def *generatedDef
   220  }
   221  
   222  type qualifiedPath = string // [build.Instance.ImportPath] + " " + [cue.Path.String]
   223  
   224  func (g *generator) qualifiedPath(path cue.Path) qualifiedPath {
   225  	return g.pkg.ImportPath + " " + path.String()
   226  }
   227  
   228  // generatedDef holds information about a Go type generated for a CUE definition.
   229  type generatedDef struct {
   230  	// inProgress helps detect cyclic definitions and prevents emitting any Go source
   231  	// before we are done analyzing and generating the relevant types.
   232  	inProgress bool
   233  
   234  	// facts records useful information about the generated type.
   235  	// Note that this only records the facts about the top-level type generated for a definition;
   236  	// the facts about its sub-types, such as the types of fields in a struct,
   237  	// are computed by recursive calls to [generator.emitType] but are not recorded here.
   238  	facts typeFacts
   239  
   240  	// src is the generated Go type expression source.
   241  	// We generate types as plaintext Go source rather than [goast.Expr]
   242  	// as the latter makes it very hard to use empty lines and comment placement correctly.
   243  	src []byte
   244  }
   245  
   246  // typeFacts holds useful information about a generated type,
   247  // such as how it was configured by the user, or qualities about the generated Go type.
   248  type typeFacts struct {
   249  	// isTypeOverride records whether the generated type came from a @go(,type=expr) expression.
   250  	isTypeOverride bool
   251  
   252  	// isNillable records whether the generated Go type can be compared to nil,
   253  	// such that @go(,optional=nillable) can avoid wrapping it in a pointer.
   254  	isNillable bool
   255  }
   256  
   257  func (g *generatedDef) printf(format string, args ...any) {
   258  	if !g.inProgress {
   259  		// It only makes sense to append to src while we are building the Go type expression.
   260  		// If we append bytes after we're done, it's pointless, and likely a bug.
   261  		panic("generatedDef.printf called when inProgress is false")
   262  	}
   263  	g.src = fmt.Appendf(g.src, format, args...)
   264  }
   265  
   266  type optionalStrategy int
   267  
   268  const (
   269  	_ optionalStrategy = iota
   270  	// optional=zero (default); emit the Go type as-is and rely on the zero value.
   271  	optionalZero
   272  	// optional=nillable; emit the Go type with a pointer unless it can already
   273  	// be compared to nil.
   274  	optionalNillable
   275  )
   276  
   277  // genDef analyzes and generates a CUE definition as a Go type,
   278  // adding it to [generator.generatedTypes] as well as [generator.emitDefs]
   279  // to ensure that it is emitted as part of the resulting Go source.
   280  func (g *generator) genDef(path cue.Path, val cue.Value) (*generatedDef, error) {
   281  	qpath := g.qualifiedPath(path)
   282  	if def, ok := g.generatedTypes[qpath]; ok {
   283  		return def, nil // already done or in progress
   284  	}
   285  	g.emitDefs = append(g.emitDefs, path)
   286  
   287  	// When generating a Go type for a CUE definition, we may recurse into
   288  	// this very method if a CUE field references another definition.
   289  	// Store the current [generatedDef] in the stack so we don't lose
   290  	// what we have generated so far, while we generate the nested type.
   291  	parentDef := g.def
   292  	def := &generatedDef{inProgress: true}
   293  	g.def = def
   294  	g.generatedTypes[qpath] = def
   295  	facts, err := g.emitType(val, optionalZero)
   296  	if err != nil {
   297  		return nil, err
   298  	}
   299  	g.def.facts = facts
   300  	g.def.inProgress = false
   301  	g.def = parentDef
   302  	return def, nil
   303  }
   304  
   305  // emitType generates a CUE value as a Go type.
   306  // When possible, the Go type is emitted in the form of a reference.
   307  // Otherwise, an inline Go type expression is used.
   308  func (g *generator) emitType(val cue.Value, optionalStg optionalStrategy) (typeFacts, error) {
   309  	var facts typeFacts
   310  	goAttr := goValueAttr(val)
   311  	// We prefer the form @go(Name,type=pkg.Baz) as it is explicit and extensible,
   312  	// but we are also backwards compatible with @go(Name,pkg.Baz) as emitted by `cue get go`.
   313  	// Make sure that we don't mistake @go(,foo=bar) for a type though.
   314  	attrType, _, _ := goAttr.Lookup(1, "type")
   315  	if attrType == "" {
   316  		if s, _ := goAttr.String(1); !strings.Contains(s, "=") {
   317  			attrType = s
   318  		}
   319  	}
   320  	if attrType != "" {
   321  		fset := gotoken.NewFileSet()
   322  		expr, importedByName, err := parseTypeExpr(fset, attrType)
   323  		if err != nil {
   324  			return facts, fmt.Errorf("cannot parse @go type expression: %w", err)
   325  		}
   326  		for _, pkgPath := range importedByName {
   327  			g.importCuePkgAsGoPkg[pkgPath] = pkgPath
   328  		}
   329  		// Collect any remaining imports from selectors on unquoted single-element std packages
   330  		// such as `@go(,type=io.Reader)`.
   331  		expr = goastutil.Apply(expr, func(c *goastutil.Cursor) bool {
   332  			if sel, _ := c.Node().(*goast.SelectorExpr); sel != nil {
   333  				if imp, _ := sel.X.(*goast.Ident); imp != nil {
   334  					if importedByName[imp.Name] != "" {
   335  						// `@go(,type="go/constant".Kind)` ends up being parsed as the Go expression `constant.Kind`;
   336  						// via importedByName we can tell that "constant" is already provided via "go/constant".
   337  						return true
   338  					}
   339  					g.importCuePkgAsGoPkg[imp.Name] = imp.Name
   340  				}
   341  			}
   342  			return true
   343  		}, nil).(goast.Expr)
   344  		var buf bytes.Buffer
   345  		// We emit in plaintext, so format the parsed Go expression and print it out.
   346  		if err := goformat.Node(&buf, fset, expr); err != nil {
   347  			return facts, err
   348  		}
   349  		// TODO: try using go/packages or go/types to resolve this Go type
   350  		// and find details about it, such as for [typeInfo.isNillable].
   351  		g.def.printf("%s", buf.Bytes())
   352  		facts.isTypeOverride = true
   353  		return facts, nil
   354  	}
   355  
   356  	// Note that type references don't get optionalStg,
   357  	// as @go(,optional=) only affects fields under the current type expression.
   358  	// TODO: support nullable types, such as `null | #SomeReference` and `null | {foo: int}`.
   359  	if done, facts, err := g.emitTypeReference(val); err != nil {
   360  		return typeFacts{}, err
   361  	} else if done {
   362  		return facts, nil
   363  	}
   364  
   365  	// Inline types are below.
   366  
   367  	switch k := val.IncompleteKind(); k {
   368  	case cue.StructKind:
   369  		if elem := val.LookupPath(cue.MakePath(cue.AnyString)); elem.Err() == nil {
   370  			facts.isNillable = true // maps can be nil
   371  			g.def.printf("map[string]")
   372  			if _, err := g.emitType(elem, optionalStg); err != nil {
   373  				return facts, err
   374  			}
   375  			break
   376  		}
   377  		// A disjunction of structs cannot be represented in Go, as it does not have sum types.
   378  		// Fall back to a map of string to any, which is not ideal, but will work for any field.
   379  		//
   380  		// TODO: consider alternatives, such as:
   381  		// * For `#StructFoo | #StructBar`, generate named types for each disjunct,
   382  		//   and use `any` here as a sum type between them.
   383  		// * For a disjunction of closed structs, generate a flat struct with the superset
   384  		//   of all fields, akin to a C union.
   385  		if op, _ := val.Expr(); op == cue.OrOp {
   386  			facts.isNillable = true // maps can be nil
   387  			g.def.printf("map[string]any")
   388  			break
   389  		}
   390  		// TODO: treat a single embedding like `{[string]: int}` like we would `[string]: int`
   391  		g.def.printf("struct {\n")
   392  		iter, err := val.Fields(cue.Definitions(true), cue.Optional(true))
   393  		if err != nil {
   394  			return facts, err
   395  		}
   396  		for iter.Next() {
   397  			sel := iter.Selector()
   398  			val := iter.Value()
   399  			if sel.IsDefinition() {
   400  				// TODO: why does removing [cue.Definitions] above break the tests?
   401  				continue
   402  			}
   403  			cueName := sel.String()
   404  			if sel.IsString() {
   405  				cueName = sel.Unquoted()
   406  			}
   407  			cueName = strings.TrimRight(cueName, "?!")
   408  			emitDocs(g.def.printf, cueName, val.Doc())
   409  
   410  			// We want the Go name from just this selector, even when it's not a definition.
   411  			goName := goNameFromPath(cue.MakePath(sel), false)
   412  
   413  			goAttr := val.Attribute("go")
   414  			if s, _ := goAttr.String(0); s != "" {
   415  				if s == "-" {
   416  					continue
   417  				}
   418  				goName = s
   419  			}
   420  
   421  			optional := sel.ConstraintType()&cue.OptionalConstraint != 0
   422  			optionalStg := optionalStg // only for this field
   423  
   424  			// TODO: much like @go(-), support @(,optional=) when embedded in a value,
   425  			// or attached to an entire package or file, to set a default for an entire scope.
   426  			switch s, ok, _ := goAttr.Lookup(1, "optional"); s {
   427  			case "zero":
   428  				optionalStg = optionalZero
   429  			case "nillable":
   430  				optionalStg = optionalNillable
   431  			default:
   432  				if ok {
   433  					return facts, fmt.Errorf("unknown optional strategy %q", s)
   434  				}
   435  			}
   436  
   437  			// Since CUE fields using double quotes or commas in their names are rare,
   438  			// and the upcoming encoding/json/v2 will support field tags with name quoting,
   439  			// we choose to ignore such fields with a clear note for now.
   440  			if strings.ContainsAny(cueName, "\\\"`,\n") {
   441  				g.def.printf("// CUE field %q: encoding/json does not support this field name\n\n", cueName)
   442  				continue
   443  			}
   444  			g.def.printf("%s ", goName)
   445  
   446  			// Pointers in Go are a prefix in the syntax, but we won't find out the generated type facts
   447  			// until we have emitted its Go source, which we do into the same buffer to avoid copies.
   448  			// Luckily, since a pointer is always one byte, and we gofmt the result anyway for nice formatting,
   449  			// we can add the pointer first and replace it with whitespace later if not wanted.
   450  			ptrOffset := len(g.def.src)
   451  			g.def.printf("*")
   452  
   453  			facts, err := g.emitType(val, optionalStg)
   454  			if err != nil {
   455  				return facts, err
   456  			}
   457  			if !usePointer(facts, optional, optionalStg) {
   458  				g.def.src[ptrOffset] = ' '
   459  			}
   460  
   461  			// TODO: should we generate cuego tags like `cue:"expr"`?
   462  			// If not, at least move the /* CUE */ comments to the end of the line.
   463  			omitEmpty := ""
   464  			if optional {
   465  				omitEmpty = ",omitempty"
   466  			}
   467  			g.def.printf(" `json:\"%s%s\"`", cueName, omitEmpty)
   468  			g.def.printf("\n\n")
   469  		}
   470  		g.def.printf("}")
   471  	case cue.ListKind:
   472  		// We mainly care about patterns like [...string].
   473  		// Anything else can convert into []any as a fallback.
   474  		facts.isNillable = true // slices can be nil
   475  		g.def.printf("[]")
   476  		elem := val.LookupPath(cue.MakePath(cue.AnyIndex))
   477  		if !elem.Exists() {
   478  			// TODO: perhaps mention the original type.
   479  			g.def.printf("any /* CUE closed list */")
   480  		} else if _, err := g.emitType(elem, optionalStg); err != nil {
   481  			return facts, err
   482  		}
   483  
   484  	case cue.NullKind:
   485  		facts.isNillable = true // pointers can be nil
   486  		g.def.printf("*struct{} /* CUE null */")
   487  	case cue.BoolKind:
   488  		g.def.printf("bool")
   489  	case cue.IntKind:
   490  		g.def.printf("int64")
   491  	case cue.FloatKind:
   492  		g.def.printf("float64")
   493  	case cue.StringKind:
   494  		g.def.printf("string")
   495  	case cue.BytesKind:
   496  		facts.isNillable = true // slices can be nil
   497  		g.def.printf("[]byte")
   498  
   499  	case cue.NumberKind:
   500  		// Can we do better for numbers?
   501  		facts.isNillable = true // interfaces can be nil
   502  		g.def.printf("any /* CUE number; int64 or float64 */")
   503  
   504  	case cue.TopKind:
   505  		facts.isNillable = true // interfaces can be nil
   506  		g.def.printf("any /* CUE top */")
   507  
   508  	// TODO: generate e.g. int8 where appropriate
   509  	// TODO: uint64 would be marginally better than int64 for unsigned integer types
   510  
   511  	default:
   512  		// A disjunction of various kinds cannot be represented in Go, as it does not have sum types.
   513  		// Also see the potential approaches in the TODO about disjunctions of structs.
   514  		if op, _ := val.Expr(); op == cue.OrOp {
   515  			facts.isNillable = true // interfaces can be nil
   516  			g.def.printf("any /* CUE disjunction: %s */", k)
   517  			break
   518  		}
   519  		facts.isNillable = true // interfaces can be nil
   520  		g.def.printf("any /* TODO: IncompleteKind: %s */", k)
   521  	}
   522  	return facts, nil
   523  }
   524  
   525  func usePointer(facts typeFacts, optional bool, strategy optionalStrategy) bool {
   526  	if facts.isTypeOverride {
   527  		// @(,type=) overrides any @(,optional=) setting
   528  		return false
   529  	}
   530  	if !optional {
   531  		// Regular and required fields never use pointers.
   532  		return false
   533  	}
   534  	switch strategy {
   535  	case optionalZero:
   536  		return false
   537  	case optionalNillable:
   538  		// Only use a pointer when the type isn't already nillable.
   539  		return !facts.isNillable
   540  	default:
   541  		panic("unreachable")
   542  	}
   543  }
   544  
   545  // parseTypeExpr extends [goparser.ParseExpr] to allow selecting from full import paths.
   546  // `[]go/constant.Kind` is not a valid Go expression, and `[]constant.Kind` is valid
   547  // but doesn't specify a full import path, so it's ambiguous.
   548  //
   549  // Accept `[]"go/constant".Kind` with a pre-processing step to find quoted strings,
   550  // record them as imports keyed by package name in the returned map,
   551  // and rewrite the Go expression to be in terms of the imported package.
   552  // Note that a pre-processing step is necessary as ParseExpr rejects this custom syntax.
   553  func parseTypeExpr(fset *gotoken.FileSet, src string) (goast.Expr, map[string]string, error) {
   554  	var goSrc strings.Builder
   555  	importedByName := make(map[string]string)
   556  
   557  	var scan goscanner.Scanner
   558  	scan.Init(fset.AddFile("", fset.Base(), len(src)), []byte(src), nil, 0)
   559  	lastStringLit := ""
   560  	for {
   561  		_, tok, lit := scan.Scan()
   562  		if tok == gotoken.EOF {
   563  			break
   564  		}
   565  		if lastStringLit != "" {
   566  			if tok == gotoken.PERIOD {
   567  				imp, err := strconv.Unquote(lastStringLit)
   568  				if err != nil {
   569  					panic(err) // should never happen
   570  				}
   571  				// We assume the package name is the last path component.
   572  				// TODO: consider how we might support renaming imports,
   573  				// so that importing both foo.com/x and bar.com/x is possible.
   574  				_, impName, _ := cutLast(imp, "/")
   575  				importedByName[impName] = imp
   576  				goSrc.WriteString(impName)
   577  			} else {
   578  				goSrc.WriteString(lastStringLit)
   579  			}
   580  			lastStringLit = ""
   581  		}
   582  		switch tok {
   583  		case gotoken.STRING:
   584  			lastStringLit = lit
   585  		case gotoken.IDENT, gotoken.INT, gotoken.FLOAT, gotoken.IMAG, gotoken.CHAR:
   586  			goSrc.WriteString(lit)
   587  		case gotoken.SEMICOLON:
   588  			// TODO: How can we support multi-line types such as structs?
   589  			// Note that EOF inserts a semicolon, which breaks goparser.ParseExpr.
   590  			if lit == "\n" {
   591  				break // inserted semicolon at EOF
   592  			}
   593  			fallthrough
   594  		default:
   595  			goSrc.WriteString(tok.String())
   596  		}
   597  	}
   598  	expr, err := goparser.ParseExpr(goSrc.String())
   599  	return expr, importedByName, err
   600  }
   601  
   602  func cutLast(s, sep string) (before, after string, found bool) {
   603  	if i := strings.LastIndex(s, sep); i >= 0 {
   604  		return s[:i], s[i+len(sep):], true
   605  	}
   606  	return "", s, false
   607  }
   608  
   609  // goNameFromPath transforms a CUE path, such as "#foo.bar?",
   610  // into a suitable name for a generated Go type, such as "Foo_bar".
   611  // When defsOnly is true, all path elements must be definitions, or "" is returned.
   612  func goNameFromPath(path cue.Path, defsOnly bool) string {
   613  	export := true
   614  	var sb strings.Builder
   615  	for i, sel := range path.Selectors() {
   616  		if defsOnly && !sel.IsDefinition() {
   617  			return ""
   618  		}
   619  		if i > 0 {
   620  			// To aid in readability, nested names are separated with underscores.
   621  			sb.WriteString("_")
   622  		}
   623  		str := sel.String()
   624  		if sel.IsString() {
   625  			str = sel.Unquoted()
   626  		}
   627  		str, hidden := strings.CutPrefix(str, "_")
   628  		if hidden {
   629  			// If any part of the path is hidden, we are not exporting.
   630  			export = false
   631  		}
   632  		// Leading or trailing characters for definitions, optional, or required
   633  		// are not included as part of Go names.
   634  		str = strings.TrimPrefix(str, "#")
   635  		str = strings.TrimRight(str, "?!")
   636  		// CUE allows quoted field names such as "foo-bar" or "123baz",
   637  		// none of which are valid Go identifiers per https://go.dev/ref/spec#Identifiers.
   638  		// Replace forbidden characters with underscores, like `go test` does with subtest names,
   639  		// and add a leading "F" if the name begins with a digit.
   640  		// TODO: this could result in name collisions; fix if it actually happens in practice.
   641  		for i, r := range str {
   642  			switch {
   643  			case unicode.IsLetter(r):
   644  				sb.WriteRune(r)
   645  			case unicode.IsDigit(r):
   646  				if i == 0 {
   647  					sb.WriteRune('F')
   648  				}
   649  				sb.WriteRune(r)
   650  			default:
   651  				sb.WriteRune('_')
   652  			}
   653  		}
   654  	}
   655  	name := sb.String()
   656  	if export {
   657  		// Capitalize the first letter to export the name in Go.
   658  		// https://go.dev/ref/spec#Exported_identifiers
   659  		first, size := utf8.DecodeRuneInString(name)
   660  		name = string(unicode.ToTitle(first)) + name[size:]
   661  	}
   662  	// TODO: lowercase if not exporting
   663  	return name
   664  }
   665  
   666  // goValueAttr is like [cue.Value.Attribute] with the string parameter "go",
   667  // but it supports [cue.DeclAttr] attributes as well and not just [cue.FieldAttr].
   668  //
   669  // TODO: surely this is a shortcoming of the method above?
   670  func goValueAttr(val cue.Value) cue.Attribute {
   671  	attrs := val.Attributes(cue.ValueAttr)
   672  	for _, attr := range attrs {
   673  		if attr.Name() == "go" {
   674  			return attr
   675  		}
   676  	}
   677  	return cue.Attribute{}
   678  }
   679  
   680  // goPkgNameForInstance determines what to name a Go package generated from a CUE instance.
   681  // By default this is the CUE package name, but it can be overriden by a @go() package attribute.
   682  func goPkgNameForInstance(inst *build.Instance, instVal cue.Value) string {
   683  	attr := goValueAttr(instVal)
   684  	if s, _ := attr.String(0); s != "" {
   685  		return s
   686  	}
   687  	return inst.PkgName
   688  }
   689  
   690  // emitTypeReference attempts to generate a CUE value as a Go type via a reference,
   691  // either to a type in the same Go package, or to a type in an imported package.
   692  func (g *generator) emitTypeReference(val cue.Value) (bool, typeFacts, error) {
   693  	// References to existing names, either from the same package or an imported package.
   694  	root, path := val.ReferencePath()
   695  	// TODO: surely there is a better way to check whether ReferencePath returned "no path",
   696  	// such as a possible path.IsValid method?
   697  	if len(path.Selectors()) == 0 {
   698  		return false, typeFacts{}, nil
   699  	}
   700  	inst := root.BuildInstance()
   701  	// Go has no notion of qualified import paths; if a CUE file imports
   702  	// "foo.com/bar:qualified", we import just "foo.com/bar" on the Go side.
   703  	// TODO: deal with multiple packages existing in the same directory.
   704  	unqualifiedPath := ast.ParseImportPath(inst.ImportPath).Unqualified().String()
   705  
   706  	// As a special case, some CUE standard library types are allowed as references
   707  	// even though they aren't definitions.
   708  	defsOnly := true
   709  	switch fmt.Sprintf("%s.%s", unqualifiedPath, path) {
   710  	case "time.Duration":
   711  		// Note that CUE represents durations as strings, but Go as int64.
   712  		// TODO: can we do better here, such as a custom duration type?
   713  		g.def.printf("string /* CUE time.Duration */")
   714  		return true, typeFacts{}, nil
   715  	case "time.Time":
   716  		defsOnly = false
   717  	}
   718  
   719  	name := goNameFromPath(path, defsOnly)
   720  	if name == "" {
   721  		return false, typeFacts{}, nil // Not a path we are generating.
   722  	}
   723  
   724  	var facts typeFacts
   725  	inProgress := false
   726  	// We did use a reference; if the referenced name was from another package,
   727  	// we need to ensure that package is imported.
   728  	// Otherwise, we need to ensure that the referenced local definition is generated.
   729  	// Either way, return the facts about the referenced type.
   730  	if root != g.pkgRoot {
   731  		g.importCuePkgAsGoPkg[inst.ImportPath] = unqualifiedPath
   732  		// TODO: populate the facts here, which will require generating imported packages first.
   733  	} else {
   734  		def, err := g.genDef(path, cue.Dereference(val))
   735  		if err != nil {
   736  			return false, typeFacts{}, err
   737  		}
   738  		facts = def.facts
   739  		inProgress = def.inProgress
   740  	}
   741  	// We generate types depth-first; if the type referenced here is still in progress,
   742  	// it means that we are in a cyclic type, so be nillable to avoid a Go type of infinite size.
   743  	// Note that sometimes we're in a complex type which is already nillable, such as:
   744  	//
   745  	//    #GraphNode: {edges?: [...#GraphNode]}
   746  	//
   747  	// So we could generate the Go field as `[]GraphNode` rather than `[]*GraphNode`,
   748  	// given that Go slices are already nillable, but we currently do use a pointer.
   749  	if inProgress && !facts.isNillable {
   750  		g.def.printf("*")
   751  		facts.isNillable = true // pointers can be nil
   752  	}
   753  	if root != g.pkgRoot {
   754  		g.def.printf("%s.", goPkgNameForInstance(inst, root))
   755  	}
   756  	g.def.printf("%s", name)
   757  	return true, facts, nil
   758  }
   759  
   760  // emitDocs generates the documentation comments attached to the following declaration.
   761  // It takes a printf function as we emit docs directly in the generated Go code
   762  // when emitting the top-level Go type definitions.
   763  func emitDocs(printf func(string, ...any), name string, groups []*ast.CommentGroup) {
   764  	// TODO: place the comment group starting with `// $name ...` first.
   765  	// TODO: ensure that the Go name is used in the godoc.
   766  	for i, group := range groups {
   767  		if i > 0 {
   768  			printf("//\n")
   769  		}
   770  		for _, line := range group.List {
   771  			printf("%s\n", line.Text)
   772  		}
   773  	}
   774  }