Skip to content

Deepen: replace the twelve hand-maintained _selected_* rule-name sets with a lazily-memoised analysis context #167

Description

@jonbaldie

Recommendation strength: Strong

Modules

src/messpy/cli.py — _findings and the _selected_* family (_selected_parents, _selected_classes, _selected_design_parents, _selected_design_contexts, _selected_design_bindings, and the rest), plus the 29 rule functions downstream of them.

Problem

Between "we parsed a file" and "we ran the rules" sits a layer of twelve functions whose entire job is to decide, from the set of enabled rule names, which precomputations to perform eagerly. Each one hand-lists the rules that need its product. Three of them list different subsets of the same five design rules.

This layer is a pure eagerness optimisation, not a correctness mechanism. Every one of the 29 rule functions already re-checks whether its own rule is enabled — the shape is repeated 29 times:

    rule = _rule(rules, NAME)
    if rule is None:
        return []

So the _selected_* sets never change what is reported; they only change what work is done before the rules ask. That makes them a shallow layer with an expensive failure mode. The interface (twelve functions, twelve rule-name sets to keep in sync, threaded into _findings as eight precomputed values) is nearly as complex as the implementation, and the penalty for getting a set wrong is silent: add a rule that needs parents, forget to add its name to _selected_parents, and the rule quietly returns nothing for some inputs. No test fails, because no test asserts the invariant.

The invariant currently holds — running each rule alone via --only produces the same findings as the full run. But it holds by hand, and nothing guards it.

The plumbing cost shows up downstream too: 19 functions in cli.py take a parents: dict[int, ast.AST] parameter and 16 take a rule_names: AbstractSet[str] parameter, all so this eager layer's products can reach the rules that need them. _design_findings alone carries seven parameters.

Proposed deepening

Introduce an analysis-context module — one object per analysed file — with a small interface of lazily memoised properties: tree, parents, classes, bindings, import_aliases, class_index, and so on. Rules take the context and ask for what they need; the first ask computes and caches, subsequent asks are free.

Then delete all twelve _selected_* functions. The eagerness decision disappears entirely, because nothing is computed that nobody asks for — which is a stronger guarantee than the current sets provide, and it is enforced by construction rather than by maintenance.

Deletion test: deleting this layer concentrates complexity into one memoisation policy rather than spreading it across twelve rule-name sets. Yes.

Benefits

Locality. Adding a rule stops being a two-place edit (write the rule, then remember to amend the right _selected_* set). The dependency is declared where it is used — at the point the rule asks the context for parents — instead of in a list maintained at a distance.

Leverage. The context is one interface that replaces both the twelve selection functions and the parameter threading. The parents parameter can come out of 19 signatures and rule_names out of 16; _design_findings shrinks from seven parameters toward two. Rule authors learn one object instead of a precomputation protocol.

Test impact

The right order of work is: add the guard first, then delete the layer. Specifically, add a differential acceptance test that, for a representative fixture tree, runs the analyzer once with all rules enabled and once per rule with --only, and asserts the per-rule findings union equals the full-run findings. This is an end-to-end test driving run(argv, stdout, stderr) on real source, exactly the default proof CODING_STANDARDS.md asks for, and it pins the invariant the _selected_* sets currently maintain by hand.

That test is valuable on its own even if this ticket is rejected: it converts a silent, class-wide failure mode into a loud one. With it green before and after, the deletion of the twelve functions is verifiable rather than hopeful. No new unit tests and no mocks are needed — the context is exercised through the same CLI surface.

Activity

  1. jonbaldie commented on Sep 20, 2026

    @jonbaldie
    ContributorAuthor

    This was generated by AI during triage.

    Agent Brief

    Category: enhancement
    Summary: Replace eager _selected_* rule precomputations with a lazily-memoized per-file analysis context

    Current behavior:
    Between file parsing and rule execution, twelve hand-maintained functions (_selected_parents, _selected_classes, _selected_design_parents, _selected_design_contexts, _selected_design_bindings, etc.) eagerly precompute AST metadata based on enabled rule names. Each function manually specifies the subsets of rules that require its data. If a rule requiring a precomputed AST property is omitted from a _selected_* function, it silently produces no findings when executed. Additionally, parameters like parents and rule_names are threaded through up to 19 functions.

    Desired behavior:
    Introduce an AnalysisContext object per analyzed file that lazily computes and caches AST properties (such as tree, parents, classes, bindings, import_aliases) upon first request by a rule. Rules receive or query the context for the specific AST properties they need. The twelve eager _selected_* functions and extensive parameter threading are removed. Before removing the precomputation functions, a differential acceptance test verifies that analyzing a file with all rules enabled produces the exact union of findings from running each rule individually with --only.

    Key interfaces:

    • AnalysisContext: per-file context exposing lazily memoized AST analysis properties.
    • Rule functions: updated to retrieve required AST structures from AnalysisContext rather than receiving threaded parameter tuples.

    Acceptance criteria:

    • A differential acceptance test is added verifying that running all rules produces the exact union of findings from running each rule individually with --only across a representative fixture suite.
    • The twelve eager _selected_* precomputation functions are removed.
    • An AnalysisContext abstraction provides lazily computed, cached access to shared AST attributes (parents, classes, etc.).
    • Downstream rule functions access needed attributes via the analysis context, reducing signature parameter bloat.
    • Finding output across all rules, rulesets, and options remains identical to baseline.
    • All existing test suites pass and self-analysis remains clean.

    Out of scope:

    • Modifying rule finding messages, priorities, or detection logic.
    • Introducing cross-file caching or persistent disk caches.
  2. added
    ready-for-agentFully specified, ready for an AFK agent
    and removed
    needs-triageMaintainer needs to evaluate this issue
    on Sep 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestready-for-agentFully specified, ready for an AFK agent

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions