// Package cli is the command-line boundary for plumb: it parses flags, drives // the loader, resolves the destination, runs the pure core, and writes output. // All diagnostics flow through the streams passed to Run so the behavior is // testable.
package cli import ( ) // Sentinel errors for destination resolution, so its failures are classified in // tests with errors.Is rather than by matching message text. var ( errNoPackages = errors.New("no packages scanned") errAmbiguousDestination = errors.New("ambiguous destination") errSyntheticImportPath = errors.New("no real import path") errInvalidImportPath = errors.New("invalid import path") errPackageNameRequired = errors.New("package name required") errInvalidPackageName = errors.New("invalid package name") errPackageNameMismatch = errors.New("package name does not match destination") ) // syntheticImportPath is the placeholder import path the go/packages loader // assigns to a package it synthesizes from file arguments (e.g. `plumb a.go`) // rather than resolving from an import-path pattern. It is syntactically a valid // path, so it passes module.CheckImportPath; plumb must reject it separately // because it is not a real, importable destination to qualify against. const syntheticImportPath = "command-line-arguments" const usage = `plumb is a demand-based compile-time dependency injection generator. Usage: plumb [-package-name=<name>] [-import-path=<path>] [-output=<file>] [-v] [packages...] plumb scans the matched packages for //plumb:<name> directives and writes one generated function per set. With no package pattern it scans the current directory. ` // Run executes plumb with the given arguments and streams, returning the process // exit code. It never calls os.Exit itself. Output is split across three writers: // stdout carries the generated source (when -output is unset), stderr carries // diagnostics and the tolerated-error notes, and report carries the -v // discovery-and-inference report. That report is the deterministic, // plumb-controlled stream, kept separate from the variable diagnostics so it can // be consumed on its own. // The command wires report to the same destination as stderr. func ( []string, , , io.Writer) int { := flag.NewFlagSet("plumb", flag.ContinueOnError) .SetOutput() .Usage = func() { _, _ = fmt.Fprint(, usage) } := .String("package-name", "", "package name for the generated `package` clause") := .String("import-path", "", "import `path` of the destination package") := .String("output", "", "output `file` (default: standard output)") := .Bool("v", false, "print a discovery-and-inference report to standard error") if := .Parse(); != nil { if errors.Is(, flag.ErrHelp) { return 0 // explicitly requested help is a success, not a usage error } return 2 } , := gopackages.Load(.Args(), "") if != nil { fail(, .Error()) return 1 } , , := resolveDestination(.Packages, *, *) if != nil { fail(, .Error()) return 1 } := gen.Options{ ImportPath: , PackageName: , OutputBase: baseName(*), } , := gen.Generate(, .Packages) if != nil { fail(, .Error()) // Surface any type errors tolerated at load time, so a genuine provider // type problem (not just a stale generated file) is not swallowed when // generation then fails for a related reason. noteToleratedErrors(, .TypeErrors) return 1 } if * { // On success the load errors are tolerated, but under -v surface them so a // not-fully-type-correct input (a malformed body, a type used only // internally) is visible rather than silently swallowed. noteToleratedErrors(, .TypeErrors) _, _ = fmt.Fprint(, .Report) } if := writeOutput(*, .Source, ); != nil { fail(, .Error()) return 1 } return 0 } // resolveDestination determines the destination import path and emitted package // name from the flags and scanned packages, inferring each when its flag is // omitted and rejecting the ambiguous or contradictory combinations. func ( []*discover.Package, , string) (, string, error) { = if == "" { switch len() { case 1: = [0].PkgPath case 0: return "", "", fmt.Errorf("%w: cannot infer -import-path", errNoPackages) default: return "", "", fmt.Errorf("%w: more than one package scanned; -import-path is required", errAmbiguousDestination) } } if == syntheticImportPath { // The synthetic path can arrive two ways: inferred from a file-argument load // (the user gave no -import-path), or passed verbatim. Say which, so the // advice fits: "pass -import-path" is wrong when they already did. if == "" { return "", "", fmt.Errorf("%w: could not infer one from the arguments; pass -import-path", errSyntheticImportPath) } return "", "", fmt.Errorf("%w: %q is not a real, importable package", errSyntheticImportPath, syntheticImportPath) } if := module.CheckImportPath(); != nil { return "", "", fmt.Errorf("%w: %q: %v", errInvalidImportPath, , ) } // Find the destination among scanned packages (same-package generation). var *discover.Package for , := range { if .PkgPath == { = break } } = if == "" { if == nil { return "", "", fmt.Errorf("%w: -package-name is required when generating into a package that is not scanned (%q)", errPackageNameRequired, ) } = .Name } if !token.IsIdentifier() || == "_" { return "", "", fmt.Errorf("%w: %q must be a valid Go identifier", errInvalidPackageName, ) } // Same-package mode: the destination's real name is known, and the generated // file must share it. Honoring a conflicting flag would emit an uncompilable // package clause, so reject the mismatch rather than emit broken code. (Checked // after validity so a malformed value is still reported as invalid.) if != "" && != nil && != .Name { return "", "", fmt.Errorf("%w: -package-name %q contradicts the scanned destination package %q; omit it in same-package mode", errPackageNameMismatch, , .Name) } return , , nil } func (, string, io.Writer) error { if == "" { , := io.WriteString(, ) return } // Write atomically so an interrupted run cannot leave a truncated file where // the previous good output was. plumb does not create intermediate directories; // writefile.Write preserves that (its temp-file create fails on a missing parent). return writefile.Write(, ) } func ( string) string { if == "" { return "" } return filepath.Base() } func ( io.Writer, string) { _, _ = fmt.Fprintf(, "plumb: %s\n", ) } // noteHeader is the fixed line noteToleratedErrors prints above the tolerated // go/types errors. It is deterministic (unlike the error bodies below it), so // tests key on it to confirm the note surfaced. const noteHeader = "plumb: note: tolerated load errors:" // noteToleratedErrors prints the load-time type errors plumb tolerated, under a // single stderr note header with one indented line per error. They are surfaced // on failure (a tolerated error may be the real cause) and under -v on success // (so a not-fully-type-correct load is visible). // // The errors print in the order the loader encountered them, which is not stable // across package load orders. That is deliberate: unlike the generated source // and the -v report (which plumb guarantees byte-identical across read orders), // these notes are a diagnostic aid, and showing the errors in the order they // actually occurred is more useful than imposing an artificial, stable sort. func ( io.Writer, []error) { if len() == 0 { return } _, _ = fmt.Fprintln(, noteHeader) for , := range { _, _ = fmt.Fprintf(, " %s\n", ) } }