ruff_python_spo: add a plain-Python SPO harvest arm - #95
Merged
Conversation
extract()/extract_with() gate every class behind Odoo markers
(walk::is_model_base, or a declared _name/_inherit) before it becomes
a Model. That gate provably drops the entire monarch-initiative/dismech
resolver: 586 classes, 878 module-level defs, 201 dataclasses, zero
Odoo markers -> extract() returns an empty graph over it (confirmed
below, not assumed).
extract_plain()/extract_plain_from_source() drop the gate: every
top-level class becomes a Model (no base-class/_name/_inherit check),
class bases lower to the existing frontend-agnostic Model::inherits
carrier (no new Predicate variant -- the enum is closed), and every
method routes through the SAME functions::analyze_method the Odoo arm
already uses. That function only ever looks at `self`, for/if/raise/
Assign statements, and a closed ORM-mutator name list -- none of it is
Odoo-specific, so it needed zero changes to become reusable (mirrors
ruff_cpp_spo's walk_free_functions precedent for a language-generic
frontend body-walker). Module-level defs attach to a synthetic
per-module Model. AnnAssign fields always record; bare `x = <literal>`
fields record only when the literal kind is trivially inferable
(str/int/float/bool/list/dict/set/tuple) -- never guessed.
Measured over dismech (cargo run -p ruff_python_spo --example
plain_census -- <dismech>/src/dismech):
odoo baseline: models=0
plain: models=658 functions=1277 fields=3587 inherits_edges=546
body facts: reads=1148 writes=635 raises=83 traverses=2
guarded_writes=6 calls=57
9 new unit tests in plain.rs; 66/66 ruff_python_spo tests pass;
clippy -D warnings clean; cargo fmt clean.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_913241e8-3a15-4aa4-a7bc-02c9aaccab04) |
`<module>_<Class>` and the synthetic `<module>` model can collide -- `a/b.py`'s module model against a class `b` in `a.py` both normalise to `a_b`. `ModelGraph::models` is a Vec so nothing merges silently at this layer, but any consumer keying by name (the SPO expansion, ogar-from-ruff's lift to ogar_vocab::Class) would collide. Counted rather than assumed. Measured: 0 duplicates across all three real corpora (dismech 658 models, WoA 241, ruff/scripts 58). The counter is not vacuous -- it fires at 69 names covering 319 models over the ruff_linter fixture tree, where e.g. ruff/RUF053.py genuinely declares `class C` 38 times. That is a correct harvest of an adversarial fixture, not a naming defect, and it gives the counter both halves of the falsifiability pair: it can fire, and it stays silent on non-trivial real input.
Merged
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ruff_python_spo'sextract()gates on Odoo markers (models.Model/_name/_inherit), so it returns an empty graph on any plain-Python codebase. Measured, not assumed:models=0overmonarch-initiative/dismech's 90-file resolver package.functions::analyze_method— the part that actually earns the harvest (reads / writes / raises / traverses / calls / guarded_writes) — is already language-generic. Only theis_modelgate blocked it. This adds a second entry point that drops the gate and reusesanalyze_methodunmodified.extract_plain_from_source(source, module) -> ModelGraphextract_plain(root, namespace) -> ModelGraph— recursive*.pywalk mirroringcollect_py; module name from path (export/sepio_export.py→export.sepio_export, dot→underscore normalised as the Odoo arm does)Every top-level
classbecomes aModel; bases becomeModel::inherits(reusing the frontend-agnostic carrier and the existingInheritsFromlowering); module-leveldefs attach to a synthetic per-moduleModelso free functions keephas_function+ their body facts;AnnAssignand simple literal assigns becomeFields. Precedent:ruff_cpp_spo'swalk_free_functions.No new
Predicatevariants — the enum is closed and stays closed;expand.rswas read to confirmhas_field/field_type/InheritsFromlower generically rather than Odoo-coupled.Measurement
The
models=0baseline is measured in the same run over the same root, not asserted.Genericity check on a second, unrelated corpus —
/home/user/WoA/woa(Flask + SQLAlchemy, no Odoo, no relation to dismech), run untouched:Different ORM, different framework, different shape (function-heavy rather than field-heavy, as Flask blueprints should be), no code change required.
Deliberately conservative, with the gaps named
Name, orSubscript's head) and falls toNonefor unions /X | None/ forward-ref strings. dismech itself contains dataclasses,Enums,TypedDict-likes and heavyOptional[...]— the rule had to survive within-corpus variety before it ever met a second corpus.logger = logging.getLogger(__name__)must not become a field.Assignconstants are invisible — which notably means CURIE-shaped constants likesepio_export.py'sHAS_PATHOPHYSIOLOGY = "dismech:has_pathophysiology"are not harvested — and nested classes aren't walked (same posture as the Odoo arm). Both are contained follow-ups.Test plan
cargo test -p ruff_python_spo— 66/66 (57 pre-existing + 9 new), independently re-runcargo clippy -p ruff_python_spo --all-targets -- -D warnings— cleancargo fmt -p ruff_python_spo— cleanuvx prek run --from-ref <base>— clean (onetyposauto-fix, committed separately, tests re-verified green)New tests assert exact counts (
==, not>=) and cover: dataclass fields, base + method quartet, module-level def, unparsable source, empty module, literal-inference boundary, subscript/complex-annotation boundary, module-name normalisation, tree walk.Generated by Claude Code