Skip to content

fix(parser): resolve model bases across the whole scan, not per-file (#20) - #22

Merged
FROWNINGdev merged 2 commits into
FROWNINGdev:mainfrom
Nitjsefnie-OSC:cross-module-bases
Jul 19, 2026
Merged

fix(parser): resolve model bases across the whole scan, not per-file (#20)#22
FROWNINGdev merged 2 commits into
FROWNINGdev:mainfrom
Nitjsefnie-OSC:cross-module-bases

Conversation

@Nitjsefnie

@Nitjsefnie Nitjsefnie commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Fixes #20.

What

scan_workspace now collects class definitions from every parsed file first, then runs the transitive is_model_name resolution once over the union — so a base class only needs to be defined somewhere in the scanned tree, not in the same file as its subclass. Mechanically, parse_models_file's tail was split into _collect_defs (pass 1, per file) and _resolve_and_filter (pass 2); parse_models_file composes both, so single-file behavior (and the parity/abstract tests that call it directly) is unchanged, and scan_workspace calls pass 2 once over all files' defs, regrouping resolved models back into their apps by file path afterward.

Includes the two-module regression fixture from the issue: common/models.py defines an abstract TimeStampBase, app/models.py defines Article(TimeStampBase), and the test asserts Article survives the scan (it returns [] on the current parser).

A gotcha worth knowing about

The issue's illustrative name TimeStampedModel is a trap: it is hardcoded in _looks_like_model's heuristic pattern list, so a fixture using that literal name is recognized by name alone and the regression test passes even against the unfixed parser. Caught by mutation-testing the test itself — the fixture base is deliberately named TimeStampBase, with a comment explaining why.

Honest notes

  • The golden fixtures don't visibly change. All five vendored projects scan identically before/after (verified with a full ordered structural diff, not just name sets). In particular the BlogCategory case does not newly appear: mezzanine/core/models.py (where Slugged lives) was never vendored by scripts/fetch_golden_fixtures.py, and no resolution scope can find a definition that isn't in the tree. Happy to extend the fetch script to vendor it as a follow-up if you want the golden fixture to demonstrate this fix.
  • Known tradeoff of name-based union resolution (inherent to the approach the issue proposes): scanning unrelated projects in one combined tree can cross-pollinate bare names — scanning the whole tests/fixtures/golden/ directory at once (as the timing test does) picks up django-cms's Page/PageType because wagtail's unrelated Page is a real model with the same name. In single-project use this only bites if two genuinely different classes in one project share a class name. No test assertions are sensitive to it; flagging it so the choice is yours.
  • The VS Code extension's TypeScript parser presumably has the same per-file limitation; Parser drops models whose base classes live in another module #20 targets the CLI, so this PR is scoped to cli/.

Verification

  • Suite: 48 passed on clean main → 50 passed with the fix (py3.13, and py3.12 + Django 5.2; the CLI suite never actually imports Django — it only appears inside fixture source strings).
  • Mutation proof: with the new tests in place, reverting only parser.py to main fails exactly 1 test — test_model_with_cross_module_base_is_present, AssertionError: 'Article' not found in [] — collection stays at 50, so the test genuinely bites.

Generated by Claude Fable 5 (brief, review), Claude Sonnet 5 (implementation)

Summary by CodeRabbit

  • Bug Fixes

    • Models inheriting from base classes defined in other modules are now detected correctly.
    • Abstract base models are excluded from discovered model results.
  • Tests

    • Added coverage for cross-module inheritance and abstract model filtering.

Nitjsefnie and others added 2 commits July 18, 2026 21:28
The 2-pass transitive resolution in parse_models_file only matched a
model's base classes against definitions collected from the SAME parsed
file. A concrete model inheriting an abstract base defined in another
scanned module (e.g. a shared common/models.py) was silently dropped
from scan_workspace's output, since the base's name never entered
is_model_name.

Split parse_models_file into _collect_defs (pass 1: gather class defs)
and _resolve_and_filter (pass 2: transitive resolution + abstract drop).
parse_models_file composes both, unchanged for single-file callers.
scan_workspace now collects defs from every file first and calls
_resolve_and_filter once over the union, so bases defined in a
different scanned file are found.

Adds a two-module regression fixture (cross_module/common + app) and
test proving Article(TimeStampedModel) is no longer dropped when the
base lives in a different file.

Fixes FROWNINGdev#20.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
TimeStampedModel (the name used in issue FROWNINGdev#20's illustrative example) is
one of the hardcoded heuristic tail names in _looks_like_model, so a
class inheriting it is recognized as a model by name alone, without any
base resolution — the fixture test passed even against the unfixed
per-file parser, silently failing to exercise the bug. Renamed to
TimeStampBase, which does not match any heuristic pattern; the fixture
now only passes when the base is genuinely resolved.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@FROWNINGdev
FROWNINGdev merged commit 7f01973 into FROWNINGdev:main Jul 19, 2026
13 checks passed
Repository owner deleted a comment from coderabbitai Bot Jul 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Parser drops models whose base classes live in another module

2 participants