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 }