// Package gen is the orchestrator of plumb's pure core: it runs the discover → // solve → emit pipeline over loaded, type-checked packages to produce generated // injector source, performing no I/O, holding no global state, and consulting no // clock or randomness. Its output is a deterministic function of its inputs. // The phases live in sibling packages (discover, solve, emit) over shared leaves // (diag, gotypes); this package only wires them together.
package gen import ( ) // Options configures a single generation run. The cli builds it from flags; gen // distributes the fields each phase needs. type Options struct { // ImportPath is the destination package's import path. It decides what is // referenced unqualified (the destination) and what is imported and // qualified (everything else). Required and never empty by the time the // core runs. ImportPath string // PackageName is the identifier emitted in the generated `package <name>` // clause. Required and never empty by the time the core runs. PackageName string // OutputBase is the base name of the file being written (e.g. // "plumb_gen.go"), used to identify the file being overwritten for the // same-package function-name collision check. Empty when writing to stdout, // in which case that check is skipped. OutputBase string } // Generate is the pure entry point: it analyzes the loaded packages, resolves // every set, and returns the generated source and report. It performs no I/O. // The returned error, when non-nil, is always a *diag.Error with a source // position where one applies. func ( Options, []*discover.Package) (*emit.Result, error) { // PackageName and ImportPath are required; the cli always resolves them, so an // empty value is an internal-caller invariant violation, not a user error. if .PackageName == "" { panic("plumb: gen.Options.PackageName is required") } if .ImportPath == "" { panic("plumb: gen.Options.ImportPath is required") } , := discover.Analyze(, .ImportPath, .OutputBase) if != nil { return nil, } if len() == 0 { return nil, diag.Errorf(token.Position{}, diag.ErrNoDirectives, "nothing to generate") } := map[string][]*discover.Provider{} for , := range { [.SetName] = append([.SetName], ) } , := buildDestInfo(, ) if != nil { return nil, } := types.NewContext() var []*solve.Plan for , := range slices.Sorted(maps.Keys()) { , := solve.Set(, [], .ImportPath, .OutputBase, , ) if != nil { return nil, } = append(, ) } return emit.File(.ImportPath, .PackageName, , , ), nil } // buildDestInfo gathers what the emitter needs to know about the destination // package: whether it was scanned, its name, and the file each top-level // identifier is declared in (for the collision safeguards). It also rejects a // destination declaration that shadows a predeclared identifier, a // whole-destination property, so it is checked here once rather than per set. func ( Options, []*discover.Package) (*solve.DestInfo, *diag.Error) { := &solve.DestInfo{PkgName: .PackageName, Names: map[string]string{}, Imports: map[string]string{}} for , := range { if .PkgPath != .ImportPath || .Types == nil { continue } .Scanned = true := .Types.Scope() for , := range .Names() { := .Lookup() := filepath.Base(diag.PosIn(.Fset, .Pos()).Filename) if == .OutputBase { // The file plumb is about to overwrite declares only generated // injectors, which no injector body references. Recording their names // as reserved would make a local or lifted parameter derived from a // set name (e.g. a "server" local in set "server") free on the first // generation but reserved once the output file exists, breaking // idempotent regeneration. Skip them here: gen owns output-file // exclusion (the collision check trusts these maps and does not // re-filter), and import aliases re-add the set names independently // (assignAliases). continue } // The generated file spells predeclared identifiers unqualified (error and // nil in fallible wiring, basic-type names in signatures) and no // qualification can restore a shadowed builtin, so a destination that // declares a universe name at top level is rejected, regardless of whether // any one set's signature happens to spell it. scope.Names() is sorted, so // the reported name is deterministic; anchoring at the declaration points at // the offending source directly. if gotypes.IsUniverseName() { return nil, diag.Errorf(diag.PosIn(.Fset, .Pos()), diag.ErrDestShadowsPredeclared, "destination package %q declares %s (%s), shadowing the predeclared identifier; rename the declaration", .ImportPath, , ) } .Names[] = } collectImportQualifiers(, .OutputBase, .Imports) } return , nil } // collectImportQualifiers records, for each import qualifier used by a hand-written // file in the destination package, the base name of a file that uses it (a hint for // the collision diagnostic). The file plumb will overwrite (outputBase) is skipped // entirely: plumb rewrites its imports, so a qualifier used only there can never // collide with a generated set name; only one a hand-written sibling still uses // can. This makes gen the sole owner of output-file exclusion, for import // qualifiers as for top-level names (see buildDestInfo). Among the eligible files // the first base in sorted order wins, so the result never depends on the loader's // file-iteration order. func ( *discover.Package, string, map[string]string) { := map[string]string{} for , := range .Types.Imports() { [.Path()] = .Name() } := slices.SortedFunc(slices.Values(.Syntax), func(, *ast.File) int { return strings.Compare(discover.FileBase(, ), discover.FileBase(, )) }) for , := range { := discover.FileBase(, ) if == { continue } for , := range .Imports { := "" if .Name != nil { if .Name.Name == "_" || .Name.Name == "." { continue // blank/dot imports introduce no qualifier } = .Name.Name } else { , := strconv.Unquote(.Path.Value) = [] } if == "" { continue } if , := []; ! { [] = } } } }