// Copyright 2024 CUE Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package gotypes import ( "bytes" "fmt" goast "go/ast" goformat "go/format" goparser "go/parser" goscanner "go/scanner" gotoken "go/token" "maps" "os" "slices" "strconv" "strings" "unicode" "unicode/utf8" goastutil "golang.org/x/tools/go/ast/astutil" "cuelang.org/go/cue" "cuelang.org/go/cue/ast" "cuelang.org/go/cue/build" ) // WriteFunc is a function that writes the generated Go source for an instance. type WriteFunc func(inst *build.Instance, data []byte) error // Generate produces Go type definitions from exported CUE definitions. // See the help text for `cue help exp gengotypes`. // // writeFile is called to write the formatted Go source for each instance. func Generate(ctx *cue.Context, writeFile WriteFunc, insts ...*build.Instance) error { // record which package instances have already been generated instDone := make(map[*build.Instance]bool) goPkgNamesDoneByDir := make(map[string]string) g := generator{generatedTypes: make(map[qualifiedPath]*generatedDef)} // ensure we don't modify the parameter slice insts = slices.Clip(insts) for len(insts) > 0 { // we append imports to this list inst := insts[0] insts = insts[1:] if err := inst.Err; err != nil { return err } if instDone[inst] { continue } instDone[inst] = true instVal := ctx.BuildInstance(inst) if err := instVal.Validate(); err != nil { return err } g.pkg = inst g.emitDefs = nil g.pkgRoot = instVal g.importCuePkgAsGoPkg = make(map[string]string) // gather the import aliases for the instance. // NOTE: this implementation means that if an import alias is employed in one file // it will be used anywhere that package is referenced importAliases, err := gatherImportAliases(inst) if err != nil { return err } g.importAliases = importAliases iter, err := instVal.Fields(cue.Definitions(true)) if err != nil { return err } // TODO: support ignoring an entire package via a @go(-) package attribute. // TODO: support ignoring an entire file via a @go(-) file attribute above a package clause. for iter.Next() { sel := iter.Selector() if !sel.IsDefinition() { continue } path := cue.MakePath(sel) if _, err := g.genDef(path, iter.Value()); err != nil { return err } } // TODO: we should refuse to generate for packages which are not // part of the main module, as they may be inside the read-only module cache. for _, imp := range inst.Imports { if !instDone[imp] && g.importCuePkgAsGoPkg[imp.ImportPath] != "" { insts = append(insts, imp) } } var buf []byte printf := func(format string, args ...any) { buf = fmt.Appendf(buf, format, args...) } printf("// Code generated by \"cue exp gengotypes\"; DO NOT EDIT.\n\n") goPkgName := goPkgNameForInstance(inst, instVal, nil) if prev, ok := goPkgNamesDoneByDir[inst.Dir]; ok && prev != goPkgName { return fmt.Errorf("cannot generate two Go packages in one directory; %s and %s", prev, goPkgName) } else { goPkgNamesDoneByDir[inst.Dir] = goPkgName } printf("package %s\n\n", goPkgName) importedGo := slices.Sorted(maps.Values(g.importCuePkgAsGoPkg)) importedGo = slices.Compact(importedGo) if len(importedGo) > 0 { printf("import (\n") for _, path := range importedGo { if alias, ok := g.importAliases[path]; ok { printf("\t%s %q\n", alias, path) } else { printf("\t%q\n", path) } } printf(")\n") } for _, path := range g.emitDefs { qpath := g.qualifiedPath(path) val := instVal.LookupPath(path) goName := goNameFromPath(path, true) if goName == "" { return fmt.Errorf("unexpected path in emitDefs: %q", qpath) } goAttr := goValueAttr(val) if s, _ := goAttr.String(0); s != "" { if s == "-" { continue } goName = s } emitDocs(printf, goName, val.Doc()) printf("type %s ", goName) // As we grab the generated source, do some sanity checks too. gen, ok := g.generatedTypes[qpath] if !ok { return fmt.Errorf("expected type in generatedTypes: %q", qpath) } if gen.inProgress { return fmt.Errorf("unexpected in-progress type in generatedTypes: %q", qpath) } if len(gen.src) == 0 { return fmt.Errorf("unexpected empty type in generatedTypes: %q", qpath) } buf = append(buf, gen.src...) printf("\n\n") } formatted, err := goformat.Source(buf) if err != nil { // Showing the generated Go code helps debug where the syntax error is. // This should only occur if our code generator is buggy. lines := bytes.Split(buf, []byte("\n")) var withLineNums []byte for i, line := range lines { withLineNums = fmt.Appendf(withLineNums, "% 4d: %s\n", i+1, line) } fmt.Fprintf(os.Stderr, "-- %s --\n%s\n--\n", inst.ImportPath, withLineNums) return err } if err := writeFile(inst, formatted); err != nil { return err } } return nil } // generator holds the state for generating Go code for one CUE package instance. type generator struct { // Fields for the entire invocation, to track information about referenced definitions. // generatedTypes records CUE definitions which we have analyzed and translated // to Go type expressions. // // Analyzing types before we start emitting is useful so that, for instance, // a Go field can skip using a pointer to a Go type if the type is already nilable. generatedTypes map[qualifiedPath]*generatedDef // Fields for each package instance. pkg *build.Instance // emitDefs records paths for the definitions we should emit as Go types. emitDefs []cue.Path // importCuePkgAsGoPkg records which CUE packages need to be imported as which Go packages in the generated Go package. // This is collected as we emit types, given that some CUE fields and types are omitted // and we don't want to end up with unused Go imports. // // The keys are full CUE import paths; the values are their resulting Go import paths. importCuePkgAsGoPkg map[string]string // pkgRoot is the root value of the CUE package, necessary to tell if a referenced value // belongs to the current package or not. pkgRoot cue.Value // importAliases maps package names to a given alias. In the case that there are multiple aliases present for // same package then we will use the first alias encountered. When the same alias is used for multiple packages // across different files, then a number will be added to the end of the alias to avoid conflicts. importAliases map[string]string // Fields for each definition. // def tracks the generation state for a single CUE definition. def *generatedDef } type qualifiedPath = string // [build.Instance.ImportPath] + " " + [cue.Path.String] func (g *generator) qualifiedPath(path cue.Path) qualifiedPath { return g.pkg.ImportPath + " " + path.String() } // generatedDef holds information about a Go type generated for a CUE definition. type generatedDef struct { // inProgress helps detect cyclic definitions and prevents emitting any Go source // before we are done analyzing and generating the relevant types. inProgress bool // facts records useful information about the generated type. // Note that this only records the facts about the top-level type generated for a definition; // the facts about its sub-types, such as the types of fields in a struct, // are computed by recursive calls to [generator.emitType] but are not recorded here. facts typeFacts // src is the generated Go type expression source. // We generate types as plaintext Go source rather than [goast.Expr] // as the latter makes it very hard to use empty lines and comment placement correctly. src []byte } // typeFacts holds useful information about a generated type, // such as how it was configured by the user, or qualities about the generated Go type. type typeFacts struct { // isTypeOverride records whether the generated type came from a @go(,type=expr) expression. isTypeOverride bool // isNillable records whether the generated Go type can be compared to nil, // such that @go(,optional=nillable) can avoid wrapping it in a pointer. isNillable bool } func (g *generatedDef) printf(format string, args ...any) { if !g.inProgress { // It only makes sense to append to src while we are building the Go type expression. // If we append bytes after we're done, it's pointless, and likely a bug. panic("generatedDef.printf called when inProgress is false") } g.src = fmt.Appendf(g.src, format, args...) } type optionalStrategy int const ( _ optionalStrategy = iota // optional=zero (default); emit the Go type as-is and rely on the zero value. optionalZero // optional=nillable; emit the Go type with a pointer unless it can already // be compared to nil. optionalNillable ) // genDef analyzes and generates a CUE definition as a Go type, // adding it to [generator.generatedTypes] as well as [generator.emitDefs] // to ensure that it is emitted as part of the resulting Go source. func (g *generator) genDef(path cue.Path, val cue.Value) (*generatedDef, error) { qpath := g.qualifiedPath(path) if def, ok := g.generatedTypes[qpath]; ok { return def, nil // already done or in progress } g.emitDefs = append(g.emitDefs, path) // When generating a Go type for a CUE definition, we may recurse into // this very method if a CUE field references another definition. // Store the current [generatedDef] in the stack so we don't lose // what we have generated so far, while we generate the nested type. parentDef := g.def def := &generatedDef{inProgress: true} g.def = def g.generatedTypes[qpath] = def facts, err := g.emitType(val, optionalZero) if err != nil { return nil, err } g.def.facts = facts g.def.inProgress = false g.def = parentDef return def, nil } // emitType generates a CUE value as a Go type. // When possible, the Go type is emitted in the form of a reference. // Otherwise, an inline Go type expression is used. func (g *generator) emitType(val cue.Value, optionalStg optionalStrategy) (typeFacts, error) { var facts typeFacts goAttr := goValueAttr(val) // We prefer the form @go(Name,type=pkg.Baz) as it is explicit and extensible, // but we are also backwards compatible with @go(Name,pkg.Baz) as emitted by `cue get go`. // Make sure that we don't mistake @go(,foo=bar) for a type though. attrType, _, _ := goAttr.Lookup(1, "type") if attrType == "" { if s, _ := goAttr.String(1); !strings.Contains(s, "=") { attrType = s } } if attrType != "" { fset := gotoken.NewFileSet() expr, importedByName, err := parseTypeExpr(fset, attrType) if err != nil { return facts, fmt.Errorf("cannot parse @go type expression: %w", err) } for _, pkgPath := range importedByName { g.importCuePkgAsGoPkg[pkgPath] = pkgPath } // Collect any remaining imports from selectors on unquoted single-element std packages // such as `@go(,type=io.Reader)`. expr = goastutil.Apply(expr, func(c *goastutil.Cursor) bool { if sel, _ := c.Node().(*goast.SelectorExpr); sel != nil { if imp, _ := sel.X.(*goast.Ident); imp != nil { if importedByName[imp.Name] != "" { // `@go(,type="go/constant".Kind)` ends up being parsed as the Go expression `constant.Kind`; // via importedByName we can tell that "constant" is already provided via "go/constant". return true } g.importCuePkgAsGoPkg[imp.Name] = imp.Name } } return true }, nil).(goast.Expr) var buf bytes.Buffer // We emit in plaintext, so format the parsed Go expression and print it out. if err := goformat.Node(&buf, fset, expr); err != nil { return facts, err } // TODO: try using go/packages or go/types to resolve this Go type // and find details about it, such as for [typeInfo.isNillable]. g.def.printf("%s", buf.Bytes()) facts.isTypeOverride = true return facts, nil } // Note that type references don't get optionalStg, // as @go(,optional=) only affects fields under the current type expression. // TODO: support nullable types, such as `null | #SomeReference` and `null | {foo: int}`. if done, facts, err := g.emitTypeReference(val); err != nil { return typeFacts{}, err } else if done { return facts, nil } // Inline types are below. switch k := val.IncompleteKind(); k { case cue.StructKind: if elem := val.LookupPath(cue.MakePath(cue.AnyString)); elem.Err() == nil { facts.isNillable = true // maps can be nil g.def.printf("map[string]") if _, err := g.emitType(elem, optionalStg); err != nil { return facts, err } break } // A disjunction of structs cannot be represented in Go, as it does not have sum types. // Fall back to a map of string to any, which is not ideal, but will work for any field. // // TODO: consider alternatives, such as: // * For `#StructFoo | #StructBar`, generate named types for each disjunct, // and use `any` here as a sum type between them. // * For a disjunction of closed structs, generate a flat struct with the superset // of all fields, akin to a C union. if op, _ := val.Expr(); op == cue.OrOp { facts.isNillable = true // maps can be nil g.def.printf("map[string]any") break } // TODO: treat a single embedding like `{[string]: int}` like we would `[string]: int` g.def.printf("struct {\n") iter, err := val.Fields(cue.Definitions(true), cue.Optional(true)) if err != nil { return facts, err } for iter.Next() { sel := iter.Selector() val := iter.Value() if sel.IsDefinition() { // TODO: why does removing [cue.Definitions] above break the tests? continue } cueName := sel.String() if sel.IsString() { cueName = sel.Unquoted() } cueName = strings.TrimRight(cueName, "?!") emitDocs(g.def.printf, cueName, val.Doc()) // We want the Go name from just this selector, even when it's not a definition. goName := goNameFromPath(cue.MakePath(sel), false) goAttr := val.Attribute("go") if s, _ := goAttr.String(0); s != "" { if s == "-" { continue } goName = s } optional := sel.ConstraintType()&cue.OptionalConstraint != 0 optionalStg := optionalStg // only for this field // TODO: much like @go(-), support @(,optional=) when embedded in a value, // or attached to an entire package or file, to set a default for an entire scope. switch s, ok, _ := goAttr.Lookup(1, "optional"); s { case "zero": optionalStg = optionalZero case "nillable": optionalStg = optionalNillable default: if ok { return facts, fmt.Errorf("unknown optional strategy %q", s) } } // Since CUE fields using double quotes or commas in their names are rare, // and the upcoming encoding/json/v2 will support field tags with name quoting, // we choose to ignore such fields with a clear note for now. if strings.ContainsAny(cueName, "\\\"`,\n") { g.def.printf("// CUE field %q: encoding/json does not support this field name\n\n", cueName) continue } g.def.printf("%s ", goName) // Pointers in Go are a prefix in the syntax, but we won't find out the generated type facts // until we have emitted its Go source, which we do into the same buffer to avoid copies. // Luckily, since a pointer is always one byte, and we gofmt the result anyway for nice formatting, // we can add the pointer first and replace it with whitespace later if not wanted. ptrOffset := len(g.def.src) g.def.printf("*") facts, err := g.emitType(val, optionalStg) if err != nil { return facts, err } if !usePointer(facts, optional, optionalStg) { g.def.src[ptrOffset] = ' ' } // TODO: should we generate cuego tags like `cue:"expr"`? // If not, at least move the /* CUE */ comments to the end of the line. omitEmpty := "" if optional { omitEmpty = ",omitempty" } g.def.printf(" `json:\"%s%s\"`", cueName, omitEmpty) g.def.printf("\n\n") } g.def.printf("}") case cue.ListKind: // We mainly care about patterns like [...string]. // Anything else can convert into []any as a fallback. facts.isNillable = true // slices can be nil g.def.printf("[]") elem := val.LookupPath(cue.MakePath(cue.AnyIndex)) if !elem.Exists() { // TODO: perhaps mention the original type. g.def.printf("any /* CUE closed list */") } else if _, err := g.emitType(elem, optionalStg); err != nil { return facts, err } case cue.NullKind: facts.isNillable = true // pointers can be nil g.def.printf("*struct{} /* CUE null */") case cue.BoolKind: g.def.printf("bool") case cue.IntKind: g.def.printf("int64") case cue.FloatKind: g.def.printf("float64") case cue.StringKind: g.def.printf("string") case cue.BytesKind: facts.isNillable = true // slices can be nil g.def.printf("[]byte") case cue.NumberKind: // Can we do better for numbers? facts.isNillable = true // interfaces can be nil g.def.printf("any /* CUE number; int64 or float64 */") case cue.TopKind: facts.isNillable = true // interfaces can be nil g.def.printf("any /* CUE top */") // TODO: generate e.g. int8 where appropriate // TODO: uint64 would be marginally better than int64 for unsigned integer types default: // A disjunction of various kinds cannot be represented in Go, as it does not have sum types. // Also see the potential approaches in the TODO about disjunctions of structs. if op, _ := val.Expr(); op == cue.OrOp { facts.isNillable = true // interfaces can be nil g.def.printf("any /* CUE disjunction: %s */", k) break } facts.isNillable = true // interfaces can be nil g.def.printf("any /* TODO: IncompleteKind: %s */", k) } return facts, nil } func usePointer(facts typeFacts, optional bool, strategy optionalStrategy) bool { if facts.isTypeOverride { // @(,type=) overrides any @(,optional=) setting return false } if !optional { // Regular and required fields never use pointers. return false } switch strategy { case optionalZero: return false case optionalNillable: // Only use a pointer when the type isn't already nillable. return !facts.isNillable default: panic("unreachable") } } // parseTypeExpr extends [goparser.ParseExpr] to allow selecting from full import paths. // `[]go/constant.Kind` is not a valid Go expression, and `[]constant.Kind` is valid // but doesn't specify a full import path, so it's ambiguous. // // Accept `[]"go/constant".Kind` with a pre-processing step to find quoted strings, // record them as imports keyed by package name in the returned map, // and rewrite the Go expression to be in terms of the imported package. // Note that a pre-processing step is necessary as ParseExpr rejects this custom syntax. func parseTypeExpr(fset *gotoken.FileSet, src string) (goast.Expr, map[string]string, error) { var goSrc strings.Builder importedByName := make(map[string]string) var scan goscanner.Scanner scan.Init(fset.AddFile("", fset.Base(), len(src)), []byte(src), nil, 0) lastStringLit := "" for { _, tok, lit := scan.Scan() if tok == gotoken.EOF { break } if lastStringLit != "" { if tok == gotoken.PERIOD { imp, err := strconv.Unquote(lastStringLit) if err != nil { panic(err) // should never happen } // We assume the package name is the last path component. // TODO: consider how we might support renaming imports, // so that importing both foo.com/x and bar.com/x is possible. _, impName, _ := cutLast(imp, "/") importedByName[impName] = imp goSrc.WriteString(impName) } else { goSrc.WriteString(lastStringLit) } lastStringLit = "" } switch tok { case gotoken.STRING: lastStringLit = lit case gotoken.IDENT, gotoken.INT, gotoken.FLOAT, gotoken.IMAG, gotoken.CHAR: goSrc.WriteString(lit) case gotoken.SEMICOLON: // TODO: How can we support multi-line types such as structs? // Note that EOF inserts a semicolon, which breaks goparser.ParseExpr. if lit == "\n" { break // inserted semicolon at EOF } fallthrough default: goSrc.WriteString(tok.String()) } } expr, err := goparser.ParseExpr(goSrc.String()) return expr, importedByName, err } func cutLast(s, sep string) (before, after string, found bool) { if i := strings.LastIndex(s, sep); i >= 0 { return s[:i], s[i+len(sep):], true } return "", s, false } // goNameFromPath transforms a CUE path, such as "#foo.bar?", // into a suitable name for a generated Go type, such as "Foo_bar". // When defsOnly is true, all path elements must be definitions, or "" is returned. func goNameFromPath(path cue.Path, defsOnly bool) string { export := true var sb strings.Builder for i, sel := range path.Selectors() { if defsOnly && !sel.IsDefinition() { return "" } if i > 0 { // To aid in readability, nested names are separated with underscores. sb.WriteString("_") } str := sel.String() if sel.IsString() { str = sel.Unquoted() } str, hidden := strings.CutPrefix(str, "_") if hidden { // If any part of the path is hidden, we are not exporting. export = false } // Leading or trailing characters for definitions, optional, or required // are not included as part of Go names. str = strings.TrimPrefix(str, "#") str = strings.TrimRight(str, "?!") // CUE allows quoted field names such as "foo-bar" or "123baz", // none of which are valid Go identifiers per https://go.dev/ref/spec#Identifiers. // Replace forbidden characters with underscores, like `go test` does with subtest names, // and add a leading "F" if the name begins with a digit. // TODO: this could result in name collisions; fix if it actually happens in practice. for i, r := range str { switch { case unicode.IsLetter(r): sb.WriteRune(r) case unicode.IsDigit(r): if i == 0 { sb.WriteRune('F') } sb.WriteRune(r) default: sb.WriteRune('_') } } } name := sb.String() if export { // Capitalize the first letter to export the name in Go. // https://go.dev/ref/spec#Exported_identifiers first, size := utf8.DecodeRuneInString(name) name = string(unicode.ToTitle(first)) + name[size:] } // TODO: lowercase if not exporting return name } // goValueAttr is like [cue.Value.Attribute] with the string parameter "go", // but it supports [cue.DeclAttr] attributes as well and not just [cue.FieldAttr]. // // TODO: surely this is a shortcoming of the method above? func goValueAttr(val cue.Value) cue.Attribute { attrs := val.Attributes(cue.ValueAttr) for _, attr := range attrs { if attr.Name() == "go" { return attr } } return cue.Attribute{} } // goPkgNameForInstance determines what to name a Go package generated from a CUE instance. // By default this is the CUE package name, but it can be overriden by a @go() package attribute. // When supplying importAliases, and if no package attribute is found, the returned package name // reflects the alias name that the package is being imported as. func goPkgNameForInstance(inst *build.Instance, instVal cue.Value, importAliases map[string]string) string { attr := goValueAttr(instVal) if s, _ := attr.String(0); s != "" { return s } if alias, ok := importAliases[inst.ImportPath]; ok { return alias } return inst.PkgName } // emitTypeReference attempts to generate a CUE value as a Go type via a reference, // either to a type in the same Go package, or to a type in an imported package. func (g *generator) emitTypeReference(val cue.Value) (bool, typeFacts, error) { // References to existing names, either from the same package or an imported package. var root cue.Value var path cue.Path for _, candidate := range refCandidates(val) { root, path = candidate.ReferencePath() if root.Exists() { val = candidate break } } if !root.Exists() { return false, typeFacts{}, nil } inst := root.BuildInstance() // Go has no notion of qualified import paths; if a CUE file imports // "foo.com/bar:qualified", we import just "foo.com/bar" on the Go side. // TODO: deal with multiple packages existing in the same directory. unqualifiedPath := ast.ParseImportPath(inst.ImportPath).Unqualified().String() // As a special case, some CUE standard library types are allowed as references // even though they aren't definitions. defsOnly := true switch fmt.Sprintf("%s.%s", unqualifiedPath, path) { case "time.Duration": // Note that CUE represents durations as strings, but Go as int64. // TODO: can we do better here, such as a custom duration type? g.def.printf("string /* CUE time.Duration */") return true, typeFacts{}, nil case "time.Time": defsOnly = false } name := goNameFromPath(path, defsOnly) if name == "" { return false, typeFacts{}, nil // Not a path we are generating. } var facts typeFacts inProgress := false // We did use a reference; if the referenced name was from another package, // we need to ensure that package is imported. // Otherwise, we need to ensure that the referenced local definition is generated. // Either way, return the facts about the referenced type. if root != g.pkgRoot { g.importCuePkgAsGoPkg[inst.ImportPath] = unqualifiedPath // TODO: populate the facts here, which will require generating imported packages first. } else { def, err := g.genDef(path, cue.Dereference(val)) if err != nil { return false, typeFacts{}, err } facts = def.facts inProgress = def.inProgress } // We generate types depth-first; if the type referenced here is still in progress, // it means that we are in a cyclic type, so be nillable to avoid a Go type of infinite size. // Note that sometimes we're in a complex type which is already nillable, such as: // // #GraphNode: {edges?: [...#GraphNode]} // // So we could generate the Go field as `[]GraphNode` rather than `[]*GraphNode`, // given that Go slices are already nillable, but we currently do use a pointer. if inProgress && !facts.isNillable { g.def.printf("*") facts.isNillable = true // pointers can be nil } if root != g.pkgRoot { g.def.printf("%s.", goPkgNameForInstance(inst, root, g.importAliases)) } g.def.printf("%s", name) return true, facts, nil } // refCandidates returns the values to try when looking for a type reference. // For a conjunction like `#Def & {extra constraints}`, the unified value has no // ReferencePath, so we return the operands that subsume the full value, // meaning the operand's type fully covers the conjunction's fields // and the extra constraints only narrow without adding new fields. func refCandidates(val cue.Value) []cue.Value { op, args := val.Expr() if op != cue.AndOp { return []cue.Value{val} } var candidates []cue.Value for _, arg := range args { if arg.Subsume(val) == nil { candidates = append(candidates, arg) } } return candidates } // emitDocs generates the documentation comments attached to the following declaration. // It takes a printf function as we emit docs directly in the generated Go code // when emitting the top-level Go type definitions. func emitDocs(printf func(string, ...any), name string, groups []*ast.CommentGroup) { // TODO: place the comment group starting with `// $name ...` first. // TODO: ensure that the Go name is used in the godoc. for i, group := range groups { if i > 0 { printf("//\n") } for _, line := range group.List { printf("%s\n", line.Text) } } } // gatherImportAliases collects the aliases from imports across the instance. func gatherImportAliases(inst *build.Instance) (map[string]string, error) { fileAliases := make(map[string]string) tracked := make(map[string]int) type pair struct{ path, alias string } var explicit []pair for _, file := range inst.Files { for spec := range file.ImportSpecs() { pkgPath, err := strconv.Unquote(spec.Path.Value) if err != nil { return nil, err } // Unaliased import: reserve its package name, which can come // from an explicit or implicit qualifier. if spec.Name == nil { pkgName := ast.ParseImportPath(pkgPath).Qualifier if pkgName == "" { return nil, fmt.Errorf("could not find an imported package qualifier: %q", pkgPath) } tracked[pkgName]++ continue } // Explicit alias: queue for resolution. alias := spec.Name.Name explicit = append(explicit, pair{path: pkgPath, alias: alias}) } } // Resolve explicit aliases with conflict suffixing. for _, e := range explicit { alias := e.alias if count, ok := tracked[alias]; ok { alias = fmt.Sprintf("%s%d", alias, count) } fileAliases[e.path] = alias tracked[e.alias]++ // track the alias name (unsuffixed) for future conflicts } return fileAliases, nil }