constrains-ignores-dotted-names said "api.constrains silently ignores
dotted field names". Odoo 18 is not silent about it: _constraint_methods
logs "@constrains parameter %r is not a field name" the first time the
model collects its constraints (odoo/models.py, 18.0 at 3c3e3b3), and
the check still never fires for that name. The title now reads "ignores
dotted field names without an error", which is exact. The why already
said a dotted name is not an error, and is unchanged.
The title is embedded text, so nexa build re-embedded the index and
regenerated the Modelfile, and ai-odoo was re-registered. Retrieval was
measured before and after: 28 of 28 at top 6 and 26 at top 3 both times,
no rule changed rank, and the rule stays first after the pinned one for
its own probe, at 0.700 where it was 0.694.
794 passed, 2 skipped; every marked figure agrees.
odoo.sql-constraints-are-ignored-from-19, from OpenSPP 208d9758 (#394):
a _sql_constraints block on a 19.0 module, which Odoo 19 logs once and
never creates, so duplicates were saved silently. An AST pass over the
file plus the manifest's series, legacy_sql_constraints_on_19. Reported
on 19+; silent on 16 to 18, where it is the only spelling; silent when a
walked tree pins no series (odoo core, 174 files); advisory per finding
when nothing was walked at all.
Fires nowhere at HEAD on the three trees, as expected: OpenSPP had
migrated, OCA and core are 18.0. OpenSPP's history holds the evidence:
it catches all four fixed instances (three in ca44b5e3, one in 208d9758)
on the files before their fix and none after, and nexa check reports the
#394 file at the line the fix removed. Graded defect: on 19 the attribute
does nothing, so every finding is a constraint that does not exist.
Also corrects "models.Constraint arrived in Odoo 17": 18.0 core has none
and the 18.0 ORM reference has no Co…
24 days ago
datetime: Odoo runs in UTC, and the rule that carried the Python half did not know
Sampling naive-and-aware-datetimes on core found its premise false inside
Odoo. import odoo pins the process to TZ=UTC (odoo/_monkeypatches, as
OpenERP did since 2013), and fields.Datetime.now() is datetime.now()
itself. There is no server offset for datetime.now() to be wrong by. Core's
sample is 0 of 15.
Re-read under a criterion that does not depend on the server clock, the
OpenSPP and OCA verdicts are 3 true of 33, from 21. Eighteen reversed,
each keeping was: true. What stands: naive UTC timestamps leaving through
an API with no offset (the shape recall's two catches fixed with
datetime.now(UTC)), and the server's UTC date used as a company's calendar
day. The composed-in Python rules go from 27 of 51 to 9 of 51.
The rule's advice was wrong for Odoo too: an aware datetime written to a
Datetime field raises ValueError. It now says aware belongs at the edges
and names fields.Date.context_today for a user's day. Graded preference,
so it gates nothing; narrowing the checker to the t…
Four stacks become one. The TypeScript and JavaScript rules are removed, the
eight plain-Python rules are composed into the Odoo stack, and a file in any
other language is refused by name rather than reviewed against rules that
cannot match it.
WHY REMOVE RATHER THAN LABEL. Firing the other twenty-five rules at real code
decided it: JavaScript classified out at 21 true against 22 false, TypeScript
was measured at 3.8/kloc and never classified. A rule that costs nothing at
query time is not free -- it is a claim the README carries and a file `check`
accepts and then says nothing useful about. An empty finding list reads as a
clean bill of health, so `subject.Unreviewable` refuses the file instead.
WHY COMPOSE RATHER THAN DELETE. Selection was strict equality on `stack`, so an
Odoo module got the 19 ORM rules and none of the 8 Python ones -- and Odoo
modules are Python. `fieldtest --stack python` over OpenSPP found 32 hits
`--stack odoo` never reported. "ruff covers it" does not hold a…
review: a pass over the tool itself, and the silence it was keeping
The corpus has been measured from every angle; the code running it had not
been read once. The worst thing here was not a wrong answer but an absent
one.
A SYNTAX ERROR ANYWHERE ERASED EVERY AST FINDING. Each semantic checker
parses for itself and returns [] when that fails, so nine of them agreed
there was nothing to say and `check` printed "no checker findings" and
exited 0. A file with a real stale-compute bug reported it; the same file
with a trailing `def oops(:` reported nothing. scan_source now parses once
and returns a gap with the line number in it.
@api.depends DID NOT COVER ITS OWN PREFIX. Declaring 'line_ids.price'
registers the whole chain, so a body reading rec.line_ids needs nothing
more. Exact string matching called that stale: 42 of the rule's 104 hits
on OpenSPP, 40% of its output. Its own `correct` sample hid it by
declaring the prefix redundantly, which is why validate never saw it.
THE OTHER 25 RULES HAD NEVER BEEN FIRED AT REAL CODE. Every committed
artifact wa…
rules: except-order ships advisory, and advisory grows a second ground
Asked to ship the exception-shadowing checker advisory. It does not fit the
label as written -- "states a design preference rather than a defect" -- because
`except Exception` above `except KeyError` is dead code, not a taste. Shipping
it under that wording would make the field mean two things and stop being worth
reading.
So the field names two grounds instead, each with its own bar. One mechanism
serving two reasons is fine; one word meaning two things is not.
preference a better way to write correct code
bar: recall --aim, never fires on a line somebody fixed
unproven a real defect nothing measured here has met yet
bar: zero findings in fieldtest, no incident in recall
prefer-sql-constraint is the first. odoo.except-order-follows-the-hierarchy is
the second, and the rule's own `why` says which it is, as the schema now
requires.
HOW THE RULE GOT HERE, INCLUDING THE WRONG PART
A tally over the 62 fixes the corpus was silent on said nine…
crosswalk: an outside opinion, and three rules with something behind them
Two gaps in `nexa check`, both of them about the corpus rather than the code:
it can only find what it has a rule for, and the worklist that was meant to
say where the missing rules are had been pointing at the wrong place.
AN OUTSIDE OPINION
crosscheck, adversarial, fieldtest and recall all ask whether what we wrote
behaves. None of them can ask whether it is the right set of things to have
written, because a rule nobody thought of is invisible to every check that
reads the rules.
OpenSPP runs semgrep against 44 hand-written Odoo rules of its own. `nexa
crosswalk` compares the two corpora without running semgrep -- not installed
here, and an offline tool will not grow a package index for one question.
Each of their patterns becomes the smallest snippet it would match and OUR
checkers run over that, so every pairing rests on a checker firing rather
than on two rule descriptions sounding alike.
their patterns turned into a probe 146 of 146
both corpora fire on the sam…
python: a fourth stack, so a plain .py file gets a real answer
Adding the stack is most of this. Registering its model is what exposed the
rest.
1. The stack.
nexa check mapped every .py to Odoo, then for a while refused a .py with no
odoo import. Refusing was honest -- fifteen ORM rules produce fifteen findings
about a framework the file does not use -- and it was not an answer.
Eight rules, the same subject as the rest of the corpus: validation, and the
places a check that looks like it holds does not.
assert-is-not-validation -O removes it, so it is absent in
production and present in development
mutable-default-argument evaluated once, shared by every caller
bare-except-catches-the-exit swallows KeyboardInterrupt and SystemExit
eval-and-pickle-execute-their-input
subprocess-shell-true-is-injection
path-join-does-not-contain-a-path an absolute second argument discards the
base entirely
decimal-for-money-not-float
naive-and-aw…
modelfiles: write LF, like the rest of the generated corpus
Same defect build_corpus.py had before this pass: opening the output in text
mode lands CRLF on Windows, so every regeneration was a whole-file diff and
the committed Modelfiles depended on which machine last ran nexa build.
Caught by .gitattributes warning on the previous commit, which is the file
doing its job.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
checkers: two more move to AST, and the OCA findings are classified
Item 3 and item 5 from the open list, and they turned out to be the same
piece of work: classifying the hits is what showed where the checkers were
wrong.
check-company-on-relations was a regex over one declaration, and whether the
model has a company at all is the condition that decides the rule. Of its 37
hits on OCA:
20 the class declares company_id locally true positives
11 TransientModel wizards a different, smaller problem
6 _inherit = 'res.company' wrong -- odoo/fields.py has
a branch for exactly this
It now requires the model to be company-scoped, read from the code rather
than assumed: the class declares a company_id of its own. A class that only
inherits one cannot be resolved without an Odoo registry and is skipped
rather than guessed -- silence over a false critical. 37 -> 19.
stored-compute-needs-complete-depends never checked the word in its own name.
Classifi…
Validation was two checks: do the cited rule ids exist, and does the code match
a violation pattern. Both survive as the knowledge and domain layers, but
neither ever saw an answer whose Python does not compile, whose @api.constrains
omits a field it reads, or whose imports name a module that is not installed.
Those were passing.
Every answer now goes through nine layers -- syntactic, structural, semantic,
knowledge, domain, context, empirical, adversarial, provenance -- and each
reports a verdict, what it checked, and what it could not check and why. A layer
that cannot run reports UNKNOWN, and the decision carries that forward as
INSUFFICIENT EVIDENCE rather than letting it read as a pass. Confidence comes
back as four separate words and is never averaged into one number.
Two real defects this found in the existing product:
* All 12 satisfied_patterns in the corpus could be cleared by a COMMENT quoting
the fix instead of applying it, four of them on critical rules. Writing an
…