Inheritance Lint or Static Analysis¶
Static-analysis tool — instantiates Controlled Inheritance Propagation
Scans an inheritance graph statically for structural smells — chains too deep, overrides that shadow nothing, exceptions past their budget — and flags them before they reach runtime.
Inheritance Lint or Static Analysis is the automated inspector that reads an inheritance graph without running it and flags structural smells before they reach production. It resolves no values and traces no specific change; it patrols the shape of the hierarchy — how deep chains run, how many overrides pile up on a member, which overrides shadow nothing — and raises warnings against declared limits. Its defining idea is prevention by structure: many inheritance problems are visible in the static graph (an 11-level chain, a redundant override, an exception budget blown) long before they cause a bug, so a linter converts them into blocking CI findings rather than production surprises. It judges structure and counts, not meaning.
Example¶
A game studio's entity system began as Entity → Actor → Character → Player and, over three years, grew branches eleven levels deep. Builds pass, but new hires can no longer tell where a given behavior comes from. The team adds an inheritance linter to CI. On the next run it raises two classes of smell. Depth: six chains exceed the team's limit of six levels, each reported with the offending path. Override budget: the update() method is overridden 30 times, and nine of those overrides carry a body identical to the parent's — dead overrides that shadow nothing and only add noise.
None of these is a runtime bug today; all of them are debt the linter makes visible and countable. That lets the team set a ratchet — no new class deeper than six levels — and burn down the dead overrides deliberately, instead of discovering the tangle during the next incident.
How it works¶
- Build the graph. Parse the hierarchy into a static graph — classes or scopes as nodes, inherits-from as edges — without executing anything.
- Measure structure. Compute inheritance depth per leaf, override count and override-to-inherit ratio per member, overrides whose body or effect is identical to the ancestor's (dead), and exceptions exceeding a declared budget.
- Compare to limits. Emit a warning or error wherever a measure crosses its configured threshold, ideally gating CI so new violations cannot merge.
- Report where, not why. Point at the exact class or path; remediation is left to the human.
Its distinguishing move is that it judges the graph's structure and counts against limits, statically — it never needs the resolved values or a runtime, which is what separates it from the effective-state diff and the impact report.
Tuning parameters¶
- Depth limit — the maximum chain length before a warning. Lower forces flatter, more legible hierarchies but pushes teams toward composition; higher tolerates deep specialization.
- Override budget — how many overrides, or what override ratio, a member may accumulate before it is flagged. Tight budgets catch god-methods early; loose ones cut noise.
- Dead-override sensitivity — whether to flag only byte-identical overrides or also effectively no-op ones. Higher sensitivity finds more debt and more false positives.
- Severity and gating — warn versus block the build. Blocking stops new debt cold but can wall off an urgent fix; warning informs without enforcing.
- Ratchet vs absolute — enforce a hard limit everywhere, or forbid only new violations while grandfathering existing ones — the usual way to adopt a linter on a legacy graph.
When it helps, and when it misleads¶
Its strength is catching inheritance debt while it is cheap — in review, not in an incident — and turning fuzzy worries ("this hierarchy is too deep") into countable, gated facts. A ratchet lets a team stop the bleeding without a big-bang refactor.
Its failure mode is that it sees structure, not meaning: a deep chain can be perfectly sound and a shallow one deeply wrong, and the linter cannot tell them apart. Chasing its metrics for their own sake invites Goodhart's law — teams flatten a hierarchy to satisfy a depth rule and smuggle the complexity elsewhere, or delete "dead" overrides that were subtly load-bearing. The deeper hazard it can only hint at is the fragile base class problem — a base change silently breaking descendants — which lives in runtime semantics the static graph cannot see.[1] The discipline is to treat lint findings as questions, not verdicts, tune limits to this codebase rather than importing someone else's, and pair structural lint with the semantic checks (substitutability, impact tracing) it cannot perform.
How it implements the components¶
Inheritance Lint realizes the structural-guardrail side of the archetype — bounding the shape of the hierarchy, not its values or meaning:
inheritance_depth_limit— measures each chain's depth and flags those past the configured maximum, keeping lineages shallow enough to reason about.override_budget_or_exception_limit— counts overrides and exceptions per member and flags dead or over-budget ones, bounding how much local deviation quietly accumulates.
It does NOT guarantee subtype substitutability — that contract is Object-Oriented Class Inheritance's type system; it does not trace the blast radius of a specific ancestor change — that's Lineage Impact Analysis Report; and it does not retire overrides on a schedule — that's Override Expiry Workflow.
Related¶
- Instantiates: Controlled Inheritance Propagation — the automated guardrail that keeps the inheritance graph's shape within declared limits.
- Consumes: the lineage substrate it analyzes — Object-Oriented Class Inheritance or Configuration Inheritance Tree.
- Sibling mechanisms: Object-Oriented Class Inheritance · Lineage Impact Analysis Report · Configuration Inheritance Tree · CSS Cascade and Specificity Rule · Effective Configuration Diff · Override Expiry Workflow · Schema Extension and Override Check · Prototype Delegation Chain · Permission Inheritance with Explicit Denial · Policy Inheritance Matrix · Platform Variant Option Model · Template Clause Override Register · Trait or Mixin Composition Rule
Notes¶
The analysis is static, so anything wired up at runtime — dynamic mixins, reflection, monkey-patching, plugins resolved on load — is invisible to it. A clean lint report over a heavily dynamic system says less than it appears to; there, the effective-state and impact-tracing mechanisms carry more of the weight.
References¶
[1] The fragile base class problem — modifications to a base class inadvertently breaking derived classes that relied on its internal behavior — is a classic hazard of implementation inheritance. Because it lives in runtime semantics, a static structural linter can flag warning signs (deep chains, heavy overriding) but cannot detect the breakage itself. ↩