Module: Rigor::Effects::LiskovCheck
- Defined in:
- lib/rigor/effects/liskov_check.rb
Overview
Judges an override against the envelope it inherits (ADR-103 WD1 / WD14; #386). The one place
effect.liskov-widened is decided.
An envelope is a contract about a method, and Ruby's class PgRepo < Repo says a PgRepo is
usable wherever a Repo is. So a bound written on Repo#find binds PgRepo#find too:
implementations may be purer than the bound they inherit, never less pure. That is Liskov
inclusion applied to the second dimension, and it is what makes the declared lane's nominal carrier
(#386, EnvelopeIndex) honest — a caller that imports ≤ io.db from a Repo-typed receiver is
entitled to that bound whichever subclass actually arrives.
Two comparisons, and an override is subject to exactly one of them:
- Proven against the inherited bound — the override declares nothing of its own, so what it
does is what the ancestor's bound has to admit. It reads the same lane
EnvelopeCheck reads, for the same reasons: the proven closure, undischarged per policy,
mutate.localtolerated, taint ignored. - Declared against the inherited bound — the override declares its own envelope, and a bound wider than the one it inherits is a Liskov violation in the declaration, before any body is consulted. Two authored bounds compared by subsumption; nothing proven enters it.
The split is exclusive on purpose. An override that declares its own envelope is already held to
that envelope by effect.envelope-exceeded, so running the proven comparison too would put two
diagnostics on one line for one label. What the author asserted is the thing Liskov has to judge;
whether the body honours the assertion is the other rule's question.
Both-sides-authored, in the ADR-35 sense: nothing fires unless an author wrote an envelope on the ancestor. That is the accepted construction the false-positive budget is spent under — a firing is never unsolicited.
Nominal subclassing only. An included module's method is not an override in this slice: Ruby's
ancestry puts an includer's own def ahead of the module's rather than under it, and the
substitutability argument that licenses the check is the subclass one. The superclasses table is
the collector's own, as-written and resolved here exactly as Propagator resolves it, so the
relation the check reads and the closed world the proven lane travels can never disagree.
Defined Under Namespace
Classes: Finding
Class Method Summary collapse
-
.run(table:, superclasses:, method_envelopes:, class_envelopes:, config_envelopes: {}, positions: EnvelopeCheck::Positions.empty, apply_tolerated: true) ⇒ Array<Finding>
Sorted by position then key then label.
Class Method Details
.run(table:, superclasses:, method_envelopes:, class_envelopes:, config_envelopes: {}, positions: EnvelopeCheck::Positions.empty, apply_tolerated: true) ⇒ Array<Finding>
Returns sorted by position then key then label.
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
# File 'lib/rigor/effects/liskov_check.rb', line 69 def run(table:, superclasses:, method_envelopes:, class_envelopes:, config_envelopes: {}, positions: EnvelopeCheck::Positions.empty, apply_tolerated: true) # The distributed strata, over a base of the raw per-method annotations: a base class whose method # exists only in `.rbs` — an abstract `def find: (Integer) -> User` with no Ruby body — has no # key in the table and so no distributed entry, and its bound is exactly the one an override # inherits. Distribution wins on collision, which is the same value for a key that has both. envelopes = method_envelopes.merge( EnvelopeCheck.distribute(table, method_envelopes, class_envelopes, config_envelopes) ) return NO_FINDINGS if envelopes.empty? parents = parent_map(table, superclasses) return NO_FINDINGS if parents.empty? findings = [] keys = table.keys keys.each { |key| collect(findings, table, key, envelopes, parents, positions, apply_tolerated) } findings.sort_by { |f| [f.path.to_s, f.line, f.key, f.label] }.freeze end |