Documentation
¶
Overview ¶
Package contractcheck inspects a project's internal/ tree for contract-shape violations. It is the single home for the analyzers that previously lived as four sibling files under internal/linter/forgeconv/:
- internal-package-contract-names (Service/Deps/New canonical shape)
- deps-are-interfaces (every Deps field is an interface)
- outbound-io-no-rpc (an outbound boundary registers no RPC)
The forge entry points that used to call those analyzers individually (`preCodegenContractCheck` in internal/cli/generate.go and `runConventionLint` in internal/cli/lint.go) now route through one Inspect function. Per-call-site differences — which rules to run, what to exclude — are expressed as fields on Options, not as branches inside the engine.
API design notes ¶
Findings carry the same shape as the other forge lint rules (forgeconv.Finding) so they can be folded into the existing `combined := forgeconv.Result{}` pipelines without adapter glue. Severity, rule names, and remediation phrasing are preserved verbatim from the old call sites — this package is a re-organization, not a behavior change.
What this package is NOT ¶
The cmd/contractlint binary (and its supporting internal/linter/contract package) performs a fundamentally different check: it runs as a go/analysis multichecker that needs full type information to assert "exported methods on impl types must be declared in the contract interface". That check operates on *analysis.Pass, not on a rootDir walk, and so cannot share this engine. The two are complementary — contractcheck catches structural-shape mistakes at codegen time (before the type checker has anything to chew on); the multichecker catches behavioral-shape mistakes at vet time.
Index ¶
Constants ¶
"runs and the codegen this rule protects does not exist here. The shape is still worth keeping; " +
"`forge lint --strict` gates it.)"
CodegenUnavailableNote is appended to the remediation of a finding downgraded under Options.CodegenUnavailable, so the reader sees both why it no longer gates and how to make it gate again.
Variables ¶
This section is empty.
Functions ¶
func AsResult ¶
AsResult wraps findings in a forgeconv.Result for callers that want to feed the output through the existing FormatText / combined-result plumbing without changing their shape.
func HasErrors ¶
HasErrors reports whether any finding in fs has Severity == SeverityError. Convenience so call sites don't have to roll their own loop.
func Inspect ¶
Inspect runs the configured rules over rootDir and returns findings in deterministic (file, line, rule) order. A missing rootDir/internal directory is not an error — it produces an empty result.
The context is honored only at coarse granularity: between rules. Individual rule implementations are tree-walks that complete in milliseconds on real projects, so we don't propagate ctx into the walk loops.
Types ¶
type Options ¶
type Options struct {
// Rules is the subset of rules to run. nil or empty means
// "run all rules in [AllRules]".
Rules []Rule
// Excludes is the contracts.exclude list from forge.yaml — a list
// of module-relative slash paths to skip wholesale. The two
// canonical-shape rules consult it — internal-package-contract-names
// (what a contract.go must declare) and internal-package-missing-contract
// (that a component-shaped package have one at all). Both ask whether
// forge's shape applies to the package, so "not bootstrap-managed" is a
// real exemption from both, and honoring it in one but not the other
// would tell an excluded package to run the very verb the exclusion says
// it is out of. The remaining rules assert properties that hold
// regardless of whether forge wires the package, and honoring the
// exclude list there would silence them on exactly the packages most
// likely to need them.
Excludes []string
// this tree (no forge.yaml): a contract.go yields no bootstrap wiring,
// no mock and no decorator. internal-package-contract-names is an
// ERROR only because "the bootstrap template hardcodes these names and
// a non-canonical contract produces a bootstrap that doesn't compile",
// so with no bootstrap it reports as a WARNING that says why. The
// other rules keep their severity: all-mock testability does not
// depend on codegen.
CodegenUnavailable bool
}
Options controls a single Inspect run.
Per-call-site differences between forge's contract-check entry points (pre-codegen check vs. `forge lint --conventions`) all reduce to differences in these fields. Adding a new entry point should never require modifying the engine — it should pick the rule subset and excludes it needs and call Inspect.
type Rule ¶
type Rule string
Rule identifies a contract-shape analyzer by its canonical name. The names match the strings that already appear in finding output, so log-search queries and CI grep patterns survive this refactor unchanged.
const ( // RuleInternalPackageContractNames enforces the canonical // `type Service interface`, `type Deps struct`, and // `func New(Deps) Service` trio on every internal-package // contract.go. Errors gate the build because the bootstrap codegen // template hardcodes these names — a non-canonical contract // produces a bootstrap that doesn't compile. RuleInternalPackageContractNames Rule = "forgeconv-internal-package-contract-names" // RuleDepsAreInterfaces warns when any package declares a Deps // field whose type is a concrete struct (or pointer to one) rather // than an interface. Concrete deps defeat the all-mock unit-test // surface that `New(Deps)` exists to provide. RuleDepsAreInterfaces Rule = "forgeconv-deps-are-interfaces" // RuleOutboundIONoRPC warns when a package marked // `// forge:outbound-io` registers a Connect RPC handler. The // marker asserts the package only calls OUT; an inbound handler // contradicts it, and the RPC surface is the actual service. RuleOutboundIONoRPC Rule = "forgeconv-outbound-io-no-rpc" // RuleInternalPackageMissingContract warns when a package under // internal/ has a component's shape but no contract.go — a component // created by hand instead of by `forge scaffold package`. Everything // downstream keys on contract.go (bootstrap wiring, mock generation, // the observe decorator, every reporter), so such a package is not // half-wired, it is invisible. RuleInternalPackageContractNames only // ever looked INSIDE a contract.go; this is its complement. RuleInternalPackageMissingContract Rule = "forgeconv-internal-package-missing-contract" )
The set of rules this package owns. New rules added here MUST be surfaced in AllRules so the default "run everything" path picks them up.
func AllRules ¶
func AllRules() []Rule
AllRules is the canonical "run everything" list. Callers that pass Options.Rules == nil get this set. It is a getter (not an exported package var) so the returned slice can't be mutated by callers and so it satisfies the exported-vars contract rule (no mutable exported package state).