narrow: one corpus, and it is Odoo
Browse filesFour 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 against
OpenSPP's own .ruff.toml, which selects B/C90/E501/I/UP and leaves S and DTZ
off, so six of the eight are genuinely unreported there.
MEASURED BEFORE SHIPPING, to the same bar as a new rule. 50 new findings across
OpenSPP and OCA, every one read in its own file: 26 true, 24 false, in
classification-python-2026-09.json. OpenSPP goes 162 -> 194 live findings
(0.71 -> 0.9/kloc), OCA 102 -> 120 (3.0 -> 3.5). Twenty of the 26 true are one
rule and one fact: an Odoo Datetime is naive UTC by contract, and a naive local
datetime reaching one is off by the server's offset. Three rules fired and were
never right, three never fired at all, and both lists are in the README rather
than quietly dropped.
Two defects the classification found and no test would have: scan_source
matches inside docstrings, and suppression reads any marker in a statement
without asking which rule it names. Named, not fixed here.
The id prefix now means provenance, not stack -- python.* rules carry stack
odoo -- so every committed classification, LIKELIHOOD grade and evidence bundle
still points at the same rule. cmd_validate checks the prefix against
corpus.ORIGINS instead.
prefer-sql-constraint had to be reworded: 27 rules competing where 19 were made
its old probe, the generic "stop anyone entering a negative quantity", stop
retrieving it. 27 of 27 retrievable, 25 in the top 3.
Also: the wordmark loses a row, five instead of six.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- ARCHITECTURE.md +98 -27
- INSTALL.md +4 -12
- README.md +193 -50
- bin/adversarial.py +4 -1
- bin/context.py +26 -56
- bin/corpus.py +34 -30
- bin/crosswalk.py +10 -13
- bin/eval.py +26 -36
- bin/fieldtest.py +19 -60
- bin/layers.py +26 -34
- bin/recall.py +17 -12
- bin/report.py +13 -6
- bin/subject.py +43 -26
- classification-js-2026-09.json +0 -408
- classification-python-2026-09.json +571 -0
- corpus/build_corpus.py +106 -417
- corpus/check_sources.py +12 -1
- corpus/corpus_meta.json +6 -8
- corpus/rule.schema.json +5 -7
- corpus/rules/javascript.json +0 -136
- corpus/rules/odoo.json +196 -1
- corpus/rules/python.json +0 -197
- corpus/rules/typescript.json +0 -238
- fieldtest-js-2026-09.json +0 -451
- fieldtest-oca-2026-09.json +1260 -0
- fieldtest-openspp-2026-09.json +331 -1
- fieldtest-py-2026-09.json +0 -50
- fieldtest-ts-2026-09.json +0 -2790
- modelfiles/gen-javascript.Modelfile +0 -107
- modelfiles/gen-odoo.Modelfile +103 -1
- modelfiles/gen-python.Modelfile +0 -141
- modelfiles/gen-typescript.Modelfile +0 -166
- nexa.py +29 -25
- nexa_cmds.py +8 -3
- subjects/README.md +7 -4
- subjects/web/src/board.js +0 -52
- subjects/web/src/partners.ts +0 -57
- subjects/web/tsconfig.json +0 -14
- tests/test_cli.py +1 -1
- tests/test_commands.py +1 -1
- tests/test_context.py +13 -50
- tests/test_crosswalk.py +5 -5
- tests/test_env.py +2 -2
- tests/test_eval.py +24 -31
- tests/test_fieldtest.py +19 -57
- tests/test_layers.py +0 -17
- tests/test_report.py +20 -10
- tests/test_subject.py +35 -25
- tests/test_validate.py +17 -6
|
@@ -14,7 +14,7 @@ A decision record that only lists the final answer hides the most useful informa
|
|
| 14 |
question
|
| 15 |
│
|
| 16 |
▼
|
| 17 |
-
┌──────────┐ embed on CPU, cosine over the
|
| 18 |
│ retrieve │ + pinned rules that bypass retrieval
|
| 19 |
└────┬─────┘
|
| 20 |
▼
|
|
@@ -2120,6 +2120,78 @@ nothing, which is the right place for a finding whose harm is real and latent.
|
|
| 2120 |
| Machine-checkable | 41 (93%) | 40 (93%) |
|
| 2121 |
| JavaScript findings, classified | 21 true / 24 false | **21 true / 22 false** |
|
| 2122 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2123 |
## 4. Performance traps, all found by measurement
|
| 2124 |
|
| 2125 |
Within four instrumented runs the report showed the 7B at ~10 tok/s against a
|
|
@@ -2191,22 +2263,25 @@ in a corpus; it does not close the reasoning gap.
|
|
| 2191 |
|
| 2192 |
### Checking is partial, and the layers say where
|
| 2193 |
|
| 2194 |
-
-
|
| 2195 |
-
false pass, and `nexa coverage` prints the reason
|
| 2196 |
-
-
|
| 2197 |
catches textual signatures, not meaning. Code wrong in a way no pattern describes
|
| 2198 |
-
passes silently. The other
|
| 2199 |
-
|
| 2200 |
-
|
| 2201 |
-
|
|
|
|
|
|
|
|
|
|
| 2202 |
- The empirical layer resolves imports against this interpreter and assembles
|
| 2203 |
bytecode. It does not execute anything unless `NEXA_EMPIRICAL_EXEC=1`, and even then
|
| 2204 |
only for snippets importing nothing outside a short allowlist — which in practice
|
| 2205 |
means no Odoo answer is ever executed, because they all import `odoo`. A subprocess
|
| 2206 |
with a timeout reduces blast radius; it is **not a security boundary**.
|
| 2207 |
- The context layer is only as good as the file it was given. `nexa ask` has no file,
|
| 2208 |
-
so it reports UNKNOWN; `nexa check <file>` reads the manifest, the model names
|
| 2209 |
-
the
|
| 2210 |
- Every Odoo rule declares `odoo_versions: >=16.0`, so the context layer has a
|
| 2211 |
question it can answer about a module on any series and reports per-rule when one
|
| 2212 |
does not apply. `corpus/check_sources.py` supports that range from the
|
|
@@ -2338,13 +2413,13 @@ unawares.
|
|
| 2338 |
|
| 2339 |
### Corpus provenance
|
| 2340 |
|
| 2341 |
-
|
| 2342 |
deserve more scepticism. Most of the rules were written by an assistant rather than
|
| 2343 |
harvested from production incidents, and rules drawn from real incidents are worth
|
| 2344 |
more. Three now are: `commit-inside-a-loop` and `sql-needs-query-parameters` came
|
| 2345 |
from an external ruleset disagreeing with this one (3.34), and
|
| 2346 |
`depends-must-not-name-unread-fields` from a fix commit in OpenSPP's history (3.35).
|
| 2347 |
-
Three of
|
| 2348 |
|
| 2349 |
This is the deepest unresolved problem in the design and no layer addresses it: the
|
| 2350 |
validator and the thing being validated are the same class of system. Provenance
|
|
@@ -2353,21 +2428,17 @@ tracks sources, versions and checker identities. It cannot establish that a rule
|
|
| 2353 |
|
| 2354 |
### Scope
|
| 2355 |
|
| 2356 |
-
Odoo,
|
| 2357 |
-
|
| 2358 |
|
| 2359 |
-
|
| 2360 |
-
|
| 2361 |
-
hand classification of
|
| 2362 |
-
|
| 2363 |
-
|
| 2364 |
-
to say anything with. Until each has a second codebase, every one of those numbers
|
| 2365 |
-
is one sample.
|
| 2366 |
|
| 2367 |
-
|
| 2368 |
-
|
| 2369 |
-
holds does not. It is not a style guide and does not try to be. `nexa check` on a
|
| 2370 |
-
`.py` file now decides from the file itself which of the two Python corpora applies.
|
| 2371 |
|
| 2372 |
### Known imprecision, not yet fixed
|
| 2373 |
|
|
@@ -2473,7 +2544,7 @@ nexa/
|
|
| 2473 |
│ ├── build_corpus.py RULE SOURCE OF TRUTH + patterns + provenance
|
| 2474 |
│ ├── corpus_meta.json generated; hash, build time, checked_against
|
| 2475 |
│ ├── selftest_patterns.py negative test for both self-checks
|
| 2476 |
-
│ └── rules/
|
| 2477 |
├── bin/
|
| 2478 |
│ ├── corpus.py validate, index, ask, crosscheck, coverage
|
| 2479 |
│ ├── layers.py the nine layers, and the rule that combines them
|
|
@@ -2494,7 +2565,7 @@ nexa/
|
|
| 2494 |
│ ├── local-ai.ps1 PowerShell helpers
|
| 2495 |
│ └── bench.ps1 raw throughput benchmark
|
| 2496 |
├── modelfiles/
|
| 2497 |
-
│ ├── gen-
|
| 2498 |
│ └── arch-advisor.Modelfile hand-written 30B persona
|
| 2499 |
└── subjects/ the fixed file set `nexa eval --check` reviews
|
| 2500 |
```
|
|
|
|
| 14 |
question
|
| 15 |
│
|
| 16 |
▼
|
| 17 |
+
┌──────────┐ embed on CPU, cosine over the corpus
|
| 18 |
│ retrieve │ + pinned rules that bypass retrieval
|
| 19 |
└────┬─────┘
|
| 20 |
▼
|
|
|
|
| 2120 |
| Machine-checkable | 41 (93%) | 40 (93%) |
|
| 2121 |
| JavaScript findings, classified | 21 true / 24 false | **21 true / 22 false** |
|
| 2122 |
|
| 2123 |
+
### 3.46 Narrowing to Odoo, and composing the Python rules into it
|
| 2124 |
+
|
| 2125 |
+
**Decided 2026-09-16.** The corpus had four stacks. It has one, and the eight
|
| 2126 |
+
plain-Python rules are inside it rather than beside it.
|
| 2127 |
+
|
| 2128 |
+
**Why narrow.** §3.44 fired the other twenty-five rules at real code for the first
|
| 2129 |
+
time, and the answer decided this. JavaScript classified out at **21 true against 22
|
| 2130 |
+
false**. TypeScript was measured at 3.8 per thousand lines and never classified, so
|
| 2131 |
+
its number said nothing. Against those, the Odoo half carries 87 true / 15 false on
|
| 2132 |
+
its two flagship rules across two ecosystems. The edge is Odoo ORM semantics;
|
| 2133 |
+
everywhere else this corpus competes with mature tools and loses.
|
| 2134 |
+
|
| 2135 |
+
Removing them rather than labelling them experimental is the part worth arguing. A
|
| 2136 |
+
rule that costs nothing at query time is not free: it is a claim the README has to
|
| 2137 |
+
carry, a number somebody will quote, and a file `nexa check` will accept and then say
|
| 2138 |
+
nothing useful about. A `.ts` file now raises `subject.Unreviewable` and is refused by
|
| 2139 |
+
name, because **an empty finding list reads as a clean bill of health**, and silence
|
| 2140 |
+
about a file nobody checked is the one lie this design exists to prevent.
|
| 2141 |
+
|
| 2142 |
+
**Why the Python rules were composed in instead.** Rules were selected by strict
|
| 2143 |
+
equality on `stack` (`corpus.py`), so an Odoo module got the 19 ORM rules and none of
|
| 2144 |
+
the 8 Python ones. Odoo modules are Python. `fieldtest --stack python` over OpenSPP
|
| 2145 |
+
found **32 hits `--stack odoo` never reported**.
|
| 2146 |
+
|
| 2147 |
+
"ruff covers it" was the first answer and it does not hold. OpenSPP's own `.ruff.toml`
|
| 2148 |
+
selects `B, C90, E501, I, UP` plus defaults — `S` and `DTZ` are off — so six of the
|
| 2149 |
+
eight are genuinely unreported in that deployment, and a narrow selection is what most
|
| 2150 |
+
Odoo shops run.
|
| 2151 |
+
|
| 2152 |
+
**What it cost, measured before shipping.** The composition was held to the bar in
|
| 2153 |
+
§3.43: 50 new findings across OpenSPP and OCA, every one read in its own file.
|
| 2154 |
+
**26 true, 24 false**, in `classification-python-2026-09.json`.
|
| 2155 |
+
|
| 2156 |
+
| | | |
|
| 2157 |
+
|---|---|---|
|
| 2158 |
+
| OpenSPP live findings | 162 | **194** (0.71 → 0.9 per kloc) |
|
| 2159 |
+
| OCA live findings | 102 | **120** (3.0 → 3.5 per kloc) |
|
| 2160 |
+
| Corpus precision, composed-in half | — | 26 true / 24 false |
|
| 2161 |
+
|
| 2162 |
+
Twenty of the twenty-six are one rule, `naive-and-aware-datetimes-do-not-compare`, and
|
| 2163 |
+
one fact: an Odoo `Datetime` is **naive UTC by contract**, so a naive local datetime
|
| 2164 |
+
reaching one is off by the server's offset. Latent on a UTC-clocked server, wrong on
|
| 2165 |
+
any other, and OpenSPP's history carries two fixes of exactly it.
|
| 2166 |
+
|
| 2167 |
+
Three rules fired and were never right, and the most interesting is
|
| 2168 |
+
`decimal-for-money-not-float`: Odoo's `Monetary` field **is** a float, and the
|
| 2169 |
+
framework's answer to precision is `float_round`/`float_compare`, not `Decimal`. The
|
| 2170 |
+
rule fires on the idiom and names a fix that cannot be taken. Three findings do not
|
| 2171 |
+
settle a checker either way, so it is recorded rather than removed.
|
| 2172 |
+
|
| 2173 |
+
**Two defects the classification found that no test would have.** `scan_source` strips
|
| 2174 |
+
`#` comments before matching and not string literals, so `eval()` named in a docstring
|
| 2175 |
+
reads as code. And suppression reads any marker within a simple statement without
|
| 2176 |
+
asking which rule it names, so a `# nosemgrep: odoo-sudo-without-context` silenced a
|
| 2177 |
+
datetime finding four lines above it — the mute button of §3.42 reached from the other
|
| 2178 |
+
side. Both are named in the README rather than quietly fixed.
|
| 2179 |
+
|
| 2180 |
+
**What the id prefix means now.** `python.decimal-for-money-not-float` carries
|
| 2181 |
+
`stack: odoo`. The prefix is **provenance** — where the rule came from and which
|
| 2182 |
+
documentation `check_sources` verifies it against — and the stack is **selection**. The
|
| 2183 |
+
validator's old invariant was "id prefix equals stack"; it is now "id prefix is one of
|
| 2184 |
+
`corpus.ORIGINS`". Keeping the ids is what keeps every committed classification,
|
| 2185 |
+
`LIKELIHOOD` grade and evidence bundle pointing at the same rule: renaming the subject
|
| 2186 |
+
of a measurement is the quiet way a number stops meaning what it says.
|
| 2187 |
+
|
| 2188 |
+
**One rule had to be reworded.** Retrieval is scoped to a stack, so merging them put
|
| 2189 |
+
27 rules in competition where 19 had been. `prefer-sql-constraint` ranked first for
|
| 2190 |
+
*"stop anyone entering a negative quantity on a sale order line"* against 19 and
|
| 2191 |
+
stopped being retrieved at all against 27 — that probe is the generic validation
|
| 2192 |
+
question every constraint rule here answers. Reworded to the one only it answers.
|
| 2193 |
+
**27 of 27 retrievable, 25 in the top 3.**
|
| 2194 |
+
|
| 2195 |
## 4. Performance traps, all found by measurement
|
| 2196 |
|
| 2197 |
Within four instrumented runs the report showed the 7B at ~10 tok/s against a
|
|
|
|
| 2263 |
|
| 2264 |
### Checking is partial, and the layers say where
|
| 2265 |
|
| 2266 |
+
- 1 of 27 rules has no automated check at all. It returns **no verdict**, never a
|
| 2267 |
+
false pass, and `nexa coverage` prints the reason.
|
| 2268 |
+
- 16 of the 26 checked rules are decided by a **regex over generated code**. That
|
| 2269 |
catches textual signatures, not meaning. Code wrong in a way no pattern describes
|
| 2270 |
+
passes silently. The other 10 are decided by an AST pass (§3.12). The regexes also
|
| 2271 |
+
match inside **docstrings**, because `scan_source` strips `#` comments and not
|
| 2272 |
+
string literals — two of the fifty findings in §3.46 were that.
|
| 2273 |
+
- Semantic analysis is **Python only**, which is the whole subject now. An answer may
|
| 2274 |
+
still contain a JavaScript block, Odoo's web client being OWL; that block gets
|
| 2275 |
+
delimiter balance and nothing deeper, and the layer reports UNKNOWN rather than
|
| 2276 |
+
PASS for it.
|
| 2277 |
- The empirical layer resolves imports against this interpreter and assembles
|
| 2278 |
bytecode. It does not execute anything unless `NEXA_EMPIRICAL_EXEC=1`, and even then
|
| 2279 |
only for snippets importing nothing outside a short allowlist — which in practice
|
| 2280 |
means no Odoo answer is ever executed, because they all import `odoo`. A subprocess
|
| 2281 |
with a timeout reduces blast radius; it is **not a security boundary**.
|
| 2282 |
- The context layer is only as good as the file it was given. `nexa ask` has no file,
|
| 2283 |
+
so it reports UNKNOWN; `nexa check <file>` reads the manifest, the model names,
|
| 2284 |
+
`ir.model.access.csv` and the record rules in XML, and can then say something real.
|
| 2285 |
- Every Odoo rule declares `odoo_versions: >=16.0`, so the context layer has a
|
| 2286 |
question it can answer about a module on any series and reports per-rule when one
|
| 2287 |
does not apply. `corpus/check_sources.py` supports that range from the
|
|
|
|
| 2413 |
|
| 2414 |
### Corpus provenance
|
| 2415 |
|
| 2416 |
+
25 of 27 rules cite upstream documentation. The rest rest on general knowledge and
|
| 2417 |
deserve more scepticism. Most of the rules were written by an assistant rather than
|
| 2418 |
harvested from production incidents, and rules drawn from real incidents are worth
|
| 2419 |
more. Three now are: `commit-inside-a-loop` and `sql-needs-query-parameters` came
|
| 2420 |
from an external ruleset disagreeing with this one (3.34), and
|
| 2421 |
`depends-must-not-name-unread-fields` from a fix commit in OpenSPP's history (3.35).
|
| 2422 |
+
Three of twenty-seven is not a change of method yet.
|
| 2423 |
|
| 2424 |
This is the deepest unresolved problem in the design and no layer addresses it: the
|
| 2425 |
validator and the thing being validated are the same class of system. Provenance
|
|
|
|
| 2428 |
|
| 2429 |
### Scope
|
| 2430 |
|
| 2431 |
+
Odoo, and nothing else (§3.46). Odoo rules were checked against 18.0 documentation and
|
| 2432 |
+
its source; behaviour differs across major versions.
|
| 2433 |
|
| 2434 |
+
One stack, 27 rules: 19 about the ORM and 8 about the Python language, which apply
|
| 2435 |
+
because an Odoo module is a Python module. Both halves carry field evidence from two
|
| 2436 |
+
ecosystems and a hand classification of every finding, and they are **not the same
|
| 2437 |
+
quality** — 85% on the two flagship ORM rules, 52% on the composed-in Python half.
|
| 2438 |
+
Reporting one blended number would describe neither.
|
|
|
|
|
|
|
| 2439 |
|
| 2440 |
+
A `.py` or `.xml` file is reviewed. Anything else is refused by name rather than
|
| 2441 |
+
reviewed against rules that cannot match it.
|
|
|
|
|
|
|
| 2442 |
|
| 2443 |
### Known imprecision, not yet fixed
|
| 2444 |
|
|
|
|
| 2544 |
│ ├── build_corpus.py RULE SOURCE OF TRUTH + patterns + provenance
|
| 2545 |
│ ├── corpus_meta.json generated; hash, build time, checked_against
|
| 2546 |
│ ├── selftest_patterns.py negative test for both self-checks
|
| 2547 |
+
│ └── rules/odoo.json generated; the whole corpus, do not hand-edit
|
| 2548 |
├── bin/
|
| 2549 |
│ ├── corpus.py validate, index, ask, crosscheck, coverage
|
| 2550 |
│ ├── layers.py the nine layers, and the rule that combines them
|
|
|
|
| 2565 |
│ ├── local-ai.ps1 PowerShell helpers
|
| 2566 |
│ └── bench.ps1 raw throughput benchmark
|
| 2567 |
├── modelfiles/
|
| 2568 |
+
│ ├── gen-odoo.Modelfile generated; do not hand-edit
|
| 2569 |
│ └── arch-advisor.Modelfile hand-written 30B persona
|
| 2570 |
└── subjects/ the fixed file set `nexa eval --check` reviews
|
| 2571 |
```
|
|
@@ -135,22 +135,14 @@ This generates the rule JSON, validates it against its schema, self-tests every
|
|
| 135 |
violation pattern, measures pattern false positives, embeds the retrieval index, and
|
| 136 |
writes the Modelfiles.
|
| 137 |
|
| 138 |
-
It finishes by printing
|
| 139 |
-
|
| 140 |
-
|
| 141 |
|
| 142 |
```bash
|
| 143 |
ollama create ai-odoo -f modelfiles/gen-odoo.Modelfile
|
| 144 |
```
|
| 145 |
|
| 146 |
-
```bash
|
| 147 |
-
ollama create ai-ts -f modelfiles/gen-typescript.Modelfile
|
| 148 |
-
```
|
| 149 |
-
|
| 150 |
-
```bash
|
| 151 |
-
ollama create ai-js -f modelfiles/gen-javascript.Modelfile
|
| 152 |
-
```
|
| 153 |
-
|
| 154 |
Optional, for the 30B persona used by `nexa ask --deep`:
|
| 155 |
|
| 156 |
```bash
|
|
@@ -284,7 +276,7 @@ rule and the problem. Fix it in `corpus/build_corpus.py` and rebuild.
|
|
| 284 |
## Uninstall
|
| 285 |
|
| 286 |
```bash
|
| 287 |
-
ollama rm ai-odoo
|
| 288 |
```
|
| 289 |
|
| 290 |
Then delete the cloned directory. Nexa writes nothing outside its own folder except the
|
|
|
|
| 135 |
violation pattern, measures pattern false positives, embeds the retrieval index, and
|
| 136 |
writes the Modelfiles.
|
| 137 |
|
| 138 |
+
It finishes by printing the `ollama create` command. Run it to register the persona —
|
| 139 |
+
about two seconds, because only metadata is written. The weights are shared with
|
| 140 |
+
`qwen2.5-coder:7b`, so it costs no extra disk.
|
| 141 |
|
| 142 |
```bash
|
| 143 |
ollama create ai-odoo -f modelfiles/gen-odoo.Modelfile
|
| 144 |
```
|
| 145 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
Optional, for the 30B persona used by `nexa ask --deep`:
|
| 147 |
|
| 148 |
```bash
|
|
|
|
| 276 |
## Uninstall
|
| 277 |
|
| 278 |
```bash
|
| 279 |
+
ollama rm ai-odoo arch-advisor qwen2.5-coder:7b qwen3-coder:30b nomic-embed-text
|
| 280 |
```
|
| 281 |
|
| 282 |
Then delete the cloned directory. Nexa writes nothing outside its own folder except the
|
|
@@ -1,8 +1,7 @@
|
|
| 1 |
```
|
| 2 |
██ ██ ██████ ██ ██ █████
|
| 3 |
███ ██ ██ ██ ██ ██ ██
|
| 4 |
-
███
|
| 5 |
-
██ ████ █████ ███ ███████
|
| 6 |
██ ███ ██ ██ ██ ██ ██
|
| 7 |
██ ██ ██████ ██ ██ ██ ██
|
| 8 |
▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░
|
|
@@ -128,17 +127,125 @@ publishing a classification.
|
|
| 128 |
|
| 129 |
## What it is not
|
| 130 |
|
| 131 |
-
**Not a general linter.**
|
| 132 |
wrapper above it does not have to.
|
| 133 |
|
| 134 |
-
**Not a bug finder.** Against real Odoo bugfixes
|
| 135 |
-
bugs are domain logic ("finished plans should not be the current plan") and no rule
|
| 136 |
corpus finds those. Measured and published here because a precision number without a
|
| 137 |
recall number beside it is the more flattering half of the picture.
|
| 138 |
|
| 139 |
-
**
|
| 140 |
-
evidence in September 2026 and it was not
|
| 141 |
-
against 22 false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
|
| 143 |
---
|
| 144 |
|
|
@@ -208,10 +315,6 @@ nexa help <command> detail for one command
|
|
| 208 |
nexa ask odoo "constrain invoice date to the fiscal year of its period"
|
| 209 |
```
|
| 210 |
|
| 211 |
-
```bash
|
| 212 |
-
nexa ask typescript "validate an Odoo res.partner JSON-RPC payload"
|
| 213 |
-
```
|
| 214 |
-
|
| 215 |
```bash
|
| 216 |
nexa ask odoo --deep "should this be a stored computed field or a related field?"
|
| 217 |
```
|
|
@@ -286,12 +389,25 @@ the same command.
|
|
| 286 |
It prints **no verdict**, because nothing generated an answer to have a verdict
|
| 287 |
about. A clean run says *no pattern fired* in those words, and exits 1 if one did.
|
| 288 |
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 292 |
|
| 293 |
```
|
| 294 |
-
checking upload_handler.py as
|
|
|
|
|
|
|
|
|
|
| 295 |
|
| 296 |
Rules retrieved:
|
| 297 |
0.698 [critical] python.assert-is-not-validation
|
|
@@ -300,10 +416,10 @@ Rules retrieved:
|
|
| 300 |
0.643 [critical] python.subprocess-shell-true-is-injection
|
| 301 |
```
|
| 302 |
|
| 303 |
-
|
| 304 |
`__manifest__.py` gives the Odoo series, the file gives its model names and existing
|
| 305 |
-
imports,
|
| 306 |
-
the question *and* into the context layer.
|
| 307 |
|
| 308 |
Where they say the corpus was verified somewhere else, that is reported as a **gap**,
|
| 309 |
not a finding — it is true of the corpus whatever the answer says, and a verdict that
|
|
@@ -418,9 +534,9 @@ field could not say so.
|
|
| 418 |
Everything else is **derived** and must never be hand-edited:
|
| 419 |
|
| 420 |
```
|
| 421 |
-
corpus/rules/
|
| 422 |
-
|
| 423 |
-
|
| 424 |
```
|
| 425 |
|
| 426 |
Same principle as `z.infer<typeof Schema>`: one definition, derived artifacts, no drift.
|
|
@@ -429,23 +545,21 @@ Same principle as `z.infer<typeof Schema>`: one definition, derived artifacts, n
|
|
| 429 |
|
| 430 |
| | |
|
| 431 |
|---|---|
|
| 432 |
-
| Rules |
|
| 433 |
-
| Critical |
|
| 434 |
-
| Machine-checkable |
|
| 435 |
-
| Traceable to upstream docs |
|
| 436 |
| Pattern false positives | 0 cross-hits across the corpus |
|
| 437 |
-
| Recall under meaning-preserving rewrite |
|
| 438 |
-
| Satisfied patterns bypassable by a comment | 0 of
|
| 439 |
-
| Retrievable by their own probe |
|
| 440 |
-
| Hit rate on 33,960 lines of reviewed OCA code | 3.
|
| 441 |
-
| Those
|
| 442 |
-
| Hit rate on 226,937 lines of OpenSPP (Odoo 19) | 0.
|
| 443 |
-
| Every one of those findings, classified by hand | **
|
| 444 |
| …the two `depends`/`constrains` rules alone | **87 true**, 15 false, 3 undecidable — 85% |
|
|
|
|
| 445 |
| …`sudo-bypasses-record-rules` alone, before it was narrowed | 6 true, 97 false — 5.8%, and the reason it is now an AST pass |
|
| 446 |
-
| Hit rate on 73,762 lines of TypeScript (hono, trpc) | 3.8 per 1000 lines — **measured, not yet classified** |
|
| 447 |
-
| Hit rate on 9,304 lines of Odoo frontend JavaScript | 4.6 per 1000 lines — 21 true positives, 22 false |
|
| 448 |
-
| Hit rate on 10,225 lines of Python (llm-router) | 0.3 per 1000 lines |
|
| 449 |
| **Recall against real Odoo bugfixes** | **1 of 56 (OCA), 7 of 162 (OpenSPP). See below.** |
|
| 450 |
| Verdict stability, 12 questions x 5 runs | 12/12 same verdict every run |
|
| 451 |
| Escalation rescued a bad draft | 11 of 21, 95% CI 32–72% |
|
|
@@ -455,9 +569,15 @@ Same principle as `z.infer<typeof Schema>`: one definition, derived artifacts, n
|
|
| 455 |
| `nexa check` verdict stability | 4 of 5 files on one run, 3 of 5 on the next |
|
| 456 |
| `nexa check --no-model` | 0.26s, identical output every run |
|
| 457 |
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
— and `nexa coverage` names
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 461 |
|
| 462 |
---
|
| 463 |
|
|
@@ -549,6 +669,12 @@ false positive left, a `sql-needs-query-parameters` carrying a marker on the lin
|
|
| 549 |
it that suppression now reads. **All 79 true positives survived both changes**, which is
|
| 550 |
again the only thing that makes the rest of it safe.
|
| 551 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 552 |
### The same treatment on a second ecosystem
|
| 553 |
|
| 554 |
A precision number from one ecosystem is a precision number for that ecosystem. The two
|
|
@@ -614,6 +740,11 @@ model nobody declared is UNKNOWN, not "no rule".
|
|
| 614 |
|
| 615 |
The last row is the only one that makes the others safe.
|
| 616 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 617 |
### What is still wrong, named
|
| 618 |
|
| 619 |
`float-comparison-needs-float-utils` is next at **2 true, 5 false**. All five compare an
|
|
@@ -625,6 +756,12 @@ means tracing the assignment, so it is written down here rather than quietly lef
|
|
| 625 |
onchange that sets a field rather than enforcing anything — which the rule's existing
|
| 626 |
narrowing was supposed to exclude and does not catch when the write is to a Many2one.
|
| 627 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 628 |
What the first run found:
|
| 629 |
|
| 630 |
- **`onchange-is-not-validation`** fired on all 16 `@api.onchange` declarations that
|
|
@@ -829,9 +966,11 @@ python corpus/check_sources.py --source # what the source defines
|
|
| 829 |
```
|
| 830 |
|
| 831 |
Each Odoo rule declares the series it holds for. The first mode checks that claim
|
| 832 |
-
against the documentation:
|
| 833 |
-
names the API, 2 more are tutorials that moved while the API stayed, and **1** is
|
| 834 |
-
supported by any 16.0 page.
|
|
|
|
|
|
|
| 835 |
|
| 836 |
Documentation has a blind spot, and the corpus had fallen into it — a function Odoo
|
| 837 |
shipped and never described reads as missing. So the second mode reads odoo/odoo
|
|
@@ -840,7 +979,7 @@ float_compare(` in `odoo/tools/float_utils.py`, not the word anywhere on a page.
|
|
| 840 |
|
| 841 |
| | |
|
| 842 |
|---|---|
|
| 843 |
-
| Rule-series defined in source across 16.0, 17.0 and 18.0 | **
|
| 844 |
|
| 845 |
That closes it. No version claim in this corpus rests on an assertion, and the one
|
| 846 |
documentation gap now carries a measurement instead of a belief: `float_compare`'s
|
|
@@ -863,16 +1002,20 @@ nexa eval --retrieval
|
|
| 863 |
asks each rule its own probe and reports whether it comes back. No generation, so it
|
| 864 |
needs only the embedding model and finishes in seconds.
|
| 865 |
|
| 866 |
-
All
|
| 867 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 868 |
somebody who already knows the answer. `no-monkey-patching` did not answer *"change
|
| 869 |
-
what a core sale.order method does"*
|
| 870 |
-
*"what type should a webhook payload have"*. Reworded, measured again, both found.
|
| 871 |
|
| 872 |
The report states its own limits. Pinned rules are prepended regardless of score, so
|
| 873 |
-
ranking against them measures the pin
|
| 874 |
-
|
| 875 |
-
letting it read as a result.
|
| 876 |
|
| 877 |
### And what does `nexa check` actually report?
|
| 878 |
|
|
|
|
| 1 |
```
|
| 2 |
██ ██ ██████ ██ ██ █████
|
| 3 |
███ ██ ██ ██ ██ ██ ██
|
| 4 |
+
██ █ ██ █████ ███ ███████
|
|
|
|
| 5 |
██ ███ ██ ██ ██ ██ ██
|
| 6 |
██ ██ ██████ ██ ██ ██ ██
|
| 7 |
▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░
|
|
|
|
| 127 |
|
| 128 |
## What it is not
|
| 129 |
|
| 130 |
+
**Not a general linter.** 27 rules. It will not replace `pylint-odoo`, and after the
|
| 131 |
wrapper above it does not have to.
|
| 132 |
|
| 133 |
+
**Not a bug finder.** Against real Odoo bugfixes the ORM rules catch 7 of 162 — about
|
| 134 |
+
4%. Most bugs are domain logic ("finished plans should not be the current plan") and no rule
|
| 135 |
corpus finds those. Measured and published here because a precision number without a
|
| 136 |
recall number beside it is the more flattering half of the picture.
|
| 137 |
|
| 138 |
+
**Odoo only, and that is the whole of it.** There was a TypeScript corpus and a
|
| 139 |
+
JavaScript one. They got their first field evidence in September 2026 and it was not
|
| 140 |
+
good — JavaScript classified out at 21 true against 22 false, TypeScript was never
|
| 141 |
+
classified at all — so both were **removed** rather than left in with a warning on
|
| 142 |
+
them. A `.ts` or `.js` file is now refused by name instead of reviewed against rules
|
| 143 |
+
that would find nothing in it, because nothing found reads as nothing to find.
|
| 144 |
+
|
| 145 |
+
**The plain-Python rules did not go the same way, and the reason is in
|
| 146 |
+
[the composition](#one-corpus-and-why-the-python-rules-are-in-it).**
|
| 147 |
+
|
| 148 |
+
---
|
| 149 |
+
|
| 150 |
+
## One corpus, and why the Python rules are in it
|
| 151 |
+
|
| 152 |
+
There is one stack. Every rule in it is a rule for reviewing Odoo code: 19 about the
|
| 153 |
+
ORM, and 8 about the Python language, which apply because **an Odoo module is a Python
|
| 154 |
+
module**. The `python.` prefix on those eight records where the rule comes from and
|
| 155 |
+
which documentation it is checked against. It is not a second mode you can select.
|
| 156 |
+
There is no second mode.
|
| 157 |
+
|
| 158 |
+
They were unreachable until September 2026, and it was one line: selection was strict
|
| 159 |
+
equality on the stack field, so an Odoo file got the 19 ORM rules and none of the
|
| 160 |
+
other 8. `fieldtest --stack python` over OpenSPP found **32 hits that `--stack odoo`
|
| 161 |
+
never reported**.
|
| 162 |
+
|
| 163 |
+
The first instinct was the opposite one — drop the Python corpus, because ruff covers
|
| 164 |
+
it. Checked against OpenSPP's actual `.ruff.toml`, that is wrong. They select `B`,
|
| 165 |
+
`C90`, `E501`, `I`, `UP` and ruff's defaults. `S` and `DTZ` are not selected:
|
| 166 |
+
|
| 167 |
+
| | ruff rule | selected there? |
|
| 168 |
+
|---|---|---|
|
| 169 |
+
| `assert-is-not-validation` | S101 | no |
|
| 170 |
+
| `eval-and-pickle-execute-their-input` | S301/S307 | no |
|
| 171 |
+
| `subprocess-shell-true-is-injection` | S602 | no |
|
| 172 |
+
| `naive-and-aware-datetimes-do-not-compare` | DTZ | no |
|
| 173 |
+
| `path-join-does-not-contain-a-path` | — | no such rule |
|
| 174 |
+
| `decimal-for-money-not-float` | — | no such rule |
|
| 175 |
+
| `mutable-default-argument` | B006 | **yes** |
|
| 176 |
+
| `bare-except-catches-the-exit` | E722 | **yes** (default) |
|
| 177 |
+
|
| 178 |
+
A narrow ruff selection is what most Odoo shops run, so six of the eight are genuinely
|
| 179 |
+
unreported in that deployment. Against ruff this is not a competing tool; inside an
|
| 180 |
+
Odoo review it is part of one.
|
| 181 |
+
|
| 182 |
+
### What the composition actually found, all 50 read by hand
|
| 183 |
+
|
| 184 |
+
Composing them moves every Odoo number, so the standard the rest of the corpus is held
|
| 185 |
+
to applies here too: 50 findings across OpenSPP and OCA, each read in its own file.
|
| 186 |
+
Every verdict is in `classification-python-2026-09.json`.
|
| 187 |
+
|
| 188 |
+
| | true | false | |
|
| 189 |
+
|---|---|---|---|
|
| 190 |
+
| `naive-and-aware-datetimes-do-not-compare` | 20 | 12 | 62% |
|
| 191 |
+
| `assert-is-not-validation` | 6 | 4 | 60% |
|
| 192 |
+
| `decimal-for-money-not-float` | 0 | 3 | |
|
| 193 |
+
| `path-join-does-not-contain-a-path` | 0 | 4 | |
|
| 194 |
+
| `eval-and-pickle-execute-their-input` | 0 | 1 | |
|
| 195 |
+
| `mutable-default-argument` | — | — | never fired |
|
| 196 |
+
| `bare-except-catches-the-exit` | — | — | never fired |
|
| 197 |
+
| `subprocess-shell-true-is-injection` | — | — | never fired |
|
| 198 |
+
| | **26** | **24** | **52%** |
|
| 199 |
+
|
| 200 |
+
**One rule carries it.** Twenty of the twenty-six true positives are the datetime rule,
|
| 201 |
+
and they are one shape: an Odoo `Datetime` field is **naive UTC by contract**, and a
|
| 202 |
+
naive *local* datetime reaches one. `membership.ended_date > datetime.now()` filters by
|
| 203 |
+
the server's offset. An autovacuum cron builds its deadline from local now and compares
|
| 204 |
+
it against `create_date`, deleting audit logs early or late. A linked-data proof appends
|
| 205 |
+
a literal `Z` — which asserts UTC — to a local time. A fiscal-year cron compares an ORM
|
| 206 |
+
`Date` against `datetime.now().date()`, so on any non-UTC server the year is created on
|
| 207 |
+
the wrong side of midnight.
|
| 208 |
+
|
| 209 |
+
Say what that makes them: on a server clocked to UTC every one is latent. On any other
|
| 210 |
+
it is wrong by the offset. OpenSPP's own history carries two fixes of exactly this class
|
| 211 |
+
— a newborn birthdate evaluated against the server's UTC date, and `Date.today()` storing
|
| 212 |
+
midnight UTC on a `Datetime` field — which is why it is graded a defect rather than a
|
| 213 |
+
theoretical one.
|
| 214 |
+
|
| 215 |
+
The twelve false ones are near enough one shape too: a timestamp that is formatted and
|
| 216 |
+
never compared — a `Generated:` line in a report, a backup filename, a `%Y-%m` period
|
| 217 |
+
label. Telling those from the twenty needs to see what happens to the value afterwards,
|
| 218 |
+
which a regex cannot.
|
| 219 |
+
|
| 220 |
+
### What is wrong with the other half, named
|
| 221 |
+
|
| 222 |
+
**Three rules fired and were never right.** `decimal-for-money-not-float` is 0 of 3, and
|
| 223 |
+
this one has a *reason* behind it rather than a sample size: Odoo's `Monetary` field **is
|
| 224 |
+
a float** — the ORM defines it that way — and the framework's answer to the precision
|
| 225 |
+
problem is `float_round` and `float_compare` against the currency's rounding, not
|
| 226 |
+
`Decimal`. Inside an Odoo module the rule fires on the idiom and names a fix that cannot
|
| 227 |
+
be taken. That is an argument and not a measurement, and three findings settle nothing,
|
| 228 |
+
so it is written down here rather than acted on.
|
| 229 |
+
|
| 230 |
+
`path-join-does-not-contain-a-path` is 0 of 4 — all four join a configured directory to
|
| 231 |
+
a name this module generated, and the pattern matches the *word* `filename` rather than
|
| 232 |
+
anything about where the value came from. `eval-and-pickle-execute-their-input` is 0 of
|
| 233 |
+
1. Four findings between them is not enough to conclude a checker is wrong.
|
| 234 |
+
|
| 235 |
+
**Three more never fired at all**, and silence is not a pass: it says 260,000 lines
|
| 236 |
+
contain none of what they look for, which is also what a broken checker says. Two of the
|
| 237 |
+
three are the two ruff already covers, which is the more likely explanation.
|
| 238 |
+
|
| 239 |
+
**A checker defect, found by classifying rather than by testing.** Two of the 50 are the
|
| 240 |
+
same fault: `scan_source` strips `#` comments before matching and does **not** strip
|
| 241 |
+
string literals, so `eval()` named in a docstring arguing *against* using it, and a
|
| 242 |
+
`datetime.now()` inside a doctest example, both read as code.
|
| 243 |
+
|
| 244 |
+
**A suppression that should not have fired.** One finding — a consent-receipt timestamp
|
| 245 |
+
— sits inside a `return` statement that carries a `# nosemgrep: odoo-sudo-without-context`
|
| 246 |
+
marker four lines below it. Suppression reads any marker within the statement without
|
| 247 |
+
asking which rule it names. That is the mute button this project says it will not build,
|
| 248 |
+
reached from the other direction, and it is counted apart here rather than dropped.
|
| 249 |
|
| 250 |
---
|
| 251 |
|
|
|
|
| 315 |
nexa ask odoo "constrain invoice date to the fiscal year of its period"
|
| 316 |
```
|
| 317 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 318 |
```bash
|
| 319 |
nexa ask odoo --deep "should this be a stored computed field or a related field?"
|
| 320 |
```
|
|
|
|
| 389 |
It prints **no verdict**, because nothing generated an answer to have a verdict
|
| 390 |
about. A clean run says *no pattern fired* in those words, and exits 1 if one did.
|
| 391 |
|
| 392 |
+
**A `.py` or `.xml` file is reviewed against all 27 rules. Anything else is refused
|
| 393 |
+
by name.**
|
| 394 |
+
|
| 395 |
+
```
|
| 396 |
+
$ nexa check web/src/partners.ts
|
| 397 |
+
! nexa reviews Odoo code: .py and .xml files. .ts is not one, and there
|
| 398 |
+
are no rules in this corpus that would say anything about it.
|
| 399 |
+
```
|
| 400 |
+
|
| 401 |
+
There is no second corpus to route to, so a plain Python script inside an Odoo tree —
|
| 402 |
+
`scripts/`, a migration helper — is reviewed like anything else. The nineteen ORM
|
| 403 |
+
rules have nothing to match in it, the eight plain-Python ones do, and the report says
|
| 404 |
+
which of the two it is rather than leaving the reader to infer it from a short list:
|
| 405 |
|
| 406 |
```
|
| 407 |
+
checking upload_handler.py as odoo
|
| 408 |
+
! no Odoo imports here and no __manifest__.py above it -- the 19 ORM rules
|
| 409 |
+
have nothing to match, so this file is effectively reviewed against the
|
| 410 |
+
8 plain-Python ones
|
| 411 |
|
| 412 |
Rules retrieved:
|
| 413 |
0.698 [critical] python.assert-is-not-validation
|
|
|
|
| 416 |
0.643 [critical] python.subprocess-shell-true-is-injection
|
| 417 |
```
|
| 418 |
|
| 419 |
+
Unlike `nexa ask`, this one has **surroundings to read**: the nearest
|
| 420 |
`__manifest__.py` gives the Odoo series, the file gives its model names and existing
|
| 421 |
+
imports, `ir.model.access.csv` and the record rules in XML say what Python cannot.
|
| 422 |
+
Those facts go into the question *and* into the context layer.
|
| 423 |
|
| 424 |
Where they say the corpus was verified somewhere else, that is reported as a **gap**,
|
| 425 |
not a finding — it is true of the corpus whatever the answer says, and a verdict that
|
|
|
|
| 534 |
Everything else is **derived** and must never be hand-edited:
|
| 535 |
|
| 536 |
```
|
| 537 |
+
corpus/rules/odoo.json --+--> modelfiles/gen-odoo.Modelfile (system prompt)
|
| 538 |
+
+--> bin/index.sqlite (retrieval)
|
| 539 |
+
+--> every answer (grounding + checking)
|
| 540 |
```
|
| 541 |
|
| 542 |
Same principle as `z.infer<typeof Schema>`: one definition, derived artifacts, no drift.
|
|
|
|
| 545 |
|
| 546 |
| | |
|
| 547 |
|---|---|
|
| 548 |
+
| Rules | 27 — 19 Odoo ORM, 8 plain-Python, one stack |
|
| 549 |
+
| Critical | 12 |
|
| 550 |
+
| Machine-checkable | 26 (96%) — 16 by pattern, 10 by AST |
|
| 551 |
+
| Traceable to upstream docs | 25 (93%), each audited for topic fit |
|
| 552 |
| Pattern false positives | 0 cross-hits across the corpus |
|
| 553 |
+
| Recall under meaning-preserving rewrite | 79/79 |
|
| 554 |
+
| Satisfied patterns bypassable by a comment | 0 of 7 |
|
| 555 |
+
| Retrievable by their own probe | 27 of 27 at top 6, 25 of them at top 3 |
|
| 556 |
+
| Hit rate on 33,960 lines of reviewed OCA code | 3.5 per 1000 lines |
|
| 557 |
+
| Those 120 hits, classified by hand | **86 true positives**, 21 false, 13 undecidable from one file |
|
| 558 |
+
| Hit rate on 226,937 lines of OpenSPP (Odoo 19) | 0.9 per 1000 lines, +69 another tool had already flagged, +71 advisory |
|
| 559 |
+
| Every one of those findings, classified by hand | **157 true positives**, 137 false, 6 undecidable |
|
| 560 |
| …the two `depends`/`constrains` rules alone | **87 true**, 15 false, 3 undecidable — 85% |
|
| 561 |
+
| …the 8 composed-in Python rules alone | **26 true**, 24 false — 52%, and 20 of the 26 are one rule |
|
| 562 |
| …`sudo-bypasses-record-rules` alone, before it was narrowed | 6 true, 97 false — 5.8%, and the reason it is now an AST pass |
|
|
|
|
|
|
|
|
|
|
| 563 |
| **Recall against real Odoo bugfixes** | **1 of 56 (OCA), 7 of 162 (OpenSPP). See below.** |
|
| 564 |
| Verdict stability, 12 questions x 5 runs | 12/12 same verdict every run |
|
| 565 |
| Escalation rescued a bad draft | 11 of 21, 95% CI 32–72% |
|
|
|
|
| 569 |
| `nexa check` verdict stability | 4 of 5 files on one run, 3 of 5 on the next |
|
| 570 |
| `nexa check --no-model` | 0.26s, identical output every run |
|
| 571 |
|
| 572 |
+
26 of 27 rules carry an automated check. The one that does not —
|
| 573 |
+
`schema-change-needs-migration`, whose violation lives in the diff between two module
|
| 574 |
+
versions — returns **no verdict** rather than a false pass, and `nexa coverage` names
|
| 575 |
+
it and says why.
|
| 576 |
+
|
| 577 |
+
Two lines above are worth reading together. The Odoo half is 85% on its two flagship
|
| 578 |
+
rules; the composed-in Python half is 52%. They are in the same corpus and they are
|
| 579 |
+
not the same quality, and the table says so rather than reporting one blended number
|
| 580 |
+
that describes neither.
|
| 581 |
|
| 582 |
---
|
| 583 |
|
|
|
|
| 669 |
it that suppression now reads. **All 79 true positives survived both changes**, which is
|
| 670 |
again the only thing that makes the rest of it safe.
|
| 671 |
|
| 672 |
+
**Re-measured again on 2026-09-16, after the eight Python rules were composed in:
|
| 673 |
+
120 hits, 3.5 per 1000 lines, 86 true, 21 false, 13 undecidable.** The 18 new ones are
|
| 674 |
+
7 true and 11 false, read the same way and listed in
|
| 675 |
+
`classification-python-2026-09.json`. The rate went up and the precision went down, and
|
| 676 |
+
that is the trade the section above prices.
|
| 677 |
+
|
| 678 |
### The same treatment on a second ecosystem
|
| 679 |
|
| 680 |
A precision number from one ecosystem is a precision number for that ecosystem. The two
|
|
|
|
| 740 |
|
| 741 |
The last row is the only one that makes the others safe.
|
| 742 |
|
| 743 |
+
That 162 is the Odoo rules alone. Composing the eight Python rules in took it to
|
| 744 |
+
**194, 0.9 per 1000 lines** — the 32 new findings are 19 true and 13 false, and they
|
| 745 |
+
are the subject of
|
| 746 |
+
[the composition section](#what-the-composition-actually-found-all-50-read-by-hand).
|
| 747 |
+
|
| 748 |
### What is still wrong, named
|
| 749 |
|
| 750 |
`float-comparison-needs-float-utils` is next at **2 true, 5 false**. All five compare an
|
|
|
|
| 756 |
onchange that sets a field rather than enforcing anything — which the rule's existing
|
| 757 |
narrowing was supposed to exclude and does not catch when the write is to a Many2one.
|
| 758 |
|
| 759 |
+
The composed-in Python half has four more, each with a number on it:
|
| 760 |
+
`decimal-for-money-not-float` firing on Odoo's own `Monetary` idiom, `path-join`
|
| 761 |
+
matching a variable's name rather than its origin, patterns matching inside docstrings,
|
| 762 |
+
and a suppression marker silencing a rule it was not written about. All four are in
|
| 763 |
+
[the section above](#what-is-wrong-with-the-other-half-named).
|
| 764 |
+
|
| 765 |
What the first run found:
|
| 766 |
|
| 767 |
- **`onchange-is-not-validation`** fired on all 16 `@api.onchange` declarations that
|
|
|
|
| 966 |
```
|
| 967 |
|
| 968 |
Each Odoo rule declares the series it holds for. The first mode checks that claim
|
| 969 |
+
against the documentation: 45 of the 48 versioned rule-series cite a page that exists
|
| 970 |
+
and names the API, 2 more are tutorials that moved while the API stayed, and **1** is
|
| 971 |
+
not supported by any 16.0 page. The 9 remaining cite something other than the Odoo
|
| 972 |
+
docs — Postgres, the contributing guidelines — and are reported as unversioned rather
|
| 973 |
+
than counted as passing.
|
| 974 |
|
| 975 |
Documentation has a blind spot, and the corpus had fallen into it — a function Odoo
|
| 976 |
shipped and never described reads as missing. So the second mode reads odoo/odoo
|
|
|
|
| 979 |
|
| 980 |
| | |
|
| 981 |
|---|---|
|
| 982 |
+
| Rule-series defined in source across 16.0, 17.0 and 18.0 | **57 of 57** |
|
| 983 |
|
| 984 |
That closes it. No version claim in this corpus rests on an assertion, and the one
|
| 985 |
documentation gap now carries a measurement instead of a belief: `float_compare`'s
|
|
|
|
| 1002 |
asks each rule its own probe and reports whether it comes back. No generation, so it
|
| 1003 |
needs only the embedding model and finishes in seconds.
|
| 1004 |
|
| 1005 |
+
All 27 are retrievable at the operating `TOP_K` of 6, and 25 of them rank in the top
|
| 1006 |
+
3. That number got harder rather than easier when the corpus became one stack: with
|
| 1007 |
+
19 rules competing, `prefer-sql-constraint` came back first for *"stop anyone entering
|
| 1008 |
+
a negative quantity on a sale order line"*; with 27 it stopped coming back at all,
|
| 1009 |
+
because that is the generic validation question every constraint rule here answers.
|
| 1010 |
+
Reworded to the question only it answers — *"should this check be a database
|
| 1011 |
+
constraint or an api.constrains method"* — and measured again. It is the third rule to
|
| 1012 |
+
fail this way, and all three failed identically: correctly worded, in the vocabulary of
|
| 1013 |
somebody who already knows the answer. `no-monkey-patching` did not answer *"change
|
| 1014 |
+
what a core sale.order method does"* either.
|
|
|
|
| 1015 |
|
| 1016 |
The report states its own limits. Pinned rules are prepended regardless of score, so
|
| 1017 |
+
ranking against them measures the pin rather than the wording, and the header says so
|
| 1018 |
+
instead of counting them.
|
|
|
|
| 1019 |
|
| 1020 |
### And what does `nexa check` actually report?
|
| 1021 |
|
|
@@ -41,7 +41,10 @@ HERE = os.path.dirname(os.path.abspath(__file__))
|
|
| 41 |
ROOT = os.path.dirname(HERE)
|
| 42 |
RULES_DIR = os.path.join(ROOT, "corpus", "rules")
|
| 43 |
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
|
| 47 |
def load_rules():
|
|
|
|
| 41 |
ROOT = os.path.dirname(HERE)
|
| 42 |
RULES_DIR = os.path.join(ROOT, "corpus", "rules")
|
| 43 |
|
| 44 |
+
# Keyed by stack, of which there is one. A dict rather than a constant because
|
| 45 |
+
# the probes index it by the rule's stack and a KeyError is the right answer if
|
| 46 |
+
# a rule ever carries one this does not know about.
|
| 47 |
+
COMMENT = {"odoo": "#"}
|
| 48 |
|
| 49 |
|
| 50 |
def load_rules():
|
|
@@ -4,14 +4,15 @@
|
|
| 4 |
WHY THIS EXISTS
|
| 5 |
---------------
|
| 6 |
ARCHITECTURE records the context layer as absent by design, and names the reason
|
| 7 |
-
honestly:
|
| 8 |
-
|
| 9 |
-
|
| 10 |
|
| 11 |
That reasoning is correct about snippets and wrong about files. When the input is
|
| 12 |
a real path in a real tree, the context is sitting on disk: the module manifest
|
| 13 |
says which Odoo version this has to run on, the file says which model it extends,
|
| 14 |
-
|
|
|
|
| 15 |
|
| 16 |
So this module reads the surroundings, and the context layer reports UNKNOWN --
|
| 17 |
never PASS -- whenever it was handed a snippet with no surroundings to read. An
|
|
@@ -53,29 +54,31 @@ _MODEL_RE = re.compile(r"^\s*_(?:name|inherit)\s*=\s*[\"']([a-z0-9_.]+)[\"']", r
|
|
| 53 |
_ODOO_IMPORT_RE = re.compile(r"^\s*from\s+odoo[\w.]*\s+import\s+(.+)$", re.M)
|
| 54 |
|
| 55 |
|
| 56 |
-
#
|
| 57 |
-
#
|
| 58 |
-
#
|
| 59 |
-
#
|
| 60 |
-
#
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
".mjs": "javascript", ".cjs": "javascript",
|
| 66 |
-
}
|
| 67 |
|
| 68 |
|
| 69 |
def stack_for(path):
|
| 70 |
-
"""The rule stack a file belongs to
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
|
|
|
|
|
|
| 77 |
"""
|
| 78 |
-
|
|
|
|
|
|
|
| 79 |
|
| 80 |
|
| 81 |
def _walk_up(start, names):
|
|
@@ -129,8 +132,6 @@ def from_file(path, stack=None):
|
|
| 129 |
"manifest": None,
|
| 130 |
"models": [],
|
| 131 |
"odoo_imports": [],
|
| 132 |
-
"ts_strict": None,
|
| 133 |
-
"tsconfig": None,
|
| 134 |
"is_test": False,
|
| 135 |
# How much of the file the answer was about. Filled in by subject.plan,
|
| 136 |
# which is the only thing that knows; None means nobody chose an
|
|
@@ -162,35 +163,9 @@ def from_file(path, stack=None):
|
|
| 162 |
for line in _ODOO_IMPORT_RE.findall(body):
|
| 163 |
imports.extend(part.strip() for part in line.split(",") if part.strip())
|
| 164 |
ctx["odoo_imports"] = sorted(set(imports))
|
| 165 |
-
else:
|
| 166 |
-
tsconfig = _walk_up(path, ("tsconfig.json",))
|
| 167 |
-
if tsconfig:
|
| 168 |
-
ctx["tsconfig"] = tsconfig
|
| 169 |
-
ctx["ts_strict"] = _strict_from(tsconfig)
|
| 170 |
return ctx
|
| 171 |
|
| 172 |
|
| 173 |
-
def _strict_from(tsconfig):
|
| 174 |
-
"""strict from a tsconfig, tolerating the comments TypeScript allows in it.
|
| 175 |
-
|
| 176 |
-
JSON with comments is not JSON, and tsconfig.json is full of them. Stripping
|
| 177 |
-
line comments first is enough in practice; a file that still will not parse
|
| 178 |
-
returns None, which reads as 'not determinable' rather than 'not strict'.
|
| 179 |
-
"""
|
| 180 |
-
text = _read(tsconfig)
|
| 181 |
-
cleaned = re.sub(r"^\s*//.*$", "", text, flags=re.M)
|
| 182 |
-
cleaned = re.sub(r"/\*.*?\*/", "", cleaned, flags=re.S)
|
| 183 |
-
cleaned = re.sub(r",(\s*[}\]])", r"\1", cleaned)
|
| 184 |
-
try:
|
| 185 |
-
data = json.loads(cleaned)
|
| 186 |
-
except ValueError:
|
| 187 |
-
return None
|
| 188 |
-
options = data.get("compilerOptions")
|
| 189 |
-
if not isinstance(options, dict):
|
| 190 |
-
return None
|
| 191 |
-
return options.get("strict")
|
| 192 |
-
|
| 193 |
-
|
| 194 |
def empty(stack):
|
| 195 |
"""The context for a bare question: nothing known, and said so.
|
| 196 |
|
|
@@ -206,8 +181,6 @@ def empty(stack):
|
|
| 206 |
"manifest": None,
|
| 207 |
"models": [],
|
| 208 |
"odoo_imports": [],
|
| 209 |
-
"ts_strict": None,
|
| 210 |
-
"tsconfig": None,
|
| 211 |
"is_test": False,
|
| 212 |
# How much of the file the answer was about. Filled in by subject.plan,
|
| 213 |
# which is the only thing that knows; None means nobody chose an
|
|
@@ -521,9 +494,6 @@ def describe(ctx):
|
|
| 521 |
lines.append("Models in this file: {0}".format(", ".join(ctx["models"])))
|
| 522 |
if ctx.get("odoo_imports"):
|
| 523 |
lines.append("Already imported from odoo: {0}".format(", ".join(ctx["odoo_imports"])))
|
| 524 |
-
if ctx.get("ts_strict") is not None:
|
| 525 |
-
lines.append("TypeScript strict mode: {0}".format(
|
| 526 |
-
"on" if ctx["ts_strict"] else "OFF -- optional properties and null are not checked"))
|
| 527 |
if ctx.get("is_test"):
|
| 528 |
lines.append("This file is a test, so throwing on bad input is acceptable here.")
|
| 529 |
return lines
|
|
|
|
| 4 |
WHY THIS EXISTS
|
| 5 |
---------------
|
| 6 |
ARCHITECTURE records the context layer as absent by design, and names the reason
|
| 7 |
+
honestly: odoo.ondelete-is-a-decision cannot be decided from a snippet, because
|
| 8 |
+
whether a dangling reference matters depends on whether anything ever reads the
|
| 9 |
+
field back -- context a snippet does not contain.
|
| 10 |
|
| 11 |
That reasoning is correct about snippets and wrong about files. When the input is
|
| 12 |
a real path in a real tree, the context is sitting on disk: the module manifest
|
| 13 |
says which Odoo version this has to run on, the file says which model it extends,
|
| 14 |
+
ir.model.access.csv says whether the model can be deleted at all. None of it
|
| 15 |
+
needs a model to guess.
|
| 16 |
|
| 17 |
So this module reads the surroundings, and the context layer reports UNKNOWN --
|
| 18 |
never PASS -- whenever it was handed a snippet with no surroundings to read. An
|
|
|
|
| 54 |
_ODOO_IMPORT_RE = re.compile(r"^\s*from\s+odoo[\w.]*\s+import\s+(.+)$", re.M)
|
| 55 |
|
| 56 |
|
| 57 |
+
# What this corpus is able to review, and nothing else. One table, read by
|
| 58 |
+
# subject.route as well, so a file cannot be refused by the command and
|
| 59 |
+
# contextualised here anyway.
|
| 60 |
+
#
|
| 61 |
+
# .py is where every rule but one lives; .xml carries the noupdate rule, whose
|
| 62 |
+
# violation is a <record> in a data file. Anything else is refused by name
|
| 63 |
+
# rather than reviewed: 27 Odoo rules run over a .ts file find nothing, and
|
| 64 |
+
# nothing reads as a clean bill of health.
|
| 65 |
+
REVIEWABLE = (".py", ".xml")
|
|
|
|
|
|
|
| 66 |
|
| 67 |
|
| 68 |
def stack_for(path):
|
| 69 |
+
"""The rule stack a file belongs to. There is one, and this says whether
|
| 70 |
+
the file is in it.
|
| 71 |
+
|
| 72 |
+
Returns "odoo" for a file the corpus can review and None for one it cannot.
|
| 73 |
+
Both an Odoo module and a plain Python script return "odoo": the corpus
|
| 74 |
+
holds the ORM rules and the plain-Python rules together, so there is no
|
| 75 |
+
longer a second stack to send a script to, and a script inside an Odoo
|
| 76 |
+
repository -- scripts/, tools/, a migration helper -- is reviewed against
|
| 77 |
+
the eight language rules with the 19 ORM rules finding nothing in it.
|
| 78 |
"""
|
| 79 |
+
if os.path.splitext(path)[1].lower() in REVIEWABLE:
|
| 80 |
+
return "odoo"
|
| 81 |
+
return None
|
| 82 |
|
| 83 |
|
| 84 |
def _walk_up(start, names):
|
|
|
|
| 132 |
"manifest": None,
|
| 133 |
"models": [],
|
| 134 |
"odoo_imports": [],
|
|
|
|
|
|
|
| 135 |
"is_test": False,
|
| 136 |
# How much of the file the answer was about. Filled in by subject.plan,
|
| 137 |
# which is the only thing that knows; None means nobody chose an
|
|
|
|
| 163 |
for line in _ODOO_IMPORT_RE.findall(body):
|
| 164 |
imports.extend(part.strip() for part in line.split(",") if part.strip())
|
| 165 |
ctx["odoo_imports"] = sorted(set(imports))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
return ctx
|
| 167 |
|
| 168 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
def empty(stack):
|
| 170 |
"""The context for a bare question: nothing known, and said so.
|
| 171 |
|
|
|
|
| 181 |
"manifest": None,
|
| 182 |
"models": [],
|
| 183 |
"odoo_imports": [],
|
|
|
|
|
|
|
| 184 |
"is_test": False,
|
| 185 |
# How much of the file the answer was about. Filled in by subject.plan,
|
| 186 |
# which is the only thing that knows; None means nobody chose an
|
|
|
|
| 494 |
lines.append("Models in this file: {0}".format(", ".join(ctx["models"])))
|
| 495 |
if ctx.get("odoo_imports"):
|
| 496 |
lines.append("Already imported from odoo: {0}".format(", ".join(ctx["odoo_imports"])))
|
|
|
|
|
|
|
|
|
|
| 497 |
if ctx.get("is_test"):
|
| 498 |
lines.append("This file is a test, so throwing on bad input is acceptable here.")
|
| 499 |
return lines
|
|
@@ -5,15 +5,20 @@ The corpus in ../corpus/rules/*.json is the SINGLE SOURCE OF TRUTH at query time
|
|
| 5 |
System prompts and the retrieval index are both derived from it, so they cannot
|
| 6 |
drift apart.
|
| 7 |
|
| 8 |
-
It is not where rules are EDITED. `nexa build` regenerates
|
| 9 |
-
|
| 10 |
-
next build and then disappears without a word. Add rules there, in four places:
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
python corpus.py validate preflight the corpus against its schema
|
| 15 |
python corpus.py prompt odoo emit a SYSTEM block derived from the rules
|
| 16 |
-
python corpus.py modelfiles regenerate
|
| 17 |
python corpus.py index embed every rule for retrieval
|
| 18 |
python corpus.py ask odoo "question" answer, grounded in retrieved rules
|
| 19 |
python corpus.py verify answer.txt check every cited rule id actually exists
|
|
@@ -103,8 +108,12 @@ def rank_findings(hits):
|
|
| 103 |
hit.line, hit.rule))
|
| 104 |
|
| 105 |
|
| 106 |
-
STACK_MODEL = {"odoo": "ai-odoo"
|
| 107 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
# Splitting an answer into code and prose, and deciding what counts as a
|
| 109 |
# citation, live in layers.py -- so the CLI, the pipeline and the layer engine
|
| 110 |
# cannot drift apart on either question. Re-exported under the old names
|
|
@@ -245,8 +254,14 @@ def cmd_validate():
|
|
| 245 |
errors.append("{0}: duplicate id (also in stack '{1}')".format(path, seen[rid]))
|
| 246 |
seen[rid] = rule.get("stack")
|
| 247 |
|
| 248 |
-
|
| 249 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
|
| 251 |
# advisory is derived from likelihood by build_corpus.py. If the two
|
| 252 |
# disagree, the JSON was hand-edited, and a rule claiming to gate while
|
|
@@ -352,23 +367,14 @@ def cmd_validate():
|
|
| 352 |
# --------------------------------------------------------------------- prompt
|
| 353 |
|
| 354 |
HEADER = {
|
|
|
|
|
|
|
|
|
|
| 355 |
"odoo": (
|
| 356 |
-
"You are an Odoo engineer. You write Python using the Odoo ORM
|
| 357 |
-
"
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
"You are a Python engineer specialising in input validation and the places a "
|
| 361 |
-
"check appears to hold and does not. You write plain Python and the standard "
|
| 362 |
-
"library. You never write Odoo ORM code, TypeScript or JavaScript: if the "
|
| 363 |
-
"question is about Odoo models, say so rather than answering it here."
|
| 364 |
-
),
|
| 365 |
-
"typescript": (
|
| 366 |
-
"You are a TypeScript engineer specialising in runtime validation at system "
|
| 367 |
-
"boundaries. You never write Python or Odoo server code. Every answer is TypeScript."
|
| 368 |
-
),
|
| 369 |
-
"javascript": (
|
| 370 |
-
"You are a JavaScript engineer specialising in runtime validation at system "
|
| 371 |
-
"boundaries. You never write Python or Odoo server code. Every answer is JavaScript."
|
| 372 |
),
|
| 373 |
}
|
| 374 |
|
|
@@ -432,8 +438,7 @@ def cmd_prompt(stack):
|
|
| 432 |
REGISTRY = "registry.ollama.ai/library/"
|
| 433 |
|
| 434 |
DEEP_MODEL = "qwen3-coder:30b"
|
| 435 |
-
BASE = {"odoo": "qwen2.5-coder:7b"
|
| 436 |
-
"typescript": "qwen2.5-coder:7b", "javascript": "qwen2.5-coder:7b"}
|
| 437 |
PARAMS = """PARAMETER temperature 0.2
|
| 438 |
PARAMETER top_p 0.85
|
| 439 |
PARAMETER top_k 30
|
|
@@ -445,8 +450,7 @@ PARAMETER num_gpu 99"""
|
|
| 445 |
def cmd_modelfiles():
|
| 446 |
"""Regenerate Modelfiles from the corpus. Never hand-edit the generated files."""
|
| 447 |
made = []
|
| 448 |
-
for stack, model in (("odoo", "ai-odoo"),
|
| 449 |
-
("typescript", "ai-ts"), ("javascript", "ai-js")):
|
| 450 |
body = render_prompt(stack).replace('"""', "'''")
|
| 451 |
text = (
|
| 452 |
"# GENERATED by bin/corpus.py modelfiles - do not hand-edit.\n"
|
|
|
|
| 5 |
System prompts and the retrieval index are both derived from it, so they cannot
|
| 6 |
drift apart.
|
| 7 |
|
| 8 |
+
It is not where rules are EDITED. `nexa build` regenerates that file from
|
| 9 |
+
corpus/build_corpus.py, so a rule added to the JSON by hand survives until the
|
| 10 |
+
next build and then disappears without a word. Add rules there, in four places:
|
| 11 |
+
ODOO (or PYTHON, for a rule about the language rather than the ORM), ENRICH or
|
| 12 |
+
SEMANTIC_CHECK, PROBES, and check_sources.SYMBOLS. Each of the four fails
|
| 13 |
+
loudly when it is the one you forgot.
|
| 14 |
+
|
| 15 |
+
There is one stack, `odoo`, and every rule is selected for it. The `python.`
|
| 16 |
+
prefix on eight of them is provenance, not a second stack: an Odoo module is a
|
| 17 |
+
Python module, so the language rules apply to it.
|
| 18 |
|
| 19 |
python corpus.py validate preflight the corpus against its schema
|
| 20 |
python corpus.py prompt odoo emit a SYSTEM block derived from the rules
|
| 21 |
+
python corpus.py modelfiles regenerate the Modelfile from the corpus
|
| 22 |
python corpus.py index embed every rule for retrieval
|
| 23 |
python corpus.py ask odoo "question" answer, grounded in retrieved rules
|
| 24 |
python corpus.py verify answer.txt check every cited rule id actually exists
|
|
|
|
| 108 |
hit.line, hit.rule))
|
| 109 |
|
| 110 |
|
| 111 |
+
STACK_MODEL = {"odoo": "ai-odoo"}
|
| 112 |
+
|
| 113 |
+
# The id prefixes a rule may carry. Provenance: "odoo." is a rule about the ORM
|
| 114 |
+
# and is sourced to the Odoo documentation, "python." one about the language,
|
| 115 |
+
# sourced to docs.python.org. Both are selected for the one stack.
|
| 116 |
+
ORIGINS = ("odoo", "python")
|
| 117 |
# Splitting an answer into code and prose, and deciding what counts as a
|
| 118 |
# citation, live in layers.py -- so the CLI, the pipeline and the layer engine
|
| 119 |
# cannot drift apart on either question. Re-exported under the old names
|
|
|
|
| 254 |
errors.append("{0}: duplicate id (also in stack '{1}')".format(path, seen[rid]))
|
| 255 |
seen[rid] = rule.get("stack")
|
| 256 |
|
| 257 |
+
# The prefix names the rule's ORIGIN, not its stack -- there is one
|
| 258 |
+
# stack and eight of its rules come from the Python corpus. It is still
|
| 259 |
+
# checked, because an id is the thing every measurement and every
|
| 260 |
+
# citation is keyed on: a typo'd prefix silently orphans a rule from
|
| 261 |
+
# its own classification artifact.
|
| 262 |
+
if isinstance(rid, str) and rid.split(".")[0] not in ORIGINS:
|
| 263 |
+
errors.append("{0}: id prefix is not one of {1}".format(
|
| 264 |
+
path, ", ".join(sorted(ORIGINS))))
|
| 265 |
|
| 266 |
# advisory is derived from likelihood by build_corpus.py. If the two
|
| 267 |
# disagree, the JSON was hand-edited, and a rule claiming to gate while
|
|
|
|
| 367 |
# --------------------------------------------------------------------- prompt
|
| 368 |
|
| 369 |
HEADER = {
|
| 370 |
+
# One stack, one header. It names the Python half explicitly because the
|
| 371 |
+
# corpus holds eight rules about the language rather than the ORM, and an
|
| 372 |
+
# answer that treats "Odoo engineer" as "ORM only" drops them.
|
| 373 |
"odoo": (
|
| 374 |
+
"You are an Odoo engineer. You write Python using the Odoo ORM, and plain "
|
| 375 |
+
"Python where the ORM is not involved -- an Odoo module is a Python module, "
|
| 376 |
+
"so the standard library's traps are yours too. You never write TypeScript, "
|
| 377 |
+
"JavaScript, Zod or Pydantic. Every answer is Python."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 378 |
),
|
| 379 |
}
|
| 380 |
|
|
|
|
| 438 |
REGISTRY = "registry.ollama.ai/library/"
|
| 439 |
|
| 440 |
DEEP_MODEL = "qwen3-coder:30b"
|
| 441 |
+
BASE = {"odoo": "qwen2.5-coder:7b"}
|
|
|
|
| 442 |
PARAMS = """PARAMETER temperature 0.2
|
| 443 |
PARAMETER top_p 0.85
|
| 444 |
PARAMETER top_k 30
|
|
|
|
| 450 |
def cmd_modelfiles():
|
| 451 |
"""Regenerate Modelfiles from the corpus. Never hand-edit the generated files."""
|
| 452 |
made = []
|
| 453 |
+
for stack, model in (("odoo", "ai-odoo"),):
|
|
|
|
| 454 |
body = render_prompt(stack).replace('"""', "'''")
|
| 455 |
text = (
|
| 456 |
"# GENERATED by bin/corpus.py modelfiles - do not hand-edit.\n"
|
|
@@ -55,16 +55,13 @@ import layers # noqa: E402
|
|
| 55 |
|
| 56 |
CROSSWALK_VERSION = "1.0.0"
|
| 57 |
|
| 58 |
-
# A semgrep rule says which languages it is about. Ours say which stack,
|
| 59 |
-
#
|
| 60 |
-
#
|
|
|
|
| 61 |
LANG_STACKS = {
|
| 62 |
-
"python": ("odoo",
|
| 63 |
-
"py": ("odoo",
|
| 64 |
-
"js": ("javascript", "typescript"),
|
| 65 |
-
"javascript": ("javascript", "typescript"),
|
| 66 |
-
"ts": ("typescript", "javascript"),
|
| 67 |
-
"typescript": ("typescript", "javascript"),
|
| 68 |
}
|
| 69 |
|
| 70 |
# Directories that are never the code under review. The same list fieldtest
|
|
@@ -426,10 +423,10 @@ def crosswalk(external, our_rules):
|
|
| 426 |
def covered_stacks(external):
|
| 427 |
"""The stacks their ruleset is even about.
|
| 428 |
|
| 429 |
-
A
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
"""
|
| 434 |
stacks = set()
|
| 435 |
for ext in external:
|
|
|
|
| 55 |
|
| 56 |
CROSSWALK_VERSION = "1.0.0"
|
| 57 |
|
| 58 |
+
# A semgrep rule says which languages it is about. Ours say which stack, of
|
| 59 |
+
# which there is one, and it is Python: a `languages: [python]` rule is
|
| 60 |
+
# answerable by this corpus and a `languages: [js]` rule is not, which is the
|
| 61 |
+
# whole of the mapping now.
|
| 62 |
LANG_STACKS = {
|
| 63 |
+
"python": ("odoo",),
|
| 64 |
+
"py": ("odoo",),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
}
|
| 66 |
|
| 67 |
# Directories that are never the code under review. The same list fieldtest
|
|
|
|
| 423 |
def covered_stacks(external):
|
| 424 |
"""The stacks their ruleset is even about.
|
| 425 |
|
| 426 |
+
A JavaScript-only ruleset having nothing to say about any rule of ours is
|
| 427 |
+
not a finding, it is the languages line at the bottom of every one of their
|
| 428 |
+
rules. Comparing across it would pad the gap with entries whose answer was
|
| 429 |
+
known before the tool ran.
|
| 430 |
"""
|
| 431 |
stacks = set()
|
| 432 |
for ext in external:
|
|
@@ -68,12 +68,12 @@ DEFAULT_QUESTIONS = [
|
|
| 68 |
("odoo", "compute a stored total from the order lines"),
|
| 69 |
("odoo", "make the partner code unique across the table"),
|
| 70 |
("odoo", "compare two prices for equality"),
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
("
|
| 75 |
-
("
|
| 76 |
-
("
|
| 77 |
]
|
| 78 |
|
| 79 |
|
|
@@ -179,9 +179,9 @@ def retrievability(stack=None, k=None):
|
|
| 179 |
This is the measurement `nexa report rules` cannot make. Telemetry can only
|
| 180 |
say what WAS retrieved by the questions somebody happened to ask, and a rule
|
| 181 |
absent from that list may simply never have had a relevant question put to
|
| 182 |
-
it.
|
| 183 |
-
|
| 184 |
-
|
| 185 |
|
| 186 |
A probe is a question aimed at one rule. If the rule does not come back for
|
| 187 |
it, retrieval has genuinely missed it, and the rule is unreachable no matter
|
|
@@ -366,9 +366,9 @@ def summarise(rows, runs, pipeline_mode):
|
|
| 366 |
|
| 367 |
retrieved = collections.Counter(
|
| 368 |
rid for r in rows for rid in r["retrieved"])
|
| 369 |
-
# Scoped to the stacks that actually ran.
|
| 370 |
-
#
|
| 371 |
-
#
|
| 372 |
stacks = {r["stack"] for r in rows}
|
| 373 |
every = {r["id"] for r in corpus.load_rules() if r["stack"] in stacks}
|
| 374 |
never = sorted(every - set(retrieved))
|
|
@@ -395,22 +395,17 @@ DEFAULT_SUBJECTS = [
|
|
| 395 |
os.path.join(SUBJECTS, "odoo_module", "models", "fsm_job.py"),
|
| 396 |
os.path.join(SUBJECTS, "odoo_module", "models", "fsm_visit.py"),
|
| 397 |
os.path.join(SUBJECTS, "etl", "load_prices.py"),
|
| 398 |
-
os.path.join(SUBJECTS, "web", "src", "partners.ts"),
|
| 399 |
-
os.path.join(SUBJECTS, "web", "src", "board.js"),
|
| 400 |
]
|
| 401 |
|
| 402 |
-
|
|
|
|
|
|
|
|
|
|
| 403 |
SKIP_DIRS = frozenset([".git", "node_modules", "__pycache__", ".venv", "venv",
|
| 404 |
"dist", "build", ".tox", "static"])
|
| 405 |
|
| 406 |
_PY_NAME = re.compile(r"^(?:class|def)\s+(\w+)", re.M)
|
| 407 |
_PY_METHOD = re.compile(r"^\s+def\s+(\w+)", re.M)
|
| 408 |
-
# Column zero on purpose. Indented const and let are locals inside a handler,
|
| 409 |
-
# and counting `page` or `size` as something the file declares would score any
|
| 410 |
-
# answer that mentions pagination as a review of this file.
|
| 411 |
-
_WEB_NAME = re.compile(
|
| 412 |
-
r"^(?:export\s+(?:default\s+)?)?(?:async\s+)?"
|
| 413 |
-
r"(?:function|class|interface|type|enum|const|let|var)\s+(\w+)", re.M)
|
| 414 |
|
| 415 |
|
| 416 |
def declared_names(body, stack):
|
|
@@ -419,17 +414,12 @@ def declared_names(body, stack):
|
|
| 419 |
Not a quality measure. An answer that never mentions a single function in
|
| 420 |
the file it was handed is not reviewing that file, whatever its verdict
|
| 421 |
says, and there is no way to see that from the verdict alone.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 422 |
"""
|
| 423 |
-
|
| 424 |
-
names = set(_PY_NAME.findall(body)) | set(_PY_METHOD.findall(body))
|
| 425 |
-
else:
|
| 426 |
-
names = set()
|
| 427 |
-
for line in body.splitlines():
|
| 428 |
-
if "require(" in line or line.startswith("import "):
|
| 429 |
-
continue # a binding for somebody else's name, not this file's
|
| 430 |
-
found = _WEB_NAME.match(line)
|
| 431 |
-
if found:
|
| 432 |
-
names.add(found.group(1))
|
| 433 |
# Dunders are noise, and a name too short to be a coincidence-free match
|
| 434 |
# against English prose cannot carry this measurement.
|
| 435 |
return sorted(n for n in names if len(n) > 3 and not n.startswith("__"))
|
|
@@ -811,10 +801,10 @@ def summarise_check(rows, runs, skipped=0):
|
|
| 811 |
|
| 812 |
print("CONTEXT -- the layer `ask` can never run")
|
| 813 |
print(" ask has no file, so this layer always reports UNKNOWN. With a file it")
|
| 814 |
-
print(" reads the manifest
|
| 815 |
-
print("
|
| 816 |
-
print("
|
| 817 |
-
print("
|
| 818 |
reached, read = 0, 0
|
| 819 |
for path, row in files.items():
|
| 820 |
verdicts = {r["context_verdict"] for r in rows if r["path"] == path}
|
|
@@ -913,7 +903,7 @@ def main(argv):
|
|
| 913 |
parser.add_argument("--runs", type=int, default=3,
|
| 914 |
help="repetitions per question (default 3)")
|
| 915 |
parser.add_argument("--stack",
|
| 916 |
-
help="odoo,
|
| 917 |
parser.add_argument("--pipeline", action="store_true",
|
| 918 |
help="measure through run(), for escalation numbers")
|
| 919 |
parser.add_argument("--retrieval", action="store_true",
|
|
|
|
| 68 |
("odoo", "compute a stored total from the order lines"),
|
| 69 |
("odoo", "make the partner code unique across the table"),
|
| 70 |
("odoo", "compare two prices for equality"),
|
| 71 |
+
# The plain-Python half of the corpus, reached through Odoo-shaped
|
| 72 |
+
# questions because that is where these rules are now selected from: an
|
| 73 |
+
# eval that never asks one measures 19 of the 27 rules.
|
| 74 |
+
("odoo", "check an amount is positive before posting a payment"),
|
| 75 |
+
("odoo", "store a total in a monetary field without losing a penny"),
|
| 76 |
+
("odoo", "compare a date typed by a user against the current time"),
|
| 77 |
]
|
| 78 |
|
| 79 |
|
|
|
|
| 179 |
This is the measurement `nexa report rules` cannot make. Telemetry can only
|
| 180 |
say what WAS retrieved by the questions somebody happened to ask, and a rule
|
| 181 |
absent from that list may simply never have had a relevant question put to
|
| 182 |
+
it. Nine recorded queries against a TOP_K of 6 and 27 rules is a sample in
|
| 183 |
+
which most of the corpus could not have appeared however well it was
|
| 184 |
+
written -- arithmetic, not evidence about the rules.
|
| 185 |
|
| 186 |
A probe is a question aimed at one rule. If the rule does not come back for
|
| 187 |
it, retrieval has genuinely missed it, and the rule is unreachable no matter
|
|
|
|
| 366 |
|
| 367 |
retrieved = collections.Counter(
|
| 368 |
rid for r in rows for rid in r["retrieved"])
|
| 369 |
+
# Scoped to the stacks that actually ran. There is one, so this is a
|
| 370 |
+
# formality now -- kept because the scoping is what stops a partial run
|
| 371 |
+
# reporting rules it never had the chance to retrieve.
|
| 372 |
stacks = {r["stack"] for r in rows}
|
| 373 |
every = {r["id"] for r in corpus.load_rules() if r["stack"] in stacks}
|
| 374 |
never = sorted(every - set(retrieved))
|
|
|
|
| 395 |
os.path.join(SUBJECTS, "odoo_module", "models", "fsm_job.py"),
|
| 396 |
os.path.join(SUBJECTS, "odoo_module", "models", "fsm_visit.py"),
|
| 397 |
os.path.join(SUBJECTS, "etl", "load_prices.py"),
|
|
|
|
|
|
|
| 398 |
]
|
| 399 |
|
| 400 |
+
# The extensions `check` will review, and the same list context.REVIEWABLE
|
| 401 |
+
# holds. A file this expands to that the router then refuses would be an
|
| 402 |
+
# eval that counts a file nobody checked.
|
| 403 |
+
CHECKABLE = (".py", ".xml")
|
| 404 |
SKIP_DIRS = frozenset([".git", "node_modules", "__pycache__", ".venv", "venv",
|
| 405 |
"dist", "build", ".tox", "static"])
|
| 406 |
|
| 407 |
_PY_NAME = re.compile(r"^(?:class|def)\s+(\w+)", re.M)
|
| 408 |
_PY_METHOD = re.compile(r"^\s+def\s+(\w+)", re.M)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 409 |
|
| 410 |
|
| 411 |
def declared_names(body, stack):
|
|
|
|
| 414 |
Not a quality measure. An answer that never mentions a single function in
|
| 415 |
the file it was handed is not reviewing that file, whatever its verdict
|
| 416 |
says, and there is no way to see that from the verdict alone.
|
| 417 |
+
|
| 418 |
+
`stack` is still taken, and still the one stack: the signature is what the
|
| 419 |
+
caller has, and a function that silently ignores an argument is worse than
|
| 420 |
+
one that names why it has it.
|
| 421 |
"""
|
| 422 |
+
names = set(_PY_NAME.findall(body)) | set(_PY_METHOD.findall(body))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 423 |
# Dunders are noise, and a name too short to be a coincidence-free match
|
| 424 |
# against English prose cannot carry this measurement.
|
| 425 |
return sorted(n for n in names if len(n) > 3 and not n.startswith("__"))
|
|
|
|
| 801 |
|
| 802 |
print("CONTEXT -- the layer `ask` can never run")
|
| 803 |
print(" ask has no file, so this layer always reports UNKNOWN. With a file it")
|
| 804 |
+
print(" reads the manifest -- but reading is not checking. A verdict needs a")
|
| 805 |
+
print(" question it can answer: a series the corpus was checked against, or a")
|
| 806 |
+
print(" rule carrying odoo_versions. Anything else is read, stated as a gap,")
|
| 807 |
+
print(" and left UNKNOWN.")
|
| 808 |
reached, read = 0, 0
|
| 809 |
for path, row in files.items():
|
| 810 |
verdicts = {r["context_verdict"] for r in rows if r["path"] == path}
|
|
|
|
| 903 |
parser.add_argument("--runs", type=int, default=3,
|
| 904 |
help="repetitions per question (default 3)")
|
| 905 |
parser.add_argument("--stack",
|
| 906 |
+
help="odoo, the only stack")
|
| 907 |
parser.add_argument("--pipeline", action="store_true",
|
| 908 |
help="measure through run(), for escalation numbers")
|
| 909 |
parser.add_argument("--retrieval", action="store_true",
|
|
@@ -49,59 +49,23 @@ import external as ext # noqa: E402
|
|
| 49 |
import layers # noqa: E402
|
| 50 |
import semantics # noqa: E402
|
| 51 |
|
| 52 |
-
# Directories that are never the code under review.
|
| 53 |
-
#
|
| 54 |
-
#
|
| 55 |
-
# the stack asking. Reviewing Python it holds assets and vendored bundles.
|
| 56 |
-
# Reviewing JavaScript on an Odoo tree it holds ALL of it: every line of
|
| 57 |
-
# first-party JS an Odoo module ships lives under static/src. Skipping it
|
| 58 |
-
# unconditionally made `fieldtest --stack javascript` answer "0 files. Wrong
|
| 59 |
-
# folder, or the wrong --stack" on any Odoo checkout, which is a large part of
|
| 60 |
-
# why those seven rules have never been measured against real code.
|
| 61 |
SKIP_DIRS = frozenset([".git", ".hg", "node_modules", "__pycache__",
|
| 62 |
-
".tox", ".venv", "venv", "dist", "build", "migrations"
|
| 63 |
-
|
| 64 |
|
| 65 |
|
| 66 |
-
|
| 67 |
-
return PYTHON_SKIP_DIRS if stack in ("odoo", "python") else SKIP_DIRS
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
# A bundle is not the code under review either, and unlike a directory it has to
|
| 71 |
-
# be recognised from the file itself. Both markers are ordinary JavaScript
|
| 72 |
-
# convention rather than anything Odoo-specific: the name says so, or one line
|
| 73 |
-
# is longer than any line a person types. Measured on OpenSPP -- its five
|
| 74 |
-
# vendored libraries under static/lib carry single lines of 32,071, 95,621,
|
| 75 |
-
# 198,502 and 375,248 characters, while its 59 first-party files under
|
| 76 |
-
# static/src top out at 87.
|
| 77 |
-
BUNDLE_SUFFIXES = (".min.js", ".min.ts", ".bundle.js", ".bundle.ts")
|
| 78 |
-
BUNDLE_LINE = 1000
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
def _is_bundle(path, name):
|
| 82 |
-
"""Generated or vendored JavaScript, whatever directory it sits in."""
|
| 83 |
-
if name.endswith(BUNDLE_SUFFIXES):
|
| 84 |
-
return True
|
| 85 |
-
try:
|
| 86 |
-
with io.open(path, encoding="utf-8", errors="ignore") as handle:
|
| 87 |
-
return any(len(line) > BUNDLE_LINE for line in handle)
|
| 88 |
-
except OSError:
|
| 89 |
-
return False
|
| 90 |
-
|
| 91 |
-
EXTENSIONS = {"odoo": (".py",),
|
| 92 |
-
"python": (".py",),
|
| 93 |
-
"typescript": (".ts", ".tsx"),
|
| 94 |
-
"javascript": (".js", ".jsx", ".mjs")}
|
| 95 |
|
| 96 |
|
| 97 |
def sources(roots, stack, include_tests):
|
| 98 |
"""Every file under roots that belongs to this stack."""
|
| 99 |
wanted = EXTENSIONS[stack]
|
| 100 |
-
skip = skip_dirs(stack)
|
| 101 |
-
js = stack in ("javascript", "typescript")
|
| 102 |
for root in roots:
|
| 103 |
for dirpath, dirnames, filenames in os.walk(root):
|
| 104 |
-
dirnames[:] = [d for d in dirnames if d not in
|
| 105 |
if not include_tests and _is_test_path(dirpath):
|
| 106 |
continue
|
| 107 |
for name in sorted(filenames):
|
|
@@ -109,10 +73,7 @@ def sources(roots, stack, include_tests):
|
|
| 109 |
continue
|
| 110 |
if not include_tests and _is_test_file(name):
|
| 111 |
continue
|
| 112 |
-
|
| 113 |
-
if js and _is_bundle(path, name):
|
| 114 |
-
continue
|
| 115 |
-
yield path
|
| 116 |
|
| 117 |
|
| 118 |
# The extensions the TRIGGERS are read from, which is a wider set than the ones
|
|
@@ -133,11 +94,10 @@ def trigger_sources(roots, include_tests=False):
|
|
| 133 |
"""
|
| 134 |
for root in roots:
|
| 135 |
for dirpath, dirnames, filenames in os.walk(root):
|
| 136 |
-
#
|
| 137 |
-
#
|
| 138 |
-
#
|
| 139 |
-
|
| 140 |
-
dirnames[:] = [d for d in dirnames if d not in PYTHON_SKIP_DIRS]
|
| 141 |
if not include_tests and _is_test_path(dirpath):
|
| 142 |
continue
|
| 143 |
for name in sorted(filenames):
|
|
@@ -150,16 +110,15 @@ def _is_test_path(path):
|
|
| 150 |
return any(p in ("tests", "test", "__tests__", "spec") for p in parts)
|
| 151 |
|
| 152 |
|
| 153 |
-
# A test named by its FILE rather than by the directory holding it.
|
| 154 |
-
#
|
| 155 |
-
#
|
| 156 |
-
#
|
| 157 |
-
TEST_FILE = re.compile(r"(^test_|_test\.
|
| 158 |
|
| 159 |
|
| 160 |
def _is_test_file(name):
|
| 161 |
-
"""
|
| 162 |
-
field test sat in files like it, none of them shipped code."""
|
| 163 |
return bool(TEST_FILE.search(name))
|
| 164 |
|
| 165 |
|
|
@@ -412,7 +371,7 @@ def main(argv):
|
|
| 412 |
parser = argparse.ArgumentParser(prog="nexa fieldtest", add_help=True)
|
| 413 |
parser.add_argument("folders", nargs="*", help="trees of real source to read")
|
| 414 |
parser.add_argument("--stack", default="odoo",
|
| 415 |
-
help="odoo
|
| 416 |
parser.add_argument("--examples", type=int, default=3,
|
| 417 |
help="example hits to print per rule (default 3)")
|
| 418 |
parser.add_argument("--json", help="write every hit here as well")
|
|
|
|
| 49 |
import layers # noqa: E402
|
| 50 |
import semantics # noqa: E402
|
| 51 |
|
| 52 |
+
# Directories that are never the code under review. `static` is in the list:
|
| 53 |
+
# on an Odoo tree it holds assets, vendored bundles and the web client's own
|
| 54 |
+
# JavaScript, and none of the 27 rules has anything to say about any of it.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
SKIP_DIRS = frozenset([".git", ".hg", "node_modules", "__pycache__",
|
| 56 |
+
".tox", ".venv", "venv", "dist", "build", "migrations",
|
| 57 |
+
"static"])
|
| 58 |
|
| 59 |
|
| 60 |
+
EXTENSIONS = {"odoo": (".py",)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
|
| 62 |
|
| 63 |
def sources(roots, stack, include_tests):
|
| 64 |
"""Every file under roots that belongs to this stack."""
|
| 65 |
wanted = EXTENSIONS[stack]
|
|
|
|
|
|
|
| 66 |
for root in roots:
|
| 67 |
for dirpath, dirnames, filenames in os.walk(root):
|
| 68 |
+
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
|
| 69 |
if not include_tests and _is_test_path(dirpath):
|
| 70 |
continue
|
| 71 |
for name in sorted(filenames):
|
|
|
|
| 73 |
continue
|
| 74 |
if not include_tests and _is_test_file(name):
|
| 75 |
continue
|
| 76 |
+
yield os.path.join(dirpath, name)
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
|
| 79 |
# The extensions the TRIGGERS are read from, which is a wider set than the ones
|
|
|
|
| 94 |
"""
|
| 95 |
for root in roots:
|
| 96 |
for dirpath, dirnames, filenames in os.walk(root):
|
| 97 |
+
# static/ is skipped here too, and for a reason of its own: an
|
| 98 |
+
# Odoo tree keeps owl templates under static/src/xml, and those are
|
| 99 |
+
# not record rules and must not be read as though they were.
|
| 100 |
+
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
|
|
|
|
| 101 |
if not include_tests and _is_test_path(dirpath):
|
| 102 |
continue
|
| 103 |
for name in sorted(filenames):
|
|
|
|
| 110 |
return any(p in ("tests", "test", "__tests__", "spec") for p in parts)
|
| 111 |
|
| 112 |
|
| 113 |
+
# A test named by its FILE rather than by the directory holding it. Odoo puts
|
| 114 |
+
# tests in a tests/ package, which _is_test_path reads; a test named test_x.py
|
| 115 |
+
# outside one still has to be recognised. Anchored rather than a bare
|
| 116 |
+
# substring, because `latest_version.py` contains "test_".
|
| 117 |
+
TEST_FILE = re.compile(r"(^test_|_test\.)", re.I)
|
| 118 |
|
| 119 |
|
| 120 |
def _is_test_file(name):
|
| 121 |
+
"""test_partner.py is a test; latest_version.py is not."""
|
|
|
|
| 122 |
return bool(TEST_FILE.search(name))
|
| 123 |
|
| 124 |
|
|
|
|
| 371 |
parser = argparse.ArgumentParser(prog="nexa fieldtest", add_help=True)
|
| 372 |
parser.add_argument("folders", nargs="*", help="trees of real source to read")
|
| 373 |
parser.add_argument("--stack", default="odoo",
|
| 374 |
+
help="odoo, the only stack (default)")
|
| 375 |
parser.add_argument("--examples", type=int, default=3,
|
| 376 |
help="example hits to print per rule (default 3)")
|
| 377 |
parser.add_argument("--json", help="write every hit here as well")
|
|
@@ -56,8 +56,10 @@ META = os.path.join(ROOT, "corpus", "corpus_meta.json")
|
|
| 56 |
PASS, WARN, FAIL, UNKNOWN = "PASS", "WARN", "FAIL", "UNKNOWN"
|
| 57 |
_RANK = {PASS: 0, UNKNOWN: 1, WARN: 2, FAIL: 3}
|
| 58 |
|
|
|
|
|
|
|
| 59 |
CITATION_RE = re.compile(
|
| 60 |
-
r"\b(odoo|python
|
| 61 |
|
| 62 |
# The labels corpus.build_messages requires the answer to carry. Structural
|
| 63 |
# validation of the ANSWER, as distinct from schema validation of the corpus --
|
|
@@ -74,6 +76,10 @@ LEAKED_TEMPLATE = (
|
|
| 74 |
)
|
| 75 |
|
| 76 |
PY_LANGS = ("python", "py", "python3", "odoo")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
JS_LANGS = ("ts", "tsx", "typescript", "js", "jsx", "javascript", "mjs", "node")
|
| 78 |
|
| 79 |
_ESSENTIAL = ("syntactic", "structural", "domain")
|
|
@@ -212,8 +218,11 @@ def cited_ids(text):
|
|
| 212 |
|
| 213 |
|
| 214 |
def comment_kind(stack):
|
| 215 |
-
"""Which comment syntax a stack's generated code uses.
|
| 216 |
-
|
|
|
|
|
|
|
|
|
|
| 217 |
|
| 218 |
|
| 219 |
def matchable(code, stack):
|
|
@@ -504,7 +513,8 @@ def _lang_of(lang, stack):
|
|
| 504 |
return "js"
|
| 505 |
if lang:
|
| 506 |
return "other"
|
| 507 |
-
|
|
|
|
| 508 |
|
| 509 |
|
| 510 |
# -------------------------------------------------------------- 1 syntactic
|
|
@@ -512,10 +522,11 @@ def _lang_of(lang, stack):
|
|
| 512 |
def layer_syntactic(text, stack):
|
| 513 |
"""Does the generated code parse at all?
|
| 514 |
|
| 515 |
-
Python gets a real parse and compile.
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
|
|
|
| 519 |
"""
|
| 520 |
blocks, unterminated = code_blocks(text)
|
| 521 |
findings, checked, gaps = [], [], []
|
|
@@ -670,7 +681,7 @@ def layer_semantic(text, stack, rules):
|
|
| 670 |
for r in semantic_rules:
|
| 671 |
gaps.append((r["id"], "no Python block in the answer to analyse"))
|
| 672 |
gaps.append(("the answer", "semantic analysis is Python-only: the standard "
|
| 673 |
-
"library has no
|
| 674 |
return LayerResult("semantic", UNKNOWN, findings, checked, gaps)
|
| 675 |
|
| 676 |
# The checkers see the repaired source, so an indented method fragment is
|
|
@@ -814,8 +825,8 @@ def layer_context(rules, ctx):
|
|
| 814 |
|
| 815 |
if not ctx or ctx.get("source") == "none":
|
| 816 |
gaps.append(("the environment",
|
| 817 |
-
"no file was supplied, so the module
|
| 818 |
-
"
|
| 819 |
return LayerResult("context", UNKNOWN, findings, checked, gaps)
|
| 820 |
|
| 821 |
series = ctx.get("odoo_series")
|
|
@@ -842,6 +853,10 @@ def layer_context(rules, ctx):
|
|
| 842 |
checked.append("Odoo series {0} from {1}".format(
|
| 843 |
series, os.path.basename(ctx.get("manifest") or "manifest")))
|
| 844 |
else:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 845 |
gaps.append(("Odoo series", "no __manifest__.py with a version was found "
|
| 846 |
"above this file"))
|
| 847 |
|
|
@@ -855,29 +870,6 @@ def layer_context(rules, ctx):
|
|
| 855 |
"rule applies to Odoo {0}, this module is {1}".format(
|
| 856 |
declared, series), rule=r["id"]))
|
| 857 |
|
| 858 |
-
if ctx.get("ts_strict") is False:
|
| 859 |
-
# Demoted with the series mismatch above, for the same reason: it fires
|
| 860 |
-
# on the project rather than on the answer, and it fires on every run in
|
| 861 |
-
# any project that has not finished turning strict on. Making it a
|
| 862 |
-
# finding again would mean checking whether this answer leans on the
|
| 863 |
-
# type system at all -- its declared enforcement layer -- rather than
|
| 864 |
-
# assuming every answer does.
|
| 865 |
-
gaps.append(("type-based guarantees",
|
| 866 |
-
"tsconfig has strict off, so the compile-time half of any "
|
| 867 |
-
"type-based guarantee in this answer is not enforced here"))
|
| 868 |
-
elif ctx.get("ts_strict") is True:
|
| 869 |
-
checked.append("tsconfig strict")
|
| 870 |
-
elif ctx.get("stack") in ("typescript", "javascript"):
|
| 871 |
-
gaps.append(("tsconfig strict", "no tsconfig.json was found above this file"))
|
| 872 |
-
elif ctx.get("stack") == "python":
|
| 873 |
-
# Neither branch above applies to a plain Python script, and this used to
|
| 874 |
-
# fall through the odoo one and report a missing __manifest__.py -- a
|
| 875 |
-
# true sentence about a file that was never going to have one, and the
|
| 876 |
-
# only thing this layer said about every Python file it was given.
|
| 877 |
-
gaps.append(("the environment",
|
| 878 |
-
"a plain Python file has no manifest and no tsconfig, so "
|
| 879 |
-
"nothing on disk says which framework or interpreter version "
|
| 880 |
-
"this has to run under"))
|
| 881 |
|
| 882 |
unversioned = [r["id"] for r in rules if not r.get("odoo_versions")
|
| 883 |
and r.get("stack") == "odoo"]
|
|
|
|
| 56 |
PASS, WARN, FAIL, UNKNOWN = "PASS", "WARN", "FAIL", "UNKNOWN"
|
| 57 |
_RANK = {PASS: 0, UNKNOWN: 1, WARN: 2, FAIL: 3}
|
| 58 |
|
| 59 |
+
# The two id prefixes the corpus uses. Both are the one stack; the prefix says
|
| 60 |
+
# where the rule came from, and corpus.ORIGINS is the list this mirrors.
|
| 61 |
CITATION_RE = re.compile(
|
| 62 |
+
r"\b(odoo|python)\.[a-z0-9]+(?:-[a-z0-9]+)*\b")
|
| 63 |
|
| 64 |
# The labels corpus.build_messages requires the answer to carry. Structural
|
| 65 |
# validation of the ANSWER, as distinct from schema validation of the corpus --
|
|
|
|
| 76 |
)
|
| 77 |
|
| 78 |
PY_LANGS = ("python", "py", "python3", "odoo")
|
| 79 |
+
# Not a stack, and not reviewed. Odoo ships an OWL front end, so an answer can
|
| 80 |
+
# legitimately contain a JavaScript block -- it is recognised here so the
|
| 81 |
+
# syntactic layer can say it went unchecked, which is the only thing this
|
| 82 |
+
# corpus has to say about JavaScript.
|
| 83 |
JS_LANGS = ("ts", "tsx", "typescript", "js", "jsx", "javascript", "mjs", "node")
|
| 84 |
|
| 85 |
_ESSENTIAL = ("syntactic", "structural", "domain")
|
|
|
|
| 218 |
|
| 219 |
|
| 220 |
def comment_kind(stack):
|
| 221 |
+
"""Which comment syntax a stack's generated code uses. One stack, one
|
| 222 |
+
answer -- kept as a function because semantics.strip_comments takes the
|
| 223 |
+
kind as an argument, and inlining the constant at each call site is how
|
| 224 |
+
the two would come to disagree."""
|
| 225 |
+
return "python"
|
| 226 |
|
| 227 |
|
| 228 |
def matchable(code, stack):
|
|
|
|
| 513 |
return "js"
|
| 514 |
if lang:
|
| 515 |
return "other"
|
| 516 |
+
# Untagged, and there is one stack: Python.
|
| 517 |
+
return "python"
|
| 518 |
|
| 519 |
|
| 520 |
# -------------------------------------------------------------- 1 syntactic
|
|
|
|
| 522 |
def layer_syntactic(text, stack):
|
| 523 |
"""Does the generated code parse at all?
|
| 524 |
|
| 525 |
+
Python gets a real parse and compile. A JavaScript block -- which an Odoo
|
| 526 |
+
answer can legitimately contain, the web client being OWL -- gets delimiter
|
| 527 |
+
balance, because there is no JavaScript grammar in the standard library.
|
| 528 |
+
That not_checked entry is permanent and says so rather than implying a
|
| 529 |
+
parse happened.
|
| 530 |
"""
|
| 531 |
blocks, unterminated = code_blocks(text)
|
| 532 |
findings, checked, gaps = [], [], []
|
|
|
|
| 681 |
for r in semantic_rules:
|
| 682 |
gaps.append((r["id"], "no Python block in the answer to analyse"))
|
| 683 |
gaps.append(("the answer", "semantic analysis is Python-only: the standard "
|
| 684 |
+
"library has no JavaScript parser"))
|
| 685 |
return LayerResult("semantic", UNKNOWN, findings, checked, gaps)
|
| 686 |
|
| 687 |
# The checkers see the repaired source, so an indented method fragment is
|
|
|
|
| 825 |
|
| 826 |
if not ctx or ctx.get("source") == "none":
|
| 827 |
gaps.append(("the environment",
|
| 828 |
+
"no file was supplied, so the module and Odoo series are "
|
| 829 |
+
"unknown; run 'nexa check <file>' for a contextual verdict"))
|
| 830 |
return LayerResult("context", UNKNOWN, findings, checked, gaps)
|
| 831 |
|
| 832 |
series = ctx.get("odoo_series")
|
|
|
|
| 853 |
checked.append("Odoo series {0} from {1}".format(
|
| 854 |
series, os.path.basename(ctx.get("manifest") or "manifest")))
|
| 855 |
else:
|
| 856 |
+
# True of an Odoo module whose manifest has no version, and of a
|
| 857 |
+
# plain script in the repository that was never going to have one.
|
| 858 |
+
# Said either way: nothing on disk states the series, so every
|
| 859 |
+
# version-specific rule below is unverified here.
|
| 860 |
gaps.append(("Odoo series", "no __manifest__.py with a version was found "
|
| 861 |
"above this file"))
|
| 862 |
|
|
|
|
| 870 |
"rule applies to Odoo {0}, this module is {1}".format(
|
| 871 |
declared, series), rule=r["id"]))
|
| 872 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 873 |
|
| 874 |
unversioned = [r["id"] for r in rules if not r.get("odoo_versions")
|
| 875 |
and r.get("stack") == "odoo"]
|
|
@@ -68,27 +68,32 @@ import layers # noqa: E402
|
|
| 68 |
import semantics # noqa: E402
|
| 69 |
import subject # noqa: E402
|
| 70 |
|
| 71 |
-
EXTENSIONS = {"odoo": (".py",)
|
| 72 |
-
"typescript": (".ts", ".tsx"),
|
| 73 |
-
"javascript": (".js", ".jsx", ".mjs")}
|
| 74 |
|
| 75 |
# A commit is IN SCOPE when its diff touches one of these. Hand-written, and
|
| 76 |
# therefore the weakest link in the measurement -- which is why it is printed
|
| 77 |
# in the report rather than buried here. Derived from what the rules are about,
|
| 78 |
# not from their patterns: using the patterns would make the denominator depend
|
| 79 |
# on the thing being measured, and inflate the answer.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
SCOPE_TOKENS = {
|
| 81 |
"odoo": ("api.constrains", "api.depends", "api.onchange", "_sql_constraints",
|
| 82 |
"ondelete", "check_company", ".sudo(", "Many2one", "Many2many",
|
| 83 |
"float_compare", "float_is_zero", "_check_recursion", "_has_cycle",
|
| 84 |
-
"ValidationError", "store=True", "noupdate"
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
"as ", ": any", "process.env", "z.union", "nullable("),
|
| 90 |
-
"javascript": ("JSON.parse", "parseInt", "parseFloat", "forEach", "new Date(",
|
| 91 |
-
"||", "hasOwnProperty", "req.body", "req.query"),
|
| 92 |
}
|
| 93 |
|
| 94 |
# Commit subjects that mean "this fixed something". OCA's own convention is a
|
|
@@ -616,7 +621,7 @@ def main(argv):
|
|
| 616 |
parser = argparse.ArgumentParser(prog="nexa recall", add_help=True)
|
| 617 |
parser.add_argument("repos", nargs="*", help="git checkouts WITH history")
|
| 618 |
parser.add_argument("--stack", default="odoo",
|
| 619 |
-
help="odoo
|
| 620 |
parser.add_argument("--limit", type=int, default=0,
|
| 621 |
help="stop after this many fix commits per repo")
|
| 622 |
parser.add_argument("--misses", type=int, default=25,
|
|
|
|
| 68 |
import semantics # noqa: E402
|
| 69 |
import subject # noqa: E402
|
| 70 |
|
| 71 |
+
EXTENSIONS = {"odoo": (".py",)}
|
|
|
|
|
|
|
| 72 |
|
| 73 |
# A commit is IN SCOPE when its diff touches one of these. Hand-written, and
|
| 74 |
# therefore the weakest link in the measurement -- which is why it is printed
|
| 75 |
# in the report rather than buried here. Derived from what the rules are about,
|
| 76 |
# not from their patterns: using the patterns would make the denominator depend
|
| 77 |
# on the thing being measured, and inflate the answer.
|
| 78 |
+
# One list, because there is one stack, and the constructs the composed-in
|
| 79 |
+
# Python rules are about are now inside it. Leaving them out would shrink the
|
| 80 |
+
# denominator against rules that are being counted in the numerator, which is
|
| 81 |
+
# the direction that flatters the tool.
|
| 82 |
+
#
|
| 83 |
+
# One substitution, stated because it is a judgment: the plain-Python list used
|
| 84 |
+
# to carry "def " for mutable-default-argument. On an Odoo tree that matches
|
| 85 |
+
# nearly every diff, and a denominator that large says nothing about anything.
|
| 86 |
+
# The construct the rule is actually about is a mutable default, so that is
|
| 87 |
+
# what is listed.
|
| 88 |
SCOPE_TOKENS = {
|
| 89 |
"odoo": ("api.constrains", "api.depends", "api.onchange", "_sql_constraints",
|
| 90 |
"ondelete", "check_company", ".sudo(", "Many2one", "Many2many",
|
| 91 |
"float_compare", "float_is_zero", "_check_recursion", "_has_cycle",
|
| 92 |
+
"ValidationError", "store=True", "noupdate",
|
| 93 |
+
# the plain-Python half
|
| 94 |
+
"assert ", "except:", "eval(", "exec(", "pickle.load", "shell=True",
|
| 95 |
+
"os.system(", "os.path.join(", "float(", "Decimal", "datetime.now(",
|
| 96 |
+
"utcnow(", "=[]", "={}", "=set()"),
|
|
|
|
|
|
|
|
|
|
| 97 |
}
|
| 98 |
|
| 99 |
# Commit subjects that mean "this fixed something". OCA's own convention is a
|
|
|
|
| 621 |
parser = argparse.ArgumentParser(prog="nexa recall", add_help=True)
|
| 622 |
parser.add_argument("repos", nargs="*", help="git checkouts WITH history")
|
| 623 |
parser.add_argument("--stack", default="odoo",
|
| 624 |
+
help="odoo, the only stack (default)")
|
| 625 |
parser.add_argument("--limit", type=int, default=0,
|
| 626 |
help="stop after this many fix commits per repo")
|
| 627 |
parser.add_argument("--misses", type=int, default=25,
|
|
@@ -149,7 +149,7 @@ def cmd_rules(events):
|
|
| 149 |
if known is None:
|
| 150 |
print(" never retrieved: corpus/rules could not be read")
|
| 151 |
else:
|
| 152 |
-
missing = sorted(known - set(retrieved))
|
| 153 |
if not missing:
|
| 154 |
print(" never retrieved: none, every rule has surfaced at least once")
|
| 155 |
else:
|
|
@@ -164,7 +164,8 @@ def _explain_absence(retrieves, missing, known):
|
|
| 164 |
"""Say how much of that list the sample size explains by itself.
|
| 165 |
|
| 166 |
One query retrieves at most TOP_K rules, so a stack with more rules than
|
| 167 |
-
queries x TOP_K has rules that COULD NOT have appeared.
|
|
|
|
| 168 |
beside rules that genuinely lost their own ranking, under one heading that
|
| 169 |
calls both dead weight, is how a measurement turns into a wrong conclusion:
|
| 170 |
four of the seven names this used to print were on it for arithmetic.
|
|
@@ -174,7 +175,7 @@ def _explain_absence(retrieves, missing, known):
|
|
| 174 |
for r in retrieves:
|
| 175 |
if r.get("stack"):
|
| 176 |
per_stack[r["stack"]] += 1
|
| 177 |
-
sizes = collections.Counter(
|
| 178 |
|
| 179 |
print()
|
| 180 |
print(" queries recorded, by stack:")
|
|
@@ -215,7 +216,13 @@ def _top_k():
|
|
| 215 |
|
| 216 |
|
| 217 |
def _corpus_rule_ids():
|
| 218 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 219 |
|
| 220 |
Telemetry can only say what WAS retrieved. "Never retrieved" is a statement
|
| 221 |
about the corpus, so the corpus has to be opened to make it -- which is why
|
|
@@ -225,7 +232,7 @@ def _corpus_rule_ids():
|
|
| 225 |
rules_dir = os.path.join(os.path.dirname(HERE), "corpus", "rules")
|
| 226 |
if not os.path.isdir(rules_dir):
|
| 227 |
return None
|
| 228 |
-
ids =
|
| 229 |
try:
|
| 230 |
for name in sorted(os.listdir(rules_dir)):
|
| 231 |
if not name.endswith(".json"):
|
|
@@ -233,7 +240,7 @@ def _corpus_rule_ids():
|
|
| 233 |
with io.open(os.path.join(rules_dir, name), encoding="utf-8") as fh:
|
| 234 |
data = json.load(fh)
|
| 235 |
for rule in (data if isinstance(data, list) else data.get("rules", [])):
|
| 236 |
-
ids
|
| 237 |
except (OSError, ValueError, KeyError, TypeError):
|
| 238 |
return None
|
| 239 |
return ids or None
|
|
|
|
| 149 |
if known is None:
|
| 150 |
print(" never retrieved: corpus/rules could not be read")
|
| 151 |
else:
|
| 152 |
+
missing = sorted(set(known) - set(retrieved))
|
| 153 |
if not missing:
|
| 154 |
print(" never retrieved: none, every rule has surfaced at least once")
|
| 155 |
else:
|
|
|
|
| 164 |
"""Say how much of that list the sample size explains by itself.
|
| 165 |
|
| 166 |
One query retrieves at most TOP_K rules, so a stack with more rules than
|
| 167 |
+
queries x TOP_K has rules that COULD NOT have appeared. `known` is
|
| 168 |
+
{id: stack} from _corpus_rule_ids. Reporting those
|
| 169 |
beside rules that genuinely lost their own ranking, under one heading that
|
| 170 |
calls both dead weight, is how a measurement turns into a wrong conclusion:
|
| 171 |
four of the seven names this used to print were on it for arithmetic.
|
|
|
|
| 175 |
for r in retrieves:
|
| 176 |
if r.get("stack"):
|
| 177 |
per_stack[r["stack"]] += 1
|
| 178 |
+
sizes = collections.Counter(known.values())
|
| 179 |
|
| 180 |
print()
|
| 181 |
print(" queries recorded, by stack:")
|
|
|
|
| 216 |
|
| 217 |
|
| 218 |
def _corpus_rule_ids():
|
| 219 |
+
"""{rule id: its stack} for the whole corpus, or None if it cannot be read.
|
| 220 |
+
|
| 221 |
+
The stack comes off the rule and not off the id prefix. Those were the same
|
| 222 |
+
thing until the plain-Python rules were composed into the Odoo stack, and
|
| 223 |
+
splitting on the dot afterwards invented a "python" stack with eight rules
|
| 224 |
+
and no queries -- then reported all eight as unreachable by arithmetic,
|
| 225 |
+
which is a conclusion about a stack that does not exist.
|
| 226 |
|
| 227 |
Telemetry can only say what WAS retrieved. "Never retrieved" is a statement
|
| 228 |
about the corpus, so the corpus has to be opened to make it -- which is why
|
|
|
|
| 232 |
rules_dir = os.path.join(os.path.dirname(HERE), "corpus", "rules")
|
| 233 |
if not os.path.isdir(rules_dir):
|
| 234 |
return None
|
| 235 |
+
ids = {}
|
| 236 |
try:
|
| 237 |
for name in sorted(os.listdir(rules_dir)):
|
| 238 |
if not name.endswith(".json"):
|
|
|
|
| 240 |
with io.open(os.path.join(rules_dir, name), encoding="utf-8") as fh:
|
| 241 |
data = json.load(fh)
|
| 242 |
for rule in (data if isinstance(data, list) else data.get("rules", [])):
|
| 243 |
+
ids[rule["id"]] = rule["stack"]
|
| 244 |
except (OSError, ValueError, KeyError, TypeError):
|
| 245 |
return None
|
| 246 |
return ids or None
|
|
@@ -39,7 +39,17 @@ sys.path.insert(0, HERE)
|
|
| 39 |
|
| 40 |
import context # noqa: E402
|
| 41 |
|
| 42 |
-
STACKS = ("odoo",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
# How much of a file `nexa check` sends. The 7B runs at num_ctx 8192, which has
|
| 45 |
# to hold the retrieved rules, the system prompt and the answer as well, so the
|
|
@@ -72,27 +82,34 @@ REVIEW_QUESTION = (
|
|
| 72 |
def route(path, body, ctx, forced=None):
|
| 73 |
"""(stack, how it was decided). The stack decision on its own, for testing.
|
| 74 |
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
|
|
|
|
|
|
|
|
|
| 83 |
"""
|
| 84 |
if forced:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
return forced, "--stack"
|
| 86 |
-
# One extension table, context.py's, so this cannot
|
| 87 |
-
#
|
| 88 |
by_extension = context.stack_for(path)
|
| 89 |
-
if by_extension
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
return
|
| 96 |
|
| 97 |
|
| 98 |
# A line that starts a new top-level thing in TS/JS. Column zero, because an
|
|
@@ -370,9 +387,10 @@ def plan(path, forced=None):
|
|
| 370 |
with io.open(path, encoding="utf-8", errors="ignore") as fh:
|
| 371 |
body = fh.read()
|
| 372 |
|
| 373 |
-
# Gathered first,
|
| 374 |
-
# a
|
| 375 |
-
#
|
|
|
|
| 376 |
ctx = context.from_file(path)
|
| 377 |
stack, routed_by = route(path, body, ctx, forced)
|
| 378 |
ctx["stack"] = stack
|
|
@@ -385,11 +403,10 @@ def plan(path, forced=None):
|
|
| 385 |
# its content does not support is still worth checking -- somebody asked --
|
| 386 |
# but the reader has to be told which of the two it is.
|
| 387 |
notes = []
|
| 388 |
-
if
|
| 389 |
-
notes.append("no Odoo imports here and no __manifest__.py above it --
|
| 390 |
-
"
|
| 391 |
-
|
| 392 |
-
notes.append("this imports odoo -- run with --stack odoo for the ORM rules")
|
| 393 |
|
| 394 |
# The surroundings go into the question, not just into the context layer. A
|
| 395 |
# model told the module targets Odoo 17.0 answers differently from one left to
|
|
|
|
| 39 |
|
| 40 |
import context # noqa: E402
|
| 41 |
|
| 42 |
+
STACKS = ("odoo",)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class Unreviewable(Exception):
|
| 46 |
+
"""The file is in a language this corpus has no rules for.
|
| 47 |
+
|
| 48 |
+
Refusing is the point. Running 27 Odoo rules over a .ts file produces an
|
| 49 |
+
empty finding list, and an empty finding list is read as "nothing wrong
|
| 50 |
+
here" -- so silence about a file nobody checked would be the one lie the
|
| 51 |
+
tool is built not to tell.
|
| 52 |
+
"""
|
| 53 |
|
| 54 |
# How much of a file `nexa check` sends. The 7B runs at num_ctx 8192, which has
|
| 55 |
# to hold the retrieved rules, the system prompt and the answer as well, so the
|
|
|
|
| 82 |
def route(path, body, ctx, forced=None):
|
| 83 |
"""(stack, how it was decided). The stack decision on its own, for testing.
|
| 84 |
|
| 85 |
+
There is one stack, so this is no longer a choice between corpora -- it is
|
| 86 |
+
the decision whether the corpus applies to this file at all. A .py or .xml
|
| 87 |
+
file is reviewed; anything else raises Unreviewable.
|
| 88 |
+
|
| 89 |
+
It used to pick between an odoo and a python corpus by reading the file for
|
| 90 |
+
an odoo import or walking up for a __manifest__.py. That choice is gone,
|
| 91 |
+
because the two corpora are one: the plain-Python rules are composed into
|
| 92 |
+
the Odoo stack, so a script in an Odoo repository gets them without being
|
| 93 |
+
routed away from the ORM rules first. What the file looks like is still
|
| 94 |
+
read -- plan() states it in the report -- it just no longer decides which
|
| 95 |
+
rules run.
|
| 96 |
"""
|
| 97 |
if forced:
|
| 98 |
+
if forced not in STACKS:
|
| 99 |
+
raise Unreviewable(
|
| 100 |
+
"unknown stack '{0}'. There is one: {1}".format(
|
| 101 |
+
forced, ", ".join(STACKS)))
|
| 102 |
return forced, "--stack"
|
| 103 |
+
# One extension table, context.py's, so this cannot refuse a file the layer
|
| 104 |
+
# that reads the surroundings would have contextualised.
|
| 105 |
by_extension = context.stack_for(path)
|
| 106 |
+
if by_extension is None:
|
| 107 |
+
raise Unreviewable(
|
| 108 |
+
"nexa reviews Odoo code: {0} files. {1} is not one, and there are "
|
| 109 |
+
"no rules in this corpus that would say anything about it.".format(
|
| 110 |
+
" and ".join(context.REVIEWABLE),
|
| 111 |
+
os.path.splitext(path)[1] or "that extension"))
|
| 112 |
+
return by_extension, "extension"
|
| 113 |
|
| 114 |
|
| 115 |
# A line that starts a new top-level thing in TS/JS. Column zero, because an
|
|
|
|
| 387 |
with io.open(path, encoding="utf-8", errors="ignore") as fh:
|
| 388 |
body = fh.read()
|
| 389 |
|
| 390 |
+
# Gathered first, and labelled afterwards: the extension cannot tell an Odoo
|
| 391 |
+
# module from a plain Python script and this can. It no longer decides which
|
| 392 |
+
# rules run -- there is one corpus -- but it still decides what the report
|
| 393 |
+
# says about the file, and the context layer needs it either way.
|
| 394 |
ctx = context.from_file(path)
|
| 395 |
stack, routed_by = route(path, body, ctx, forced)
|
| 396 |
ctx["stack"] = stack
|
|
|
|
| 403 |
# its content does not support is still worth checking -- somebody asked --
|
| 404 |
# but the reader has to be told which of the two it is.
|
| 405 |
notes = []
|
| 406 |
+
if not looks_odoo:
|
| 407 |
+
notes.append("no Odoo imports here and no __manifest__.py above it -- the 19 "
|
| 408 |
+
"ORM rules have nothing to match, so this file is effectively "
|
| 409 |
+
"reviewed against the 8 plain-Python ones")
|
|
|
|
| 410 |
|
| 411 |
# The surroundings go into the question, not just into the context layer. A
|
| 412 |
# model told the module targets Odoo 17.0 answers differently from one left to
|
|
@@ -1,408 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"generated_utc": "2026-09-16",
|
| 3 |
-
"stack": "javascript",
|
| 4 |
-
"roots": [
|
| 5 |
-
"OpenSPP2-19.0",
|
| 6 |
-
"server-tools",
|
| 7 |
-
"account-financial-tools"
|
| 8 |
-
],
|
| 9 |
-
"files": 62,
|
| 10 |
-
"lines": 9304,
|
| 11 |
-
"totals": {
|
| 12 |
-
"false": 22,
|
| 13 |
-
"true": 21
|
| 14 |
-
},
|
| 15 |
-
"classes": {
|
| 16 |
-
"string-default": 21,
|
| 17 |
-
"boolean-logic": 1
|
| 18 |
-
},
|
| 19 |
-
"hits": [
|
| 20 |
-
{
|
| 21 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 22 |
-
"path": "OpenSPP2-19.0/spp_branding_kit/static/src/js/user_menu.js",
|
| 23 |
-
"line": 11,
|
| 24 |
-
"verdict": "false",
|
| 25 |
-
"class": "string-default",
|
| 26 |
-
"why": "the default replaces only a blank, and every left side here is a URL, colour, label, mode or error message -- blank is the absence of configuration there, not a value somebody meant to store",
|
| 27 |
-
"evidence": "const docUrl = session.spp_documentation_url || \"https://docs.openspp.org\";"
|
| 28 |
-
},
|
| 29 |
-
{
|
| 30 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 31 |
-
"path": "OpenSPP2-19.0/spp_branding_kit/static/src/js/user_menu.js",
|
| 32 |
-
"line": 12,
|
| 33 |
-
"verdict": "false",
|
| 34 |
-
"class": "string-default",
|
| 35 |
-
"why": "the default replaces only a blank, and every left side here is a URL, colour, label, mode or error message -- blank is the absence of configuration there, not a value somebody meant to store",
|
| 36 |
-
"evidence": "const supportUrl = session.spp_support_url || \"https://openspp.org\";"
|
| 37 |
-
},
|
| 38 |
-
{
|
| 39 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 40 |
-
"path": "OpenSPP2-19.0/spp_branding_kit/static/src/js/webclient.js",
|
| 41 |
-
"line": 9,
|
| 42 |
-
"verdict": "false",
|
| 43 |
-
"class": "string-default",
|
| 44 |
-
"why": "the default replaces only a blank, and every left side here is a URL, colour, label, mode or error message -- blank is the absence of configuration there, not a value somebody meant to store",
|
| 45 |
-
"evidence": "const systemName = session.spp_system_name || \"OpenSPP Platform\";"
|
| 46 |
-
},
|
| 47 |
-
{
|
| 48 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 49 |
-
"path": "OpenSPP2-19.0/spp_cel_widget/static/src/js/cel_editor.js",
|
| 50 |
-
"line": 128,
|
| 51 |
-
"verdict": "true",
|
| 52 |
-
"class": null,
|
| 53 |
-
"why": null,
|
| 54 |
-
"evidence": "return this._currentProfile || this.props.profile || \"registry_individuals\";"
|
| 55 |
-
},
|
| 56 |
-
{
|
| 57 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 58 |
-
"path": "OpenSPP2-19.0/spp_cel_widget/static/src/js/cel_editor.js",
|
| 59 |
-
"line": 243,
|
| 60 |
-
"verdict": "false",
|
| 61 |
-
"class": "string-default",
|
| 62 |
-
"why": "the default replaces only a blank, and every left side here is a URL, colour, label, mode or error message -- blank is the absence of configuration there, not a value somebody meant to store",
|
| 63 |
-
"evidence": "minHeight: this.props.minHeight || \"80px\","
|
| 64 |
-
},
|
| 65 |
-
{
|
| 66 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 67 |
-
"path": "OpenSPP2-19.0/spp_cel_widget/static/src/js/cel_editor.js",
|
| 68 |
-
"line": 467,
|
| 69 |
-
"verdict": "false",
|
| 70 |
-
"class": "string-default",
|
| 71 |
-
"why": "the default replaces only a blank, and every left side here is a URL, colour, label, mode or error message -- blank is the absence of configuration there, not a value somebody meant to store",
|
| 72 |
-
"evidence": "return firstError?.message || \"Invalid expression\";"
|
| 73 |
-
},
|
| 74 |
-
{
|
| 75 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 76 |
-
"path": "OpenSPP2-19.0/spp_cel_widget/static/src/js/cel_symbol_service.js",
|
| 77 |
-
"line": 78,
|
| 78 |
-
"verdict": "false",
|
| 79 |
-
"class": "string-default",
|
| 80 |
-
"why": "the default replaces only a blank, and every left side here is a URL, colour, label, mode or error message -- blank is the absence of configuration there, not a value somebody meant to store",
|
| 81 |
-
"evidence": "message: error.message || \"Validation failed\","
|
| 82 |
-
},
|
| 83 |
-
{
|
| 84 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 85 |
-
"path": "OpenSPP2-19.0/spp_cel_widget/static/src/js/cel_language/autocomplete.js",
|
| 86 |
-
"line": 198,
|
| 87 |
-
"verdict": "false",
|
| 88 |
-
"class": "boolean-logic",
|
| 89 |
-
"why": "genuine boolean logic, which the rule explicitly reserves || for",
|
| 90 |
-
"evidence": "if (!prefix || \"andornot\".includes(prefix)) {"
|
| 91 |
-
},
|
| 92 |
-
{
|
| 93 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 94 |
-
"path": "OpenSPP2-19.0/spp_cel_widget/static/src/js/cel_language/lint.js",
|
| 95 |
-
"line": 105,
|
| 96 |
-
"verdict": "false",
|
| 97 |
-
"class": "string-default",
|
| 98 |
-
"why": "the default replaces only a blank, and every left side here is a URL, colour, label, mode or error message -- blank is the absence of configuration there, not a value somebody meant to store",
|
| 99 |
-
"evidence": "message: error.message || \"Unknown error\","
|
| 100 |
-
},
|
| 101 |
-
{
|
| 102 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 103 |
-
"path": "OpenSPP2-19.0/spp_change_request_v2/static/src/components/review_panel/review_panel.js",
|
| 104 |
-
"line": 143,
|
| 105 |
-
"verdict": "true",
|
| 106 |
-
"class": null,
|
| 107 |
-
"why": null,
|
| 108 |
-
"evidence": "currentTier: pendingReview.current_tier || 1,"
|
| 109 |
-
},
|
| 110 |
-
{
|
| 111 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 112 |
-
"path": "OpenSPP2-19.0/spp_change_request_v2/static/src/components/review_panel/review_panel.js",
|
| 113 |
-
"line": 144,
|
| 114 |
-
"verdict": "true",
|
| 115 |
-
"class": null,
|
| 116 |
-
"why": null,
|
| 117 |
-
"evidence": "totalTiers: pendingReview.tier_review_ids?.length || 1,"
|
| 118 |
-
},
|
| 119 |
-
{
|
| 120 |
-
"rule": "javascript.number-parsing-is-lossy",
|
| 121 |
-
"path": "OpenSPP2-19.0/spp_change_request_v2/static/src/js/cr_review_documents.js",
|
| 122 |
-
"line": 37,
|
| 123 |
-
"verdict": "true",
|
| 124 |
-
"class": null,
|
| 125 |
-
"why": null,
|
| 126 |
-
"evidence": "const docId = parseInt(el.dataset.docId, 10);"
|
| 127 |
-
},
|
| 128 |
-
{
|
| 129 |
-
"rule": "javascript.number-parsing-is-lossy",
|
| 130 |
-
"path": "OpenSPP2-19.0/spp_change_request_v2/static/src/js/cr_search_results_field.js",
|
| 131 |
-
"line": 36,
|
| 132 |
-
"verdict": "true",
|
| 133 |
-
"class": null,
|
| 134 |
-
"why": null,
|
| 135 |
-
"evidence": "const partnerId = parseInt(row.dataset.partnerId, 10);"
|
| 136 |
-
},
|
| 137 |
-
{
|
| 138 |
-
"rule": "javascript.number-parsing-is-lossy",
|
| 139 |
-
"path": "OpenSPP2-19.0/spp_change_request_v2/static/src/js/cr_search_results_field.js",
|
| 140 |
-
"line": 47,
|
| 141 |
-
"verdict": "true",
|
| 142 |
-
"class": null,
|
| 143 |
-
"why": null,
|
| 144 |
-
"evidence": "const page = parseInt(link.dataset.page);"
|
| 145 |
-
},
|
| 146 |
-
{
|
| 147 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 148 |
-
"path": "OpenSPP2-19.0/spp_change_request_v2/static/src/js/create_change_request.js",
|
| 149 |
-
"line": 27,
|
| 150 |
-
"verdict": "false",
|
| 151 |
-
"class": "string-default",
|
| 152 |
-
"why": "the default replaces only a blank, and every left side here is a URL, colour, label, mode or error message -- blank is the absence of configuration there, not a value somebody meant to store",
|
| 153 |
-
"evidence": "name: params.name || \"Change Request Details\","
|
| 154 |
-
},
|
| 155 |
-
{
|
| 156 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 157 |
-
"path": "OpenSPP2-19.0/spp_change_request_v2/static/src/js/create_change_request.js",
|
| 158 |
-
"line": 51,
|
| 159 |
-
"verdict": "false",
|
| 160 |
-
"class": "string-default",
|
| 161 |
-
"why": "the default replaces only a blank, and every left side here is a URL, colour, label, mode or error message -- blank is the absence of configuration there, not a value somebody meant to store",
|
| 162 |
-
"evidence": "name: params.name || \"Change Request\","
|
| 163 |
-
},
|
| 164 |
-
{
|
| 165 |
-
"rule": "javascript.number-parsing-is-lossy",
|
| 166 |
-
"path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_arch_parser.esm.js",
|
| 167 |
-
"line": 73,
|
| 168 |
-
"verdict": "true",
|
| 169 |
-
"class": null,
|
| 170 |
-
"why": null,
|
| 171 |
-
"evidence": "limit: limit ? parseInt(limit, 10) : null,"
|
| 172 |
-
},
|
| 173 |
-
{
|
| 174 |
-
"rule": "javascript.number-parsing-is-lossy",
|
| 175 |
-
"path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_arch_parser.esm.js",
|
| 176 |
-
"line": 74,
|
| 177 |
-
"verdict": "true",
|
| 178 |
-
"class": null,
|
| 179 |
-
"why": null,
|
| 180 |
-
"evidence": "countLimit: countLimit ? parseInt(countLimit, 10) : null,"
|
| 181 |
-
},
|
| 182 |
-
{
|
| 183 |
-
"rule": "javascript.json-parse-returns-any",
|
| 184 |
-
"path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_renderer/gis_renderer.esm.js",
|
| 185 |
-
"line": 397,
|
| 186 |
-
"verdict": "true",
|
| 187 |
-
"class": null,
|
| 188 |
-
"why": null,
|
| 189 |
-
"evidence": "defaultCenter = JSON.parse(editInfo.default_center);"
|
| 190 |
-
},
|
| 191 |
-
{
|
| 192 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 193 |
-
"path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_renderer/gis_renderer.esm.js",
|
| 194 |
-
"line": 606,
|
| 195 |
-
"verdict": "false",
|
| 196 |
-
"class": "string-default",
|
| 197 |
-
"why": "the default replaces only a blank, and every left side here is a URL, colour, label, mode or error message -- blank is the absence of configuration there, not a value somebody meant to store",
|
| 198 |
-
"evidence": "return layer.begin_color || \"#3388ff\";"
|
| 199 |
-
},
|
| 200 |
-
{
|
| 201 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 202 |
-
"path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_renderer/gis_renderer.esm.js",
|
| 203 |
-
"line": 611,
|
| 204 |
-
"verdict": "true",
|
| 205 |
-
"class": null,
|
| 206 |
-
"why": null,
|
| 207 |
-
"evidence": "const maxValue = config.max_value || 100;"
|
| 208 |
-
},
|
| 209 |
-
{
|
| 210 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 211 |
-
"path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_renderer/gis_renderer.esm.js",
|
| 212 |
-
"line": 649,
|
| 213 |
-
"verdict": "false",
|
| 214 |
-
"class": "string-default",
|
| 215 |
-
"why": "the default replaces only a blank, and every left side here is a URL, colour, label, mode or error message -- blank is the absence of configuration there, not a value somebody meant to store",
|
| 216 |
-
"evidence": "return layer.begin_color || \"#3388ff\";"
|
| 217 |
-
},
|
| 218 |
-
{
|
| 219 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 220 |
-
"path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_renderer/gis_renderer.esm.js",
|
| 221 |
-
"line": 665,
|
| 222 |
-
"verdict": "true",
|
| 223 |
-
"class": null,
|
| 224 |
-
"why": null,
|
| 225 |
-
"evidence": "const classCount = config.class_count || 5;"
|
| 226 |
-
},
|
| 227 |
-
{
|
| 228 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 229 |
-
"path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_renderer/gis_renderer.esm.js",
|
| 230 |
-
"line": 702,
|
| 231 |
-
"verdict": "false",
|
| 232 |
-
"class": "string-default",
|
| 233 |
-
"why": "the default replaces only a blank, and every left side here is a URL, colour, label, mode or error message -- blank is the absence of configuration there, not a value somebody meant to store",
|
| 234 |
-
"evidence": "fillColor = layer.begin_color || \"#3388ff\";"
|
| 235 |
-
},
|
| 236 |
-
{
|
| 237 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 238 |
-
"path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_renderer/gis_renderer.esm.js",
|
| 239 |
-
"line": 913,
|
| 240 |
-
"verdict": "false",
|
| 241 |
-
"class": "string-default",
|
| 242 |
-
"why": "the default replaces only a blank, and every left side here is a URL, colour, label, mode or error message -- blank is the absence of configuration there, not a value somebody meant to store",
|
| 243 |
-
"evidence": "fillColor = layer.begin_color || \"#3388ff\";"
|
| 244 |
-
},
|
| 245 |
-
{
|
| 246 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 247 |
-
"path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_renderer/gis_renderer.esm.js",
|
| 248 |
-
"line": 1040,
|
| 249 |
-
"verdict": "true",
|
| 250 |
-
"class": null,
|
| 251 |
-
"why": null,
|
| 252 |
-
"evidence": "const maxValue = config.max_value || 100;"
|
| 253 |
-
},
|
| 254 |
-
{
|
| 255 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 256 |
-
"path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_renderer/gis_renderer.esm.js",
|
| 257 |
-
"line": 1087,
|
| 258 |
-
"verdict": "true",
|
| 259 |
-
"class": null,
|
| 260 |
-
"why": null,
|
| 261 |
-
"evidence": "const classCount = config.class_count || 5;"
|
| 262 |
-
},
|
| 263 |
-
{
|
| 264 |
-
"rule": "javascript.json-parse-returns-any",
|
| 265 |
-
"path": "OpenSPP2-19.0/spp_gis/static/src/js/widgets/gis_edit_map/field_gis_edit_map.esm.js",
|
| 266 |
-
"line": 46,
|
| 267 |
-
"verdict": "true",
|
| 268 |
-
"class": null,
|
| 269 |
-
"why": null,
|
| 270 |
-
"evidence": "const default_center = JSON.parse(editInfo.default_center);"
|
| 271 |
-
},
|
| 272 |
-
{
|
| 273 |
-
"rule": "javascript.json-parse-returns-any",
|
| 274 |
-
"path": "OpenSPP2-19.0/spp_gis/static/src/js/widgets/gis_edit_map/field_gis_edit_map.esm.js",
|
| 275 |
-
"line": 96,
|
| 276 |
-
"verdict": "true",
|
| 277 |
-
"class": null,
|
| 278 |
-
"why": null,
|
| 279 |
-
"evidence": "const obj = JSON.parse(this.props.record.data[this.props.name]);"
|
| 280 |
-
},
|
| 281 |
-
{
|
| 282 |
-
"rule": "javascript.json-parse-returns-any",
|
| 283 |
-
"path": "OpenSPP2-19.0/spp_gis/static/src/js/widgets/gis_edit_map/field_gis_edit_map.esm.js",
|
| 284 |
-
"line": 140,
|
| 285 |
-
"verdict": "true",
|
| 286 |
-
"class": null,
|
| 287 |
-
"why": null,
|
| 288 |
-
"evidence": "const obj = JSON.parse(record.data[fieldName]);"
|
| 289 |
-
},
|
| 290 |
-
{
|
| 291 |
-
"rule": "javascript.json-parse-returns-any",
|
| 292 |
-
"path": "OpenSPP2-19.0/spp_gis/static/src/js/widgets/gis_edit_map/field_gis_edit_map.esm.js",
|
| 293 |
-
"line": 220,
|
| 294 |
-
"verdict": "true",
|
| 295 |
-
"class": null,
|
| 296 |
-
"why": null,
|
| 297 |
-
"evidence": "const geom = JSON.parse(this.props.record.data[this.props.name]);"
|
| 298 |
-
},
|
| 299 |
-
{
|
| 300 |
-
"rule": "javascript.number-parsing-is-lossy",
|
| 301 |
-
"path": "OpenSPP2-19.0/spp_import_match/static/src/import_match_selector/import_match_selector.js",
|
| 302 |
-
"line": 85,
|
| 303 |
-
"verdict": "true",
|
| 304 |
-
"class": null,
|
| 305 |
-
"why": null,
|
| 306 |
-
"evidence": "const matchId = parseInt(ev.target.value, 10) || false;"
|
| 307 |
-
},
|
| 308 |
-
{
|
| 309 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 310 |
-
"path": "OpenSPP2-19.0/spp_pii_encryption/static/src/js/masked_field.js",
|
| 311 |
-
"line": 117,
|
| 312 |
-
"verdict": "false",
|
| 313 |
-
"class": "string-default",
|
| 314 |
-
"why": "the default replaces only a blank, and every left side here is a URL, colour, label, mode or error message -- blank is the absence of configuration there, not a value somebody meant to store",
|
| 315 |
-
"evidence": "return result || \"••••••••\";"
|
| 316 |
-
},
|
| 317 |
-
{
|
| 318 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 319 |
-
"path": "OpenSPP2-19.0/spp_programs/static/src/js/create_program.js",
|
| 320 |
-
"line": 18,
|
| 321 |
-
"verdict": "false",
|
| 322 |
-
"class": "string-default",
|
| 323 |
-
"why": "the default replaces only a blank, and every left side here is a URL, colour, label, mode or error message -- blank is the absence of configuration there, not a value somebody meant to store",
|
| 324 |
-
"evidence": "const programName = action.params?.name || \"Program\";"
|
| 325 |
-
},
|
| 326 |
-
{
|
| 327 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 328 |
-
"path": "OpenSPP2-19.0/spp_registry/static/src/js/custom_client_action.js",
|
| 329 |
-
"line": 15,
|
| 330 |
-
"verdict": "false",
|
| 331 |
-
"class": "string-default",
|
| 332 |
-
"why": "the default replaces only a blank, and every left side here is a URL, colour, label, mode or error message -- blank is the absence of configuration there, not a value somebody meant to store",
|
| 333 |
-
"evidence": "type: params.type || \"info\","
|
| 334 |
-
},
|
| 335 |
-
{
|
| 336 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 337 |
-
"path": "OpenSPP2-19.0/spp_registry_search/static/src/js/registry_search_portal.js",
|
| 338 |
-
"line": 89,
|
| 339 |
-
"verdict": "false",
|
| 340 |
-
"class": "string-default",
|
| 341 |
-
"why": "the default replaces only a blank, and every left side here is a URL, colour, label, mode or error message -- blank is the absence of configuration there, not a value somebody meant to store",
|
| 342 |
-
"evidence": "this.state.searchMode = config.search_mode || \"unified\";"
|
| 343 |
-
},
|
| 344 |
-
{
|
| 345 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 346 |
-
"path": "OpenSPP2-19.0/spp_registry_search/static/src/js/registry_search_portal.js",
|
| 347 |
-
"line": 90,
|
| 348 |
-
"verdict": "false",
|
| 349 |
-
"class": "string-default",
|
| 350 |
-
"why": "the default replaces only a blank, and every left side here is a URL, colour, label, mode or error message -- blank is the absence of configuration there, not a value somebody meant to store",
|
| 351 |
-
"evidence": "this.state.searchField = config.target_field || \"name\";"
|
| 352 |
-
},
|
| 353 |
-
{
|
| 354 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 355 |
-
"path": "OpenSPP2-19.0/spp_registry_search/static/src/js/registry_search_portal.js",
|
| 356 |
-
"line": 91,
|
| 357 |
-
"verdict": "true",
|
| 358 |
-
"class": null,
|
| 359 |
-
"why": null,
|
| 360 |
-
"evidence": "this.resultLimit = config.result_limit || 50;"
|
| 361 |
-
},
|
| 362 |
-
{
|
| 363 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 364 |
-
"path": "OpenSPP2-19.0/spp_registry_search/static/src/js/registry_search_portal.js",
|
| 365 |
-
"line": 92,
|
| 366 |
-
"verdict": "true",
|
| 367 |
-
"class": null,
|
| 368 |
-
"why": null,
|
| 369 |
-
"evidence": "this.minSearchChars = config.min_chars || 3;"
|
| 370 |
-
},
|
| 371 |
-
{
|
| 372 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 373 |
-
"path": "OpenSPP2-19.0/spp_studio/static/src/js/logic_editor/variable_picker.js",
|
| 374 |
-
"line": 289,
|
| 375 |
-
"verdict": "false",
|
| 376 |
-
"class": "string-default",
|
| 377 |
-
"why": "the default replaces only a blank, and every left side here is a URL, colour, label, mode or error message -- blank is the absence of configuration there, not a value somebody meant to store",
|
| 378 |
-
"evidence": "const scope = this.state.scopeFilter || \"all\";"
|
| 379 |
-
},
|
| 380 |
-
{
|
| 381 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 382 |
-
"path": "OpenSPP2-19.0/spp_studio/static/src/js/logic_editor/variable_picker.js",
|
| 383 |
-
"line": 295,
|
| 384 |
-
"verdict": "false",
|
| 385 |
-
"class": "string-default",
|
| 386 |
-
"why": "the default replaces only a blank, and every left side here is a URL, colour, label, mode or error message -- blank is the absence of configuration there, not a value somebody meant to store",
|
| 387 |
-
"evidence": "const applies = v.applies_to || \"both\";"
|
| 388 |
-
},
|
| 389 |
-
{
|
| 390 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 391 |
-
"path": "OpenSPP2-19.0/spp_studio/static/src/js/logic_editor/variable_picker.js",
|
| 392 |
-
"line": 313,
|
| 393 |
-
"verdict": "false",
|
| 394 |
-
"class": "string-default",
|
| 395 |
-
"why": "the default replaces only a blank, and every left side here is a URL, colour, label, mode or error message -- blank is the absence of configuration there, not a value somebody meant to store",
|
| 396 |
-
"evidence": "const applies = variable.applies_to || \"both\";"
|
| 397 |
-
},
|
| 398 |
-
{
|
| 399 |
-
"rule": "javascript.json-parse-returns-any",
|
| 400 |
-
"path": "server-tools/base_exception/static/src/js/base_exception.esm.js",
|
| 401 |
-
"line": 25,
|
| 402 |
-
"verdict": "true",
|
| 403 |
-
"class": null,
|
| 404 |
-
"why": null,
|
| 405 |
-
"evidence": "const excData = JSON.parse(originalError.data.message);"
|
| 406 |
-
}
|
| 407 |
-
]
|
| 408 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -0,0 +1,571 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"generated_utc": "2026-09-16T07:59:24.115329+00:00",
|
| 3 |
+
"stack": "odoo",
|
| 4 |
+
"subject": "the 8 python.* rules composed into the Odoo stack",
|
| 5 |
+
"checkouts": {
|
| 6 |
+
"OpenSPP2-19.0": "cfa0dc8e",
|
| 7 |
+
"server-tools": "60cd54e5e",
|
| 8 |
+
"account-financial-tools": "e4c1b86aa"
|
| 9 |
+
},
|
| 10 |
+
"lines": {
|
| 11 |
+
"OpenSPP2-19.0": 226937,
|
| 12 |
+
"OCA": 33960
|
| 13 |
+
},
|
| 14 |
+
"totals": {
|
| 15 |
+
"true": 26,
|
| 16 |
+
"false": 24
|
| 17 |
+
},
|
| 18 |
+
"by_rule": {
|
| 19 |
+
"python.assert-is-not-validation": {
|
| 20 |
+
"true": 6,
|
| 21 |
+
"false": 4
|
| 22 |
+
},
|
| 23 |
+
"python.decimal-for-money-not-float": {
|
| 24 |
+
"true": 0,
|
| 25 |
+
"false": 3
|
| 26 |
+
},
|
| 27 |
+
"python.eval-and-pickle-execute-their-input": {
|
| 28 |
+
"true": 0,
|
| 29 |
+
"false": 1
|
| 30 |
+
},
|
| 31 |
+
"python.naive-and-aware-datetimes-do-not-compare": {
|
| 32 |
+
"true": 20,
|
| 33 |
+
"false": 12
|
| 34 |
+
},
|
| 35 |
+
"python.path-join-does-not-contain-a-path": {
|
| 36 |
+
"true": 0,
|
| 37 |
+
"false": 4
|
| 38 |
+
}
|
| 39 |
+
},
|
| 40 |
+
"classes": {
|
| 41 |
+
"validates-caller-data": 6,
|
| 42 |
+
"internal-invariant": 3,
|
| 43 |
+
"odoo-monetary-is-float": 3,
|
| 44 |
+
"matched-inside-a-docstring": 2,
|
| 45 |
+
"display-only": 8,
|
| 46 |
+
"naive-utc-across-an-api-boundary": 2,
|
| 47 |
+
"local-now-against-an-orm-datetime": 5,
|
| 48 |
+
"local-now-into-an-orm-datetime": 9,
|
| 49 |
+
"local-date-as-an-api-default": 2,
|
| 50 |
+
"self-consistent-utc": 2,
|
| 51 |
+
"local-time-labelled-as-utc": 1,
|
| 52 |
+
"no-user-supplied-component": 4,
|
| 53 |
+
"re-enforced-on-the-next-line": 1,
|
| 54 |
+
"local-date-against-an-orm-date": 1,
|
| 55 |
+
"self-consistent-local": 1
|
| 56 |
+
},
|
| 57 |
+
"hits": [
|
| 58 |
+
{
|
| 59 |
+
"rule": "python.assert-is-not-validation",
|
| 60 |
+
"tree": "OpenSPP2-19.0",
|
| 61 |
+
"path": "endpoint_route_handler/registry.py",
|
| 62 |
+
"line": 314,
|
| 63 |
+
"verdict": "true",
|
| 64 |
+
"class": "validates-caller-data",
|
| 65 |
+
"why": "options come back off a jsonb row in from_row(), so this asserts about stored data rather than a program invariant; under -O a malformed handler dict is accepted silently",
|
| 66 |
+
"evidence": "assert \"klass_dotted_path\" in value[\"handler\"]"
|
| 67 |
+
},
|
| 68 |
+
{
|
| 69 |
+
"rule": "python.assert-is-not-validation",
|
| 70 |
+
"tree": "OpenSPP2-19.0",
|
| 71 |
+
"path": "endpoint_route_handler/registry.py",
|
| 72 |
+
"line": 315,
|
| 73 |
+
"verdict": "true",
|
| 74 |
+
"class": "validates-caller-data",
|
| 75 |
+
"why": "same setter, same stored data, second key",
|
| 76 |
+
"evidence": "assert \"method_name\" in value[\"handler\"]"
|
| 77 |
+
},
|
| 78 |
+
{
|
| 79 |
+
"rule": "python.assert-is-not-validation",
|
| 80 |
+
"tree": "OpenSPP2-19.0",
|
| 81 |
+
"path": "spp_registry/models/reg_relationship.py",
|
| 82 |
+
"line": 165,
|
| 83 |
+
"verdict": "false",
|
| 84 |
+
"class": "internal-invariant",
|
| 85 |
+
"why": "_check_partner is called from two @api.constrains with the literal 'source' and 'destination'; the assert is impossible unless the module itself is broken, which is what assert is for",
|
| 86 |
+
"evidence": "assert side in [\"source\", \"destination\"]"
|
| 87 |
+
},
|
| 88 |
+
{
|
| 89 |
+
"rule": "python.decimal-for-money-not-float",
|
| 90 |
+
"tree": "OpenSPP2-19.0",
|
| 91 |
+
"path": "spp_programs/models/cel/entitlement_amount_cel.py",
|
| 92 |
+
"line": 300,
|
| 93 |
+
"verdict": "false",
|
| 94 |
+
"class": "odoo-monetary-is-float",
|
| 95 |
+
"why": "self.amount is a Monetary field, which the ORM defines AS a float; the rule's remedy (Decimal) is not the Odoo idiom and float_round/float_compare are",
|
| 96 |
+
"evidence": "\"base_amount\": float(self.amount or 0.0),"
|
| 97 |
+
},
|
| 98 |
+
{
|
| 99 |
+
"rule": "python.decimal-for-money-not-float",
|
| 100 |
+
"tree": "OpenSPP2-19.0",
|
| 101 |
+
"path": "spp_programs/models/cel/entitlement_amount_cel.py",
|
| 102 |
+
"line": 316,
|
| 103 |
+
"verdict": "false",
|
| 104 |
+
"class": "odoo-monetary-is-float",
|
| 105 |
+
"why": "the CEL result is coerced for a Monetary field; same reason",
|
| 106 |
+
"evidence": "amount = float(result)"
|
| 107 |
+
},
|
| 108 |
+
{
|
| 109 |
+
"rule": "python.eval-and-pickle-execute-their-input",
|
| 110 |
+
"tree": "OpenSPP2-19.0",
|
| 111 |
+
"path": "spp_cel_domain/models/cel_service.py",
|
| 112 |
+
"line": 417,
|
| 113 |
+
"verdict": "false",
|
| 114 |
+
"class": "matched-inside-a-docstring",
|
| 115 |
+
"why": "the text is prose in a method docstring -- 'more robust and secure than regex-based conversion to Python eval()' -- and there is no eval call here at all",
|
| 116 |
+
"evidence": "and secure than regex-based conversion to Python eval()."
|
| 117 |
+
},
|
| 118 |
+
{
|
| 119 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 120 |
+
"tree": "OpenSPP2-19.0",
|
| 121 |
+
"path": "scripts/compliance/checker.py",
|
| 122 |
+
"line": 745,
|
| 123 |
+
"verdict": "false",
|
| 124 |
+
"class": "display-only",
|
| 125 |
+
"why": "a Generated: line in a markdown report; formatted once, never compared",
|
| 126 |
+
"evidence": "f\"**Generated:** {__import__('datetime').datetime.now().isoformat()}\","
|
| 127 |
+
},
|
| 128 |
+
{
|
| 129 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 130 |
+
"tree": "OpenSPP2-19.0",
|
| 131 |
+
"path": "scripts/compliance/checker.py",
|
| 132 |
+
"line": 813,
|
| 133 |
+
"verdict": "false",
|
| 134 |
+
"class": "display-only",
|
| 135 |
+
"why": "the same timestamp in the JSON version of that report",
|
| 136 |
+
"evidence": "\"generated\": __import__(\"datetime\").datetime.now().isoformat(),"
|
| 137 |
+
},
|
| 138 |
+
{
|
| 139 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 140 |
+
"tree": "OpenSPP2-19.0",
|
| 141 |
+
"path": "spp_analytics/models/service_aggregation.py",
|
| 142 |
+
"line": 97,
|
| 143 |
+
"verdict": "false",
|
| 144 |
+
"class": "display-only",
|
| 145 |
+
"why": "computed_at in a result dict that is returned, not stored or compared",
|
| 146 |
+
"evidence": "\"computed_at\": datetime.now().isoformat(),"
|
| 147 |
+
},
|
| 148 |
+
{
|
| 149 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 150 |
+
"tree": "OpenSPP2-19.0",
|
| 151 |
+
"path": "spp_api_v2/routers/consent.py",
|
| 152 |
+
"line": 178,
|
| 153 |
+
"verdict": "true",
|
| 154 |
+
"class": "naive-utc-across-an-api-boundary",
|
| 155 |
+
"why": "datetime.utcnow() is naive, is returned in a ConsentRevokeResponse, and serialises with no offset, so the consumer cannot tell the zone; deprecated in 3.12 for this reason",
|
| 156 |
+
"evidence": "revoked_at=datetime.utcnow(),"
|
| 157 |
+
},
|
| 158 |
+
{
|
| 159 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 160 |
+
"tree": "OpenSPP2-19.0",
|
| 161 |
+
"path": "spp_api_v2/routers/consent.py",
|
| 162 |
+
"line": 270,
|
| 163 |
+
"verdict": "true",
|
| 164 |
+
"class": "naive-utc-across-an-api-boundary",
|
| 165 |
+
"why": "the consent receipt timestamp, same construct. SUPPRESSED, and wrongly: the marker inside that return statement reads 'nosemgrep: odoo-sudo-without-context' and is about a different rule",
|
| 166 |
+
"evidence": "timestamp=datetime.utcnow(),",
|
| 167 |
+
"suppressed": true
|
| 168 |
+
},
|
| 169 |
+
{
|
| 170 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 171 |
+
"tree": "OpenSPP2-19.0",
|
| 172 |
+
"path": "spp_api_v2/routers/oauth.py",
|
| 173 |
+
"line": 217,
|
| 174 |
+
"verdict": "true",
|
| 175 |
+
"class": "naive-utc-across-an-api-boundary",
|
| 176 |
+
"why": "utcnow() feeds a JWT exp and iat; the lifetime is correct only because every value in the payload comes from the same naive call, and a later switch to datetime.now() would shift every token by the server's offset with nothing failing",
|
| 177 |
+
"evidence": "now = datetime.utcnow()"
|
| 178 |
+
},
|
| 179 |
+
{
|
| 180 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 181 |
+
"tree": "OpenSPP2-19.0",
|
| 182 |
+
"path": "spp_api_v2/services/group_service.py",
|
| 183 |
+
"line": 142,
|
| 184 |
+
"verdict": "true",
|
| 185 |
+
"class": "local-now-against-an-orm-datetime",
|
| 186 |
+
"why": "m.ended_date > now, where ended_date is an Odoo Datetime (naive UTC) and now is naive LOCAL -- membership filtering is off by the server's offset",
|
| 187 |
+
"evidence": "now = datetime.now()"
|
| 188 |
+
},
|
| 189 |
+
{
|
| 190 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 191 |
+
"tree": "OpenSPP2-19.0",
|
| 192 |
+
"path": "spp_api_v2/services/group_service.py",
|
| 193 |
+
"line": 748,
|
| 194 |
+
"verdict": "true",
|
| 195 |
+
"class": "local-now-into-an-orm-datetime",
|
| 196 |
+
"why": "datetime.now() is written straight into ended_date",
|
| 197 |
+
"evidence": "ended_datetime = datetime.now()"
|
| 198 |
+
},
|
| 199 |
+
{
|
| 200 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 201 |
+
"tree": "OpenSPP2-19.0",
|
| 202 |
+
"path": "spp_api_v2/services/group_service.py",
|
| 203 |
+
"line": 886,
|
| 204 |
+
"verdict": "true",
|
| 205 |
+
"class": "local-now-into-an-orm-datetime",
|
| 206 |
+
"why": "same write, in the group-merge path",
|
| 207 |
+
"evidence": "now = datetime.now()"
|
| 208 |
+
},
|
| 209 |
+
{
|
| 210 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 211 |
+
"tree": "OpenSPP2-19.0",
|
| 212 |
+
"path": "spp_api_v2/services/group_service.py",
|
| 213 |
+
"line": 1100,
|
| 214 |
+
"verdict": "true",
|
| 215 |
+
"class": "local-now-into-an-orm-datetime",
|
| 216 |
+
"why": "same write, moving members to a new group",
|
| 217 |
+
"evidence": "now = datetime.now()"
|
| 218 |
+
},
|
| 219 |
+
{
|
| 220 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 221 |
+
"tree": "OpenSPP2-19.0",
|
| 222 |
+
"path": "spp_attendance/controllers/controllers.py",
|
| 223 |
+
"line": 198,
|
| 224 |
+
"verdict": "true",
|
| 225 |
+
"class": "local-date-as-an-api-default",
|
| 226 |
+
"why": "the local date is the default for submitted_datetime on a controller endpoint; within the offset of midnight the attendance record carries the wrong day, which is the class OpenSPP has already fixed twice",
|
| 227 |
+
"evidence": "current_date = datetime.now().strftime(\"%Y-%m-%d\")"
|
| 228 |
+
},
|
| 229 |
+
{
|
| 230 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 231 |
+
"tree": "OpenSPP2-19.0",
|
| 232 |
+
"path": "spp_attendance/controllers/controllers.py",
|
| 233 |
+
"line": 351,
|
| 234 |
+
"verdict": "true",
|
| 235 |
+
"class": "local-date-as-an-api-default",
|
| 236 |
+
"why": "the same default on the second endpoint",
|
| 237 |
+
"evidence": "current_date = datetime.now().strftime(\"%Y-%m-%d\")"
|
| 238 |
+
},
|
| 239 |
+
{
|
| 240 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 241 |
+
"tree": "OpenSPP2-19.0",
|
| 242 |
+
"path": "spp_audit/models/spp_audit_backend.py",
|
| 243 |
+
"line": 198,
|
| 244 |
+
"verdict": "false",
|
| 245 |
+
"class": "self-consistent-utc",
|
| 246 |
+
"why": "utcnow() names an audit file and the rotation check compares it against a value from the same call; nothing aware is ever involved",
|
| 247 |
+
"evidence": "today = datetime.utcnow().strftime(\"%Y-%m-%d\")"
|
| 248 |
+
},
|
| 249 |
+
{
|
| 250 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 251 |
+
"tree": "OpenSPP2-19.0",
|
| 252 |
+
"path": "spp_audit/models/spp_audit_backend.py",
|
| 253 |
+
"line": 216,
|
| 254 |
+
"verdict": "false",
|
| 255 |
+
"class": "self-consistent-utc",
|
| 256 |
+
"why": "the same value, in the rotation check itself",
|
| 257 |
+
"evidence": "today = datetime.utcnow().strftime(\"%Y-%m-%d\")"
|
| 258 |
+
},
|
| 259 |
+
{
|
| 260 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 261 |
+
"tree": "OpenSPP2-19.0",
|
| 262 |
+
"path": "spp_cel_domain/services/cel_functions.py",
|
| 263 |
+
"line": 92,
|
| 264 |
+
"verdict": "true",
|
| 265 |
+
"class": "local-now-against-an-orm-datetime",
|
| 266 |
+
"why": "now() is exposed to CEL expressions that compare it against record datetimes read from the ORM, which are naive UTC",
|
| 267 |
+
"evidence": "return datetime.now()"
|
| 268 |
+
},
|
| 269 |
+
{
|
| 270 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 271 |
+
"tree": "OpenSPP2-19.0",
|
| 272 |
+
"path": "spp_cel_domain/services/cel_parser.py",
|
| 273 |
+
"line": 757,
|
| 274 |
+
"verdict": "false",
|
| 275 |
+
"class": "matched-inside-a-docstring",
|
| 276 |
+
"why": "a doctest example inside the docstring of hours_since, not code",
|
| 277 |
+
"evidence": ">>> two_hours_ago = datetime.now() - timedelta(hours=2)"
|
| 278 |
+
},
|
| 279 |
+
{
|
| 280 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 281 |
+
"tree": "OpenSPP2-19.0",
|
| 282 |
+
"path": "spp_cel_domain/services/cel_parser.py",
|
| 283 |
+
"line": 766,
|
| 284 |
+
"verdict": "true",
|
| 285 |
+
"class": "local-now-against-an-orm-datetime",
|
| 286 |
+
"why": "datetime.now() - datetime_value, where datetime_value arrives from an ORM field; hours_since is wrong by the offset and is used in eligibility expressions",
|
| 287 |
+
"evidence": "delta = datetime.now() - datetime_value"
|
| 288 |
+
},
|
| 289 |
+
{
|
| 290 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 291 |
+
"tree": "OpenSPP2-19.0",
|
| 292 |
+
"path": "spp_dci_server/middleware/rate_limit.py",
|
| 293 |
+
"line": 52,
|
| 294 |
+
"verdict": "true",
|
| 295 |
+
"class": "local-now-against-an-orm-datetime",
|
| 296 |
+
"why": "the rate limiter compares now against sender.last_request_minute, an ORM Datetime",
|
| 297 |
+
"evidence": "now = datetime.now()"
|
| 298 |
+
},
|
| 299 |
+
{
|
| 300 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 301 |
+
"tree": "OpenSPP2-19.0",
|
| 302 |
+
"path": "spp_encryption/models/encryption_provider.py",
|
| 303 |
+
"line": 110,
|
| 304 |
+
"verdict": "true",
|
| 305 |
+
"class": "local-time-labelled-as-utc",
|
| 306 |
+
"why": "f\"{datetime.now().isoformat(...)}Z\" appends a literal Z, which asserts UTC, to a LOCAL time, inside the created field of a linked-data cryptographic proof",
|
| 307 |
+
"evidence": "curr_datetime = f\"{datetime.now().isoformat(timespec='milliseconds')}Z\""
|
| 308 |
+
},
|
| 309 |
+
{
|
| 310 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 311 |
+
"tree": "OpenSPP2-19.0",
|
| 312 |
+
"path": "spp_farmer_registry_demo/models/seeded_farm_generator.py",
|
| 313 |
+
"line": 1127,
|
| 314 |
+
"verdict": "true",
|
| 315 |
+
"class": "local-now-into-an-orm-datetime",
|
| 316 |
+
"why": "written by raw SQL into spp_program_membership.enrollment_date, which the ORM will read back as UTC",
|
| 317 |
+
"evidence": "enrollment_dt = datetime.datetime.now()"
|
| 318 |
+
},
|
| 319 |
+
{
|
| 320 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 321 |
+
"tree": "OpenSPP2-19.0",
|
| 322 |
+
"path": "spp_grm_demo/models/generate_tickets.py",
|
| 323 |
+
"line": 922,
|
| 324 |
+
"verdict": "true",
|
| 325 |
+
"class": "local-now-into-an-orm-datetime",
|
| 326 |
+
"why": "_random_date_in_range builds demo ticket dates from local now and they are stored on records",
|
| 327 |
+
"evidence": "now = datetime.now()"
|
| 328 |
+
},
|
| 329 |
+
{
|
| 330 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 331 |
+
"tree": "OpenSPP2-19.0",
|
| 332 |
+
"path": "spp_mis_demo_v2/models/mis_demo_generator.py",
|
| 333 |
+
"line": 3388,
|
| 334 |
+
"verdict": "false",
|
| 335 |
+
"class": "display-only",
|
| 336 |
+
"why": "strftime('%Y-%m') builds a period label",
|
| 337 |
+
"evidence": "current_period = datetime.datetime.now().strftime(\"%Y-%m\")"
|
| 338 |
+
},
|
| 339 |
+
{
|
| 340 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 341 |
+
"tree": "OpenSPP2-19.0",
|
| 342 |
+
"path": "spp_mis_demo_v2/models/mis_demo_generator.py",
|
| 343 |
+
"line": 3390,
|
| 344 |
+
"verdict": "false",
|
| 345 |
+
"class": "display-only",
|
| 346 |
+
"why": "the same label for the previous three months",
|
| 347 |
+
"evidence": "(datetime.datetime.now() - datetime.timedelta(days=30 * i)).strftime(\"%Y-%m\")"
|
| 348 |
+
},
|
| 349 |
+
{
|
| 350 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 351 |
+
"tree": "OpenSPP2-19.0",
|
| 352 |
+
"path": "spp_mis_demo_v2/models/mis_demo_generator.py",
|
| 353 |
+
"line": 3740,
|
| 354 |
+
"verdict": "true",
|
| 355 |
+
"class": "local-now-into-an-orm-datetime",
|
| 356 |
+
"why": "now and expires_at go onto a credential record and the expiry is compared against the current time later",
|
| 357 |
+
"evidence": "now = datetime.datetime.now()"
|
| 358 |
+
},
|
| 359 |
+
{
|
| 360 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 361 |
+
"tree": "OpenSPP2-19.0",
|
| 362 |
+
"path": "spp_mis_demo_v2/models/seeded_volume_generator.py",
|
| 363 |
+
"line": 406,
|
| 364 |
+
"verdict": "true",
|
| 365 |
+
"class": "local-now-into-an-orm-datetime",
|
| 366 |
+
"why": "the same raw-SQL enrollment_date write in the volume generator",
|
| 367 |
+
"evidence": "enrollment_dt = datetime.datetime.now()"
|
| 368 |
+
},
|
| 369 |
+
{
|
| 370 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 371 |
+
"tree": "OpenSPP2-19.0",
|
| 372 |
+
"path": "spp_programs/models/managers/program_manager.py",
|
| 373 |
+
"line": 141,
|
| 374 |
+
"verdict": "true",
|
| 375 |
+
"class": "local-now-into-an-orm-datetime",
|
| 376 |
+
"why": "datetime.now() becomes a cycle start date on spp.cycle",
|
| 377 |
+
"evidence": "new_cycle = cm.new_cycle(\"Cycle 1\", datetime.now(), 1)"
|
| 378 |
+
},
|
| 379 |
+
{
|
| 380 |
+
"rule": "python.path-join-does-not-contain-a-path",
|
| 381 |
+
"tree": "OpenSPP2-19.0",
|
| 382 |
+
"path": "spp_drims_sl_demo/wizard/drims_demo_generator.py",
|
| 383 |
+
"line": 486,
|
| 384 |
+
"verdict": "false",
|
| 385 |
+
"class": "no-user-supplied-component",
|
| 386 |
+
"why": "file_config['filename'] comes from a list of literals declared in the same function; there is nothing here a caller can point out of the directory",
|
| 387 |
+
"evidence": "file_path = os.path.join(data_path, file_config[\"filename\"])"
|
| 388 |
+
},
|
| 389 |
+
{
|
| 390 |
+
"rule": "python.assert-is-not-validation",
|
| 391 |
+
"tree": "OCA",
|
| 392 |
+
"path": "account-financial-tools/account_cash_deposit/models/account_cash_deposit.py",
|
| 393 |
+
"line": 265,
|
| 394 |
+
"verdict": "true",
|
| 395 |
+
"class": "validates-caller-data",
|
| 396 |
+
"why": "confirm_order() is a public method reachable over XML-RPC; under -O the operation-type guard is gone and the wrong record is confirmed",
|
| 397 |
+
"evidence": "assert self.operation_type == \"order\", \"Wrong operation type\""
|
| 398 |
+
},
|
| 399 |
+
{
|
| 400 |
+
"rule": "python.assert-is-not-validation",
|
| 401 |
+
"tree": "OCA",
|
| 402 |
+
"path": "account-financial-tools/account_cash_deposit/wizards/account_cash_order_reception.py",
|
| 403 |
+
"line": 27,
|
| 404 |
+
"verdict": "true",
|
| 405 |
+
"class": "validates-caller-data",
|
| 406 |
+
"why": "the assert is about self._context, which the client supplies; under -O default_get proceeds with whatever active_model it was given",
|
| 407 |
+
"evidence": "assert self._context.get(\"active_model\") == \"account.cash.deposit\""
|
| 408 |
+
},
|
| 409 |
+
{
|
| 410 |
+
"rule": "python.assert-is-not-validation",
|
| 411 |
+
"tree": "OCA",
|
| 412 |
+
"path": "account-financial-tools/account_dashboard_banner/models/account_dashboard_banner_cell.py",
|
| 413 |
+
"line": 289,
|
| 414 |
+
"verdict": "false",
|
| 415 |
+
"class": "internal-invariant",
|
| 416 |
+
"why": "sign is set by this module a few lines up and can only be 1 or -1 unless the module is broken",
|
| 417 |
+
"evidence": "assert sign in (1, -1)"
|
| 418 |
+
},
|
| 419 |
+
{
|
| 420 |
+
"rule": "python.assert-is-not-validation",
|
| 421 |
+
"tree": "OCA",
|
| 422 |
+
"path": "account-financial-tools/account_move_template/wizard/account_move_template_run.py",
|
| 423 |
+
"line": 117,
|
| 424 |
+
"verdict": "true",
|
| 425 |
+
"class": "validates-caller-data",
|
| 426 |
+
"why": "self.overwrite is a text field a user types; the assert is the type check on literal_eval's result, and the except clause catches AssertionError explicitly -- this IS the validation, and under -O it disappears and .keys() raises AttributeError instead of the ValidationError the code means to raise",
|
| 427 |
+
"evidence": "assert isinstance(overwrite_vals, dict)"
|
| 428 |
+
},
|
| 429 |
+
{
|
| 430 |
+
"rule": "python.assert-is-not-validation",
|
| 431 |
+
"tree": "OCA",
|
| 432 |
+
"path": "server-tools/base_time_parameter/models/base.py",
|
| 433 |
+
"line": 19,
|
| 434 |
+
"verdict": "true",
|
| 435 |
+
"class": "validates-caller-data",
|
| 436 |
+
"why": "get_time_parameter is public and date arrives from the caller, possibly resolved off a field by name; under -O a wrong type reaches the search",
|
| 437 |
+
"evidence": "assert type(date) is datetime.date or date is None, \"Wrong date\""
|
| 438 |
+
},
|
| 439 |
+
{
|
| 440 |
+
"rule": "python.assert-is-not-validation",
|
| 441 |
+
"tree": "OCA",
|
| 442 |
+
"path": "server-tools/jsonifier/models/ir_exports_resolver.py",
|
| 443 |
+
"line": 42,
|
| 444 |
+
"verdict": "false",
|
| 445 |
+
"class": "re-enforced-on-the-next-line",
|
| 446 |
+
"why": "the assert guards a zip(..., strict=True) one line below, which raises ValueError on the same mismatch; removing the assert changes the exception type and nothing else",
|
| 447 |
+
"evidence": "assert len(param) == len(records)"
|
| 448 |
+
},
|
| 449 |
+
{
|
| 450 |
+
"rule": "python.assert-is-not-validation",
|
| 451 |
+
"tree": "OCA",
|
| 452 |
+
"path": "server-tools/jsonifier/models/utils.py",
|
| 453 |
+
"line": 3,
|
| 454 |
+
"verdict": "false",
|
| 455 |
+
"class": "internal-invariant",
|
| 456 |
+
"why": "a module-private helper's contract with its only callers",
|
| 457 |
+
"evidence": "assert isinstance(parser, list)"
|
| 458 |
+
},
|
| 459 |
+
{
|
| 460 |
+
"rule": "python.decimal-for-money-not-float",
|
| 461 |
+
"tree": "OCA",
|
| 462 |
+
"path": "account-financial-tools/account_payroll_sheet_import/wizard/payroll_import_wizard.py",
|
| 463 |
+
"line": 110,
|
| 464 |
+
"verdict": "false",
|
| 465 |
+
"class": "odoo-monetary-is-float",
|
| 466 |
+
"why": "a spreadsheet cell becomes an accounting amount on a Monetary field, which the ORM defines as float",
|
| 467 |
+
"evidence": "amount = float(raw_value)"
|
| 468 |
+
},
|
| 469 |
+
{
|
| 470 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 471 |
+
"tree": "OCA",
|
| 472 |
+
"path": "account-financial-tools/account_fiscal_year_auto_create/models/account_fiscal_year.py",
|
| 473 |
+
"line": 23,
|
| 474 |
+
"verdict": "true",
|
| 475 |
+
"class": "local-date-against-an-orm-date",
|
| 476 |
+
"why": "last_fiscal_year.date_to < datetime.now().date() in a cron: the ORM Date is UTC-based and now().date() is the server's LOCAL date, so the fiscal year is created on the wrong side of midnight on any non-UTC server. fields.Date.context_today is the idiom",
|
| 477 |
+
"evidence": "last_fiscal_year.date_to < datetime.now().date() + relativedelta(days=1)"
|
| 478 |
+
},
|
| 479 |
+
{
|
| 480 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 481 |
+
"tree": "OCA",
|
| 482 |
+
"path": "account-financial-tools/purchase_unreconciled/models/account_move_line.py",
|
| 483 |
+
"line": 45,
|
| 484 |
+
"verdict": "true",
|
| 485 |
+
"class": "local-now-into-an-orm-datetime",
|
| 486 |
+
"why": "the default accounting date for an automatic writeoff move",
|
| 487 |
+
"evidence": "move_date = writeoff_vals.get(\"date\", datetime.now())"
|
| 488 |
+
},
|
| 489 |
+
{
|
| 490 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 491 |
+
"tree": "OCA",
|
| 492 |
+
"path": "server-tools/auditlog/models/autovacuum.py",
|
| 493 |
+
"line": 25,
|
| 494 |
+
"verdict": "true",
|
| 495 |
+
"class": "local-now-against-an-orm-datetime",
|
| 496 |
+
"why": "deadline is built from local now and compared against create_date, which is UTC; the autovacuum cron deletes logs early or late by the server's offset",
|
| 497 |
+
"evidence": "deadline = datetime.now() - timedelta(days=days)"
|
| 498 |
+
},
|
| 499 |
+
{
|
| 500 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 501 |
+
"tree": "OCA",
|
| 502 |
+
"path": "server-tools/auto_backup/models/db_backup.py",
|
| 503 |
+
"line": 146,
|
| 504 |
+
"verdict": "false",
|
| 505 |
+
"class": "display-only",
|
| 506 |
+
"why": "the timestamp names a backup file",
|
| 507 |
+
"evidence": "filename = self.filename(datetime.now(), ext=rec.backup_format)"
|
| 508 |
+
},
|
| 509 |
+
{
|
| 510 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 511 |
+
"tree": "OCA",
|
| 512 |
+
"path": "server-tools/auto_backup/models/db_backup.py",
|
| 513 |
+
"line": 171,
|
| 514 |
+
"verdict": "false",
|
| 515 |
+
"class": "display-only",
|
| 516 |
+
"why": "the same, on the sftp path",
|
| 517 |
+
"evidence": "filename = self.filename(datetime.now(), ext=rec.backup_format)"
|
| 518 |
+
},
|
| 519 |
+
{
|
| 520 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 521 |
+
"tree": "OCA",
|
| 522 |
+
"path": "server-tools/auto_backup/models/db_backup.py",
|
| 523 |
+
"line": 218,
|
| 524 |
+
"verdict": "false",
|
| 525 |
+
"class": "self-consistent-local",
|
| 526 |
+
"why": "cleanup() builds the oldest filename from the same local clock the filenames were written with, so the two agree",
|
| 527 |
+
"evidence": "now = datetime.now()"
|
| 528 |
+
},
|
| 529 |
+
{
|
| 530 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 531 |
+
"tree": "OCA",
|
| 532 |
+
"path": "server-tools/excel_import_export/models/xlsx_export.py",
|
| 533 |
+
"line": 268,
|
| 534 |
+
"verdict": "false",
|
| 535 |
+
"class": "display-only",
|
| 536 |
+
"why": "a temp filename uniquifier",
|
| 537 |
+
"evidence": "stamp = dt.utcnow().strftime(\"%H%M%S%f\")[:-3]"
|
| 538 |
+
},
|
| 539 |
+
{
|
| 540 |
+
"rule": "python.path-join-does-not-contain-a-path",
|
| 541 |
+
"tree": "OCA",
|
| 542 |
+
"path": "server-tools/auto_backup/models/db_backup.py",
|
| 543 |
+
"line": 154,
|
| 544 |
+
"verdict": "false",
|
| 545 |
+
"class": "no-user-supplied-component",
|
| 546 |
+
"why": "rec.folder is configuration and filename is generated from a timestamp; neither is a path a caller can traverse with",
|
| 547 |
+
"evidence": "with open(os.path.join(rec.folder, filename), \"wb\") as destiny:"
|
| 548 |
+
},
|
| 549 |
+
{
|
| 550 |
+
"rule": "python.path-join-does-not-contain-a-path",
|
| 551 |
+
"tree": "OCA",
|
| 552 |
+
"path": "server-tools/auto_backup/models/db_backup.py",
|
| 553 |
+
"line": 186,
|
| 554 |
+
"verdict": "false",
|
| 555 |
+
"class": "no-user-supplied-component",
|
| 556 |
+
"why": "the same join on the sftp path",
|
| 557 |
+
"evidence": "os.path.join(rec.folder, filename), \"wb\""
|
| 558 |
+
},
|
| 559 |
+
{
|
| 560 |
+
"rule": "python.path-join-does-not-contain-a-path",
|
| 561 |
+
"tree": "OCA",
|
| 562 |
+
"path": "server-tools/upgrade_analysis/models/upgrade_analysis.py",
|
| 563 |
+
"line": 106,
|
| 564 |
+
"verdict": "false",
|
| 565 |
+
"class": "no-user-supplied-component",
|
| 566 |
+
"why": "full_path is built from get_module_path and filename is internal",
|
| 567 |
+
"evidence": "logfile = os.path.join(full_path, filename)"
|
| 568 |
+
}
|
| 569 |
+
],
|
| 570 |
+
"note": "Every finding the composition ADDS to an Odoo review, read by hand in its own file. These 8 rules were previously unreachable from the odoo stack: selection was strict equality on the stack field, so an Odoo module got the 19 ORM rules and none of these.\n\nThe verdict test for the datetime rule, which is 32 of the 50: an Odoo Datetime field is naive UTC by contract, so a naive LOCAL datetime that reaches one -- written to it, compared against it, or handed to something that does -- is a true positive, and one that is only formatted for display or used to name a file is not. Note what that makes these: latent on a server clocked to UTC, wrong by the offset on any other. OpenSPP's own history carries two fixes of exactly this class, which is why it is graded true rather than theoretical.\n\nThree rules fired and were never right: decimal-for-money-not-float (0 of 3), path-join-does-not-contain-a-path (0 of 4), and eval-and-pickle-execute-their-input (0 of 1). The first has a reason rather than a sample size behind it -- Odoo's Monetary field IS a float, so the rule's remedy is not the Odoo idiom -- and the other two have four findings between them, which is not enough to conclude anything about a checker. All three are named in the README rather than quietly dropped.\n\nThree more never fired at all on either tree: mutable-default-argument, bare-except-catches-the-exit and subprocess-shell-true-is-injection. Silence is not a pass.\n\nTwo of the 50 are the same checker defect: the pattern scanner strips # comments before matching and does not strip docstrings, so `eval()` named in prose and a `datetime.now()` inside a doctest both read as code. That is a fault in scan_source, not in either rule.\n\nOne finding is suppressed and should not be. The consent-receipt timestamp sits inside a return statement that carries a `# nosemgrep: odoo-sudo-without-context` marker four lines below it, and suppression reads any marker in the statement without asking which rule it names. It is counted apart here, as fieldtest counts it."
|
| 571 |
+
}
|
|
@@ -1,8 +1,15 @@
|
|
| 1 |
-
"""Generates the validation rule corpus JSON
|
| 2 |
|
| 3 |
The corpus is the single source of truth. System prompts and the RAG index are
|
| 4 |
both DERIVED from it (corpus.py prompt / corpus.py index) so they cannot drift.
|
| 5 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
Edit the rules here, then: python build_corpus.py && python ../bin/corpus.py validate
|
| 7 |
"""
|
| 8 |
import io
|
|
@@ -21,15 +28,16 @@ import evidence # noqa: E402 -- one hashing implementation, not two
|
|
| 21 |
# must be one list, not one here and another in the validator.
|
| 22 |
from corpus import ADVISORY_LIKELIHOODS, LIKELIHOOD_DEFAULT # noqa: E402
|
| 23 |
|
| 24 |
-
# The upstream documentation version each
|
|
|
|
|
|
|
|
|
|
| 25 |
# Recorded rather than assumed: a rule verified on 18.0 is not thereby
|
| 26 |
# verified on 16.0, and the context layer needs to be able to say so when it
|
| 27 |
# meets a module pinned to another series.
|
| 28 |
CHECKED_AGAINST = {
|
| 29 |
"odoo": "18.0",
|
| 30 |
"python": "3.12",
|
| 31 |
-
"typescript": "5.x",
|
| 32 |
-
"javascript": "ES2023",
|
| 33 |
}
|
| 34 |
|
| 35 |
ODOO = [
|
|
@@ -443,273 +451,6 @@ except UserError as exc:
|
|
| 443 |
},
|
| 444 |
]
|
| 445 |
|
| 446 |
-
TYPESCRIPT = [
|
| 447 |
-
{
|
| 448 |
-
"id": "typescript.types-are-erased-at-runtime",
|
| 449 |
-
"severity": "critical",
|
| 450 |
-
"title": "Static types guarantee nothing about external data",
|
| 451 |
-
"rule": "Every value crossing a boundary must be parsed by a runtime schema. A type assertion on external data is a lie the compiler cannot catch.",
|
| 452 |
-
"why": "Interfaces and type annotations are erased during compilation. Casting an API response with 'as User' produces zero runtime checks, so a changed or malformed payload propagates as undefined deep inside unrelated code, far from its origin.",
|
| 453 |
-
"enforcement_layer": "runtime-parse",
|
| 454 |
-
"bypassed_by": ["type assertions (as T)", "any", "unchecked JSON.parse"],
|
| 455 |
-
"wrong": """const user = await res.json() as User;""",
|
| 456 |
-
"correct": """const UserSchema = z.object({ id: z.number(), name: z.string() });
|
| 457 |
-
type User = z.infer<typeof UserSchema>;
|
| 458 |
-
|
| 459 |
-
const user = UserSchema.parse(await res.json());""",
|
| 460 |
-
"tags": ["zod", "boundary", "type-erasure"],
|
| 461 |
-
},
|
| 462 |
-
{
|
| 463 |
-
"id": "typescript.derive-type-from-schema",
|
| 464 |
-
"severity": "high",
|
| 465 |
-
"title": "Derive the type from the schema, never maintain both",
|
| 466 |
-
"rule": "Write the schema first and derive the static type with z.infer. Never hand-write an interface alongside a schema for the same data.",
|
| 467 |
-
"why": "Two hand-maintained definitions drift. The compiler cannot detect the drift because each is internally consistent, so the type says one thing while the parser enforces another and the mismatch only appears at runtime.",
|
| 468 |
-
"enforcement_layer": "compile-time",
|
| 469 |
-
"bypassed_by": [],
|
| 470 |
-
"wrong": """interface Order { id: number; total: number; }
|
| 471 |
-
const OrderSchema = z.object({ id: z.number(), total: z.number(), currency: z.string() });""",
|
| 472 |
-
"correct": """const OrderSchema = z.object({
|
| 473 |
-
id: z.number(),
|
| 474 |
-
total: z.number(),
|
| 475 |
-
currency: z.string(),
|
| 476 |
-
});
|
| 477 |
-
type Order = z.infer<typeof OrderSchema>;""",
|
| 478 |
-
"tags": ["zod", "single-source-of-truth", "drift"],
|
| 479 |
-
},
|
| 480 |
-
{
|
| 481 |
-
"id": "typescript.odoo-returns-false-for-every-empty-field",
|
| 482 |
-
"severity": "critical",
|
| 483 |
-
"title": "Odoo JSON-RPC returns false for every empty field",
|
| 484 |
-
"rule": "Model every optional Odoo scalar as a union with literal false, normalised through one shared helper. This applies to Char, Text, Date, Datetime, Binary and Many2one alike, not only to relations.",
|
| 485 |
-
"why": "Odoo returns boolean false for any empty value, never null, never an empty string. Typing an optional email as string().nullable() throws at the parse boundary the first time a partner has no email, which is ordinary production data rather than an edge case.",
|
| 486 |
-
"enforcement_layer": "runtime-parse",
|
| 487 |
-
"bypassed_by": [],
|
| 488 |
-
"wrong": """const PartnerSchema = z.object({
|
| 489 |
-
email: z.string().nullable(),
|
| 490 |
-
parent_id: z.array(z.number()),
|
| 491 |
-
});""",
|
| 492 |
-
"correct": """const odooNullable = <T extends z.ZodTypeAny>(inner: T) =>
|
| 493 |
-
z.union([inner, z.literal(false)]).transform((v) => (v === false ? null : v));
|
| 494 |
-
|
| 495 |
-
const PartnerSchema = z.object({
|
| 496 |
-
id: z.number().int(),
|
| 497 |
-
name: z.string(),
|
| 498 |
-
email: odooNullable(z.string().email()),
|
| 499 |
-
parent_id: odooNullable(z.tuple([z.number().int(), z.string()])),
|
| 500 |
-
});""",
|
| 501 |
-
"tags": ["odoo", "json-rpc", "boundary", "zod"],
|
| 502 |
-
# .nullable() on an Odoo scalar means the author forgot that empty comes back as false.
|
| 503 |
-
"violation_pattern": r"z\.(?:string|number|date|coerce)[^\n]*\.nullable\(\)",
|
| 504 |
-
"satisfied_pattern": r"odooNullable|z\.literal\(false\)",
|
| 505 |
-
},
|
| 506 |
-
{
|
| 507 |
-
"id": "typescript.odoo-many2one-is-a-tuple",
|
| 508 |
-
"severity": "high",
|
| 509 |
-
"title": "A set Many2one is a two-element tuple, not an array of ids",
|
| 510 |
-
"rule": "Model a populated Many2one as z.tuple([z.number(), z.string()]) holding id and display_name. Model an empty one as false.",
|
| 511 |
-
"why": "Odoo read() returns [id, display_name] for a set Many2one. Typing it as an array of numbers parses the id but discards display_name and fails the moment the schema meets real data, because element one is a string.",
|
| 512 |
-
"enforcement_layer": "runtime-parse",
|
| 513 |
-
"bypassed_by": [],
|
| 514 |
-
"wrong": """parent_id: z.array(z.number())""",
|
| 515 |
-
"correct": """parent_id: odooNullable(z.tuple([z.number().int(), z.string()]))""",
|
| 516 |
-
"tags": ["odoo", "json-rpc", "many2one"],
|
| 517 |
-
},
|
| 518 |
-
{
|
| 519 |
-
"id": "typescript.odoo-read-returns-an-array",
|
| 520 |
-
"severity": "medium",
|
| 521 |
-
"title": "Odoo read returns an array even for one id",
|
| 522 |
-
"rule": "Parse the response of read() with z.array(Schema) and index into it, rather than parsing it as a single object.",
|
| 523 |
-
"why": "read() always returns a list of dictionaries. Parsing it directly against an object schema fails, and defensively coercing with Array.isArray hides whether the call actually returned the record or an empty list for a missing id.",
|
| 524 |
-
"enforcement_layer": "runtime-parse",
|
| 525 |
-
"bypassed_by": [],
|
| 526 |
-
"wrong": """const partner = PartnerSchema.parse(await callOdoo('res.partner', 'read', [id]));""",
|
| 527 |
-
"correct": """const rows = z.array(PartnerSchema).parse(
|
| 528 |
-
await callOdoo('res.partner', 'read', [id]),
|
| 529 |
-
);
|
| 530 |
-
if (rows.length === 0) throw new Error(`res.partner ${id} not found or not readable`);
|
| 531 |
-
const partner = rows[0];""",
|
| 532 |
-
"tags": ["odoo", "json-rpc", "read"],
|
| 533 |
-
},
|
| 534 |
-
{
|
| 535 |
-
"id": "typescript.unknown-not-any-at-boundaries",
|
| 536 |
-
"severity": "high",
|
| 537 |
-
"title": "Use unknown at boundaries, never any",
|
| 538 |
-
"rule": "Type every value that arrives unparsed as unknown: the body of an incoming webhook, a request payload, a handler parameter, the result of JSON.parse, anything from an untyped import. Reserve any for nothing.",
|
| 539 |
-
"why": "any silently disables checking for every expression derived from it, so one any on a webhook handler's payload removes type safety from the whole call chain downstream, and the compiler reports nothing. unknown refuses every use until the value has been parsed or narrowed, which is the behaviour that was wanted from the annotation in the first place.",
|
| 540 |
-
"enforcement_layer": "compile-time",
|
| 541 |
-
"bypassed_by": ["explicit any", "implicit any from untyped imports"],
|
| 542 |
-
"wrong": """function handle(payload: any) { return payload.order.total; }""",
|
| 543 |
-
"correct": """function handle(payload: unknown) {
|
| 544 |
-
const { order } = WebhookSchema.parse(payload);
|
| 545 |
-
return order.total;
|
| 546 |
-
}""",
|
| 547 |
-
"tags": ["any", "unknown", "boundary", "webhook", "payload", "request-body",
|
| 548 |
-
"handler", "external-input"],
|
| 549 |
-
},
|
| 550 |
-
{
|
| 551 |
-
"id": "typescript.safeparse-when-failure-is-expected",
|
| 552 |
-
"severity": "medium",
|
| 553 |
-
"title": "Choose parse or safeParse deliberately",
|
| 554 |
-
"rule": "Use safeParse where a malformed value is expected and recoverable; use parse only where throwing is the correct response. State which and why.",
|
| 555 |
-
"why": "parse throws a ZodError that an unprepared caller turns into a 500 and an opaque stack trace. safeParse returns a discriminated result carrying the field-level issues, which is what a user-facing error or a per-row import skip actually needs.",
|
| 556 |
-
"enforcement_layer": "runtime-parse",
|
| 557 |
-
"bypassed_by": [],
|
| 558 |
-
"wrong": """const row = RowSchema.parse(csvRow); // aborts the whole import on row 4000""",
|
| 559 |
-
"correct": """const result = RowSchema.safeParse(csvRow);
|
| 560 |
-
if (!result.success) {
|
| 561 |
-
errors.push({ line, issues: result.error.issues });
|
| 562 |
-
continue;
|
| 563 |
-
}
|
| 564 |
-
const row = result.data;""",
|
| 565 |
-
"tags": ["zod", "error-handling"],
|
| 566 |
-
},
|
| 567 |
-
{
|
| 568 |
-
"id": "typescript.validate-env-at-startup",
|
| 569 |
-
"severity": "high",
|
| 570 |
-
"title": "Parse environment variables once at startup",
|
| 571 |
-
"rule": "Validate the whole environment against a schema at process start and export the parsed object. Never read process.env deep inside application code.",
|
| 572 |
-
"why": "process.env values are string or undefined and are unchecked. A missing variable read lazily inside a rarely-exercised branch fails in production at the worst moment, whereas a startup parse turns the same fault into a deployment that refuses to boot.",
|
| 573 |
-
"enforcement_layer": "startup",
|
| 574 |
-
"bypassed_by": [],
|
| 575 |
-
"wrong": """const url = process.env.ODOO_URL; // string | undefined, used 40 files away""",
|
| 576 |
-
"correct": """const EnvSchema = z.object({
|
| 577 |
-
ODOO_URL: z.string().url(),
|
| 578 |
-
ODOO_DB: z.string().min(1),
|
| 579 |
-
PORT: z.coerce.number().int().positive().default(3000),
|
| 580 |
-
});
|
| 581 |
-
export const env = EnvSchema.parse(process.env);""",
|
| 582 |
-
"tags": ["config", "startup", "fail-fast"],
|
| 583 |
-
},
|
| 584 |
-
{
|
| 585 |
-
"id": "typescript.discriminated-union-for-variants",
|
| 586 |
-
"severity": "medium",
|
| 587 |
-
"title": "Use discriminatedUnion for tagged variant payloads",
|
| 588 |
-
"rule": "When a payload has a literal tag field selecting its shape, use z.discriminatedUnion on that field rather than z.union.",
|
| 589 |
-
"why": "A plain union tries every member and reports the failure of all of them, producing an unreadable error and slower parsing. discriminatedUnion selects the branch by tag, reports issues only for the intended shape, and narrows correctly in TypeScript afterwards.",
|
| 590 |
-
"enforcement_layer": "runtime-parse",
|
| 591 |
-
"bypassed_by": [],
|
| 592 |
-
"wrong": """z.union([PaymentSucceeded, PaymentFailed, PaymentPending])""",
|
| 593 |
-
"correct": """z.discriminatedUnion('status', [
|
| 594 |
-
z.object({ status: z.literal('succeeded'), chargeId: z.string() }),
|
| 595 |
-
z.object({ status: z.literal('failed'), reason: z.string() }),
|
| 596 |
-
z.object({ status: z.literal('pending') }),
|
| 597 |
-
])""",
|
| 598 |
-
"tags": ["zod", "unions", "narrowing"],
|
| 599 |
-
},
|
| 600 |
-
{
|
| 601 |
-
"id": "typescript.brand-validated-primitives",
|
| 602 |
-
"severity": "medium",
|
| 603 |
-
"title": "Brand validated primitives so they cannot be swapped for raw ones",
|
| 604 |
-
"rule": "Give checked primitives a branded type so a validated value is not assignable from an arbitrary string or number.",
|
| 605 |
-
"why": "Without branding, every string is assignable to every string-typed parameter, so a raw user input can be passed where a validated email or a trusted id is required and the compiler accepts it. Branding makes that a compile error.",
|
| 606 |
-
"enforcement_layer": "compile-time",
|
| 607 |
-
"bypassed_by": ["casting through as"],
|
| 608 |
-
"wrong": """function send(to: string) { /* any string at all */ }""",
|
| 609 |
-
"correct": """const Email = z.string().email().brand<'Email'>();
|
| 610 |
-
type Email = z.infer<typeof Email>;
|
| 611 |
-
|
| 612 |
-
function send(to: Email) { /* only a parsed Email */ }""",
|
| 613 |
-
"tags": ["branding", "nominal-typing"],
|
| 614 |
-
},
|
| 615 |
-
]
|
| 616 |
-
|
| 617 |
-
JAVASCRIPT = [
|
| 618 |
-
{
|
| 619 |
-
"id": "javascript.json-parse-returns-any",
|
| 620 |
-
"severity": "critical",
|
| 621 |
-
"title": "JSON.parse output is unchecked",
|
| 622 |
-
"rule": "Never use the result of JSON.parse directly. Pass it through a schema or an explicit shape check first.",
|
| 623 |
-
"why": "JSON.parse succeeds for any syntactically valid JSON, including null, a bare number, or an object missing every field you expect. Property access then yields undefined, which propagates through arithmetic as NaN and through templates as the literal string undefined.",
|
| 624 |
-
"enforcement_layer": "runtime-parse",
|
| 625 |
-
"bypassed_by": [],
|
| 626 |
-
"wrong": """const cfg = JSON.parse(raw);
|
| 627 |
-
startServer(cfg.port);""",
|
| 628 |
-
"correct": """const cfg = ConfigSchema.parse(JSON.parse(raw));
|
| 629 |
-
startServer(cfg.port);""",
|
| 630 |
-
"tags": ["json", "boundary"],
|
| 631 |
-
},
|
| 632 |
-
{
|
| 633 |
-
"id": "javascript.loose-equality-and-falsy-defaults",
|
| 634 |
-
"severity": "high",
|
| 635 |
-
"title": "Falsy defaults discard legitimate zero and empty string",
|
| 636 |
-
"rule": "Use the nullish coalescing operator for defaults and strict equality for comparisons. Reserve the logical OR operator for genuine boolean logic.",
|
| 637 |
-
"why": "The OR operator treats 0, empty string and false as absent, so a quantity of 0 or an intentionally blank note is silently replaced by the default. This is the most common way a validated value is destroyed after it has passed validation.",
|
| 638 |
-
"enforcement_layer": "runtime-parse",
|
| 639 |
-
"bypassed_by": [],
|
| 640 |
-
"wrong": """const qty = input.qty || 1; // qty of 0 becomes 1
|
| 641 |
-
if (status == null) { } // also matches undefined by design, but == elsewhere surprises""",
|
| 642 |
-
"correct": """const qty = input.qty ?? 1; // only null or undefined fall back
|
| 643 |
-
if (status === null || status === undefined) { }""",
|
| 644 |
-
"tags": ["coercion", "defaults", "silent-failure"],
|
| 645 |
-
},
|
| 646 |
-
{
|
| 647 |
-
"id": "javascript.number-parsing-is-lossy",
|
| 648 |
-
"severity": "high",
|
| 649 |
-
"title": "Coercing user input to a number accepts nonsense",
|
| 650 |
-
"rule": "Validate numeric input with an explicit check for finiteness rather than relying on Number, parseInt or the unary plus.",
|
| 651 |
-
"why": "parseInt('12abc') returns 12, Number('') returns 0, and Number(' ') returns 0 as well. Each converts invalid input into a plausible number that passes every downstream check and corrupts totals rather than raising an error.",
|
| 652 |
-
"enforcement_layer": "runtime-parse",
|
| 653 |
-
"bypassed_by": [],
|
| 654 |
-
"wrong": """const qty = parseInt(req.body.qty);""",
|
| 655 |
-
"correct": """const qty = Number(req.body.qty);
|
| 656 |
-
if (!Number.isFinite(qty) || !Number.isInteger(qty) || qty <= 0) {
|
| 657 |
-
throw new Error(`qty must be a positive integer, received: ${req.body.qty}`);
|
| 658 |
-
}""",
|
| 659 |
-
"tags": ["numbers", "coercion", "input"],
|
| 660 |
-
},
|
| 661 |
-
{
|
| 662 |
-
"id": "javascript.async-errors-need-explicit-handling",
|
| 663 |
-
"severity": "critical",
|
| 664 |
-
"title": "A rejected promise in a callback is not caught by the surrounding try",
|
| 665 |
-
"rule": "Await every promise you create, or attach a catch to it. Never pass an async function where a synchronous callback is expected.",
|
| 666 |
-
"why": "An async callback passed to forEach or to an event emitter returns a promise nobody holds. A rejection inside it escapes the enclosing try/catch entirely and becomes an unhandled rejection, which terminates the process on modern Node and silently skips work before that.",
|
| 667 |
-
"enforcement_layer": "runtime-parse",
|
| 668 |
-
"bypassed_by": [],
|
| 669 |
-
"wrong": """rows.forEach(async (row) => {
|
| 670 |
-
await save(row); // rejections escape, loop does not wait
|
| 671 |
-
});""",
|
| 672 |
-
"correct": """await Promise.all(rows.map((row) => save(row)));
|
| 673 |
-
// or, when order and back-pressure matter:
|
| 674 |
-
for (const row of rows) {
|
| 675 |
-
await save(row);
|
| 676 |
-
}""",
|
| 677 |
-
"tags": ["async", "promises", "error-handling"],
|
| 678 |
-
},
|
| 679 |
-
{
|
| 680 |
-
"id": "javascript.object-injection-via-user-keys",
|
| 681 |
-
"severity": "critical",
|
| 682 |
-
"title": "Never index an object with an unvalidated user-supplied key",
|
| 683 |
-
"rule": "Check a user-supplied key against an allowlist, or use a Map, before using it to read or write a property.",
|
| 684 |
-
"why": "A key of __proto__ or constructor reaches the prototype chain. Writing through it pollutes every object in the process, which turns a validation gap into remote code execution in the worst case and into corrupted defaults everywhere in the mildest.",
|
| 685 |
-
"enforcement_layer": "runtime-parse",
|
| 686 |
-
"bypassed_by": [],
|
| 687 |
-
"wrong": """target[req.body.key] = req.body.value;""",
|
| 688 |
-
"correct": """const ALLOWED = new Set(['name', 'email', 'phone']);
|
| 689 |
-
if (!ALLOWED.has(req.body.key)) {
|
| 690 |
-
throw new Error(`Unsupported field: ${req.body.key}`);
|
| 691 |
-
}
|
| 692 |
-
target[req.body.key] = req.body.value;""",
|
| 693 |
-
"tags": ["security", "prototype-pollution", "input"],
|
| 694 |
-
},
|
| 695 |
-
{
|
| 696 |
-
"id": "javascript.date-parsing-is-implementation-defined",
|
| 697 |
-
"severity": "medium",
|
| 698 |
-
"title": "The Date constructor accepts ambiguous strings",
|
| 699 |
-
"rule": "Parse dates from an explicit ISO-8601 string and reject anything that produces an invalid date. Never feed arbitrary user text to new Date.",
|
| 700 |
-
"why": "new Date('01/02/2026') is January the second in one locale's reading and the second of January in another, and non-ISO formats are implementation-defined across engines. new Date('nonsense') returns an Invalid Date object rather than throwing, so the failure travels onward as NaN.",
|
| 701 |
-
"enforcement_layer": "runtime-parse",
|
| 702 |
-
"bypassed_by": [],
|
| 703 |
-
"wrong": """const when = new Date(req.body.date);""",
|
| 704 |
-
"correct": """const when = new Date(req.body.date);
|
| 705 |
-
if (Number.isNaN(when.getTime())) {
|
| 706 |
-
throw new Error(`Invalid date: ${req.body.date}`);
|
| 707 |
-
}""",
|
| 708 |
-
"tags": ["dates", "coercion", "locale"],
|
| 709 |
-
},
|
| 710 |
-
]
|
| 711 |
-
|
| 712 |
-
|
| 713 |
PYTHON = [
|
| 714 |
{
|
| 715 |
"id": "python.assert-is-not-validation",
|
|
@@ -869,8 +610,6 @@ ODOO_ONCHANGE = "https://www.odoo.com/documentation/18.0/developer/tutorials/ser
|
|
| 869 |
# page does, and says the thing the rule says.
|
| 870 |
ODOO_GUIDELINES = "https://www.odoo.com/documentation/18.0/contributing/development/coding_guidelines.html"
|
| 871 |
PG_CREATE_TABLE = "https://www.postgresql.org/docs/current/ddl-constraints.html"
|
| 872 |
-
# The Zod API reference, not the homepage. Four rules cited zod.dev/, which
|
| 873 |
-
# supports nothing: a landing page is not provenance.
|
| 874 |
PY_DOCS = "https://docs.python.org/3"
|
| 875 |
PY_ASSERT = PY_DOCS + "/reference/simple_stmts.html#the-assert-statement"
|
| 876 |
PY_DEFAULTS = PY_DOCS + "/tutorial/controlflow.html#default-argument-values"
|
|
@@ -881,11 +620,6 @@ PY_OSPATH = PY_DOCS + "/library/os.path.html#os.path.join"
|
|
| 881 |
PY_DECIMAL = PY_DOCS + "/library/decimal.html"
|
| 882 |
PY_DATETIME = PY_DOCS + "/library/datetime.html"
|
| 883 |
|
| 884 |
-
ZOD = "https://zod.dev/api"
|
| 885 |
-
TS_HANDBOOK = "https://www.typescriptlang.org/docs/handbook/2/everyday-types.html"
|
| 886 |
-
NODE_PROCESS = "https://nodejs.org/api/process.html"
|
| 887 |
-
MDN = "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference"
|
| 888 |
-
|
| 889 |
ENRICH = {
|
| 890 |
# ---- Odoo -------------------------------------------------------------
|
| 891 |
# Both decorators are specified on the ORM reference page, which is where the
|
|
@@ -1038,53 +772,34 @@ ENRICH = {
|
|
| 1038 |
"source": ODOO_ORM,
|
| 1039 |
},
|
| 1040 |
|
| 1041 |
-
# ---- TypeScript -------------------------------------------------------
|
| 1042 |
-
"typescript.types-are-erased-at-runtime": {
|
| 1043 |
-
"violation_pattern": r"\.json\(\)\s+as\s+\w+|as\s+unknown\s+as\s+\w+",
|
| 1044 |
-
# Type erasure is a TypeScript fact. Zod is one answer to it, not the
|
| 1045 |
-
# thing that documents it.
|
| 1046 |
-
"source": TS_HANDBOOK,
|
| 1047 |
-
},
|
| 1048 |
-
"typescript.derive-type-from-schema": {
|
| 1049 |
-
"violation_pattern": r"interface\s+\w+\s*\{[\s\S]*?\}[\s\S]*?z\.object\(",
|
| 1050 |
-
"source": ZOD,
|
| 1051 |
-
},
|
| 1052 |
-
"typescript.odoo-returns-false-for-every-empty-field": {
|
| 1053 |
-
"violation_pattern": r"z\.(?:string|number|date|coerce)[^\n]*\.nullable\(\)",
|
| 1054 |
-
"satisfied_pattern": r"odooNullable|z\.literal\(false\)",
|
| 1055 |
-
# What comes back over JSON-RPC is the External API's subject, not the
|
| 1056 |
-
# ORM reference's.
|
| 1057 |
-
"source": ODOO_EXTERNAL_API,
|
| 1058 |
-
},
|
| 1059 |
-
"typescript.odoo-many2one-is-a-tuple": {
|
| 1060 |
-
"violation_pattern": r"_id\s*:\s*(?:\w+\()?z\.array\(\s*z\.number",
|
| 1061 |
-
"source": ODOO_EXTERNAL_API,
|
| 1062 |
-
},
|
| 1063 |
-
"typescript.odoo-read-returns-an-array": {
|
| 1064 |
-
# Must be an Odoo read() call specifically. Without this the pattern fired
|
| 1065 |
-
# on any Schema.parse(await ...), including correct non-Odoo fetch parsing.
|
| 1066 |
-
"violation_pattern": r"\w+Schema\.(?:safe)?[Pp]arse\(\s*await[^)]*(?:callOdoo|jsonrpc|'read'|\"read\")",
|
| 1067 |
-
"satisfied_pattern": r"z\.array\(",
|
| 1068 |
-
"source": ODOO_EXTERNAL_API,
|
| 1069 |
-
},
|
| 1070 |
-
"typescript.unknown-not-any-at-boundaries": {
|
| 1071 |
-
"violation_pattern": r":\s*any\b",
|
| 1072 |
-
"source": "https://www.typescriptlang.org/docs/handbook/2/everyday-types.html",
|
| 1073 |
-
},
|
| 1074 |
-
"typescript.validate-env-at-startup": {
|
| 1075 |
-
"violation_pattern": r"process\.env\.[A-Z][A-Z0-9_]*",
|
| 1076 |
-
# The claim is about process.env being string | undefined, which is
|
| 1077 |
-
# Node's documentation, not Zod's.
|
| 1078 |
-
"source": NODE_PROCESS,
|
| 1079 |
-
},
|
| 1080 |
-
"typescript.discriminated-union-for-variants": {
|
| 1081 |
-
"violation_pattern": r"z\.union\(\[\s*[A-Z]",
|
| 1082 |
-
"satisfied_pattern": r"z\.discriminatedUnion",
|
| 1083 |
-
"source": ZOD,
|
| 1084 |
-
},
|
| 1085 |
-
|
| 1086 |
# ---- Python -----------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1087 |
"python.assert-is-not-validation": {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1088 |
# Lookbehind, not a consumed newline. The match's start is what
|
| 1089 |
# scan_source turns into a line number, so a pattern that begins
|
| 1090 |
# by eating the PRECEDING newline reported the line above the
|
|
@@ -1093,24 +808,48 @@ ENRICH = {
|
|
| 1093 |
"source": PY_ASSERT,
|
| 1094 |
},
|
| 1095 |
"python.mutable-default-argument": {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1096 |
"violation_pattern": r"def\s+\w+\([^)]*=\s*(?:\[\]|\{\}|set\(\))",
|
| 1097 |
"source": PY_DEFAULTS,
|
| 1098 |
},
|
| 1099 |
"python.bare-except-catches-the-exit": {
|
|
|
|
|
|
|
|
|
|
| 1100 |
# A bare except and nothing else: `except (KeyError,) as exc:` has text
|
| 1101 |
# between the keyword and the colon and is not matched.
|
| 1102 |
"violation_pattern": r"except\s*:",
|
| 1103 |
"source": PY_ERRORS,
|
| 1104 |
},
|
| 1105 |
"python.eval-and-pickle-execute-their-input": {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1106 |
"violation_pattern": r"\beval\(|\bexec\(|pickle\.loads?\(",
|
| 1107 |
"source": PY_PICKLE,
|
| 1108 |
},
|
| 1109 |
"python.subprocess-shell-true-is-injection": {
|
|
|
|
|
|
|
|
|
|
| 1110 |
"violation_pattern": r"shell\s*=\s*True|os\.system\(|os\.popen\(",
|
| 1111 |
"source": PY_SUBPROCESS,
|
| 1112 |
},
|
| 1113 |
"python.path-join-does-not-contain-a-path": {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1114 |
# Word-anchored. Unanchored, `form` matched inside `.format(`, so
|
| 1115 |
# every os.path.join(..., "x {0}".format(y)) in a codebase was a hit.
|
| 1116 |
"violation_pattern": (r"os\.path\.join\([^)]*"
|
|
@@ -1123,6 +862,15 @@ ENRICH = {
|
|
| 1123 |
"source": PY_OSPATH,
|
| 1124 |
},
|
| 1125 |
"python.decimal-for-money-not-float": {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1126 |
# Money, not arithmetic. A bare \bfloat\( caught `float(successes) /
|
| 1127 |
# total` inside a Wilson score interval, which is exactly what binary
|
| 1128 |
# floating point is for. The name has to say it is an amount.
|
|
@@ -1134,51 +882,27 @@ ENRICH = {
|
|
| 1134 |
"source": PY_DECIMAL,
|
| 1135 |
},
|
| 1136 |
"python.naive-and-aware-datetimes-do-not-compare": {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1137 |
# Empty parentheses specifically: datetime.now(timezone.utc) is the fix.
|
| 1138 |
"violation_pattern": r"datetime\.now\(\s*\)|utcnow\(\s*\)",
|
| 1139 |
"source": PY_DATETIME,
|
| 1140 |
},
|
| 1141 |
|
| 1142 |
-
# ---- JavaScript -------------------------------------------------------
|
| 1143 |
-
"javascript.json-parse-returns-any": {
|
| 1144 |
-
"violation_pattern": r"=\s*JSON\.parse\(",
|
| 1145 |
-
"source": MDN + "/Global_Objects/JSON/parse",
|
| 1146 |
-
},
|
| 1147 |
-
"javascript.loose-equality-and-falsy-defaults": {
|
| 1148 |
-
# A default that is ITSELF falsy destroys nothing: `x || 0` is 0
|
| 1149 |
-
# when x is 0, and `x || ""` is "" when x is blank. The rule is
|
| 1150 |
-
# about a legitimate value being REPLACED, and here there is no
|
| 1151 |
-
# replacement to make. 31 of this rule's 69 hits on 9,641 lines of
|
| 1152 |
-
# Odoo JS were that -- the largest single class, found the first
|
| 1153 |
-
# time these seven rules were ever fired at code nobody wrote for
|
| 1154 |
-
# them.
|
| 1155 |
-
"violation_pattern": r"\w\s*\|\|\s*(?!0(?!\d|\.)|\"\"|'')(?:\d|['\"])",
|
| 1156 |
-
"source": MDN + "/Operators/Nullish_coalescing",
|
| 1157 |
-
},
|
| 1158 |
-
"javascript.async-errors-need-explicit-handling": {
|
| 1159 |
-
"violation_pattern": r"\.forEach\(\s*async",
|
| 1160 |
-
"source": MDN + "/Statements/async_function",
|
| 1161 |
-
},
|
| 1162 |
-
"javascript.number-parsing-is-lossy": {
|
| 1163 |
-
"violation_pattern": r"parseInt\(|parseFloat\(",
|
| 1164 |
-
"satisfied_pattern": r"Number\.isFinite|Number\.isInteger",
|
| 1165 |
-
"source": MDN + "/Global_Objects/parseInt",
|
| 1166 |
-
},
|
| 1167 |
-
"javascript.object-injection-via-user-keys": {
|
| 1168 |
-
"violation_pattern": r"\w+\[\s*(?:req\.(?:body|query|params)|input|payload)\.\w+\s*\]\s*=",
|
| 1169 |
-
# hasOwnProperty was dropped from this alternation deliberately. Bare
|
| 1170 |
-
# obj.hasOwnProperty(key) is the prototype-unsafe spelling -- the method
|
| 1171 |
-
# can be shadowed by the very key being probed -- and the token also
|
| 1172 |
-
# matched any passing MENTION of it, including an answer arguing
|
| 1173 |
-
# against its use. The two safe spellings are named explicitly instead.
|
| 1174 |
-
"satisfied_pattern": r"ALLOWED|allowlist|\.has\(|Object\.hasOwn\(|Object\.prototype\.hasOwnProperty\.call|Object\.create\(null\)",
|
| 1175 |
-
"source": MDN + "/Global_Objects/Object/proto",
|
| 1176 |
-
},
|
| 1177 |
-
"javascript.date-parsing-is-implementation-defined": {
|
| 1178 |
-
"violation_pattern": r"new Date\(\s*(?:req|input|body|payload|row)\.",
|
| 1179 |
-
"satisfied_pattern": r"Number\.isNaN|isNaN\(",
|
| 1180 |
-
"source": MDN + "/Global_Objects/Date/Date",
|
| 1181 |
-
},
|
| 1182 |
}
|
| 1183 |
|
| 1184 |
# ----------------------------------------------------------------- LIKELIHOOD
|
|
@@ -1273,8 +997,15 @@ PROBES = {
|
|
| 1273 |
"comparing a price or a quantity to zero gives the wrong answer sometimes",
|
| 1274 |
"odoo.constrains-iterate-recordset":
|
| 1275 |
"expected singleton error when a batch import writes several records at once",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1276 |
"odoo.prefer-sql-constraint":
|
| 1277 |
-
"
|
| 1278 |
"odoo.check-constraint-cannot-cross-tables":
|
| 1279 |
"my CHECK constraint will not install because it reads the product stock",
|
| 1280 |
"odoo.check-company-on-relations":
|
|
@@ -1302,28 +1033,6 @@ PROBES = {
|
|
| 1302 |
"odoo.except-order-follows-the-hierarchy":
|
| 1303 |
"my except AccessError block never runs and the API returns 409",
|
| 1304 |
|
| 1305 |
-
# ---- TypeScript -------------------------------------------------------
|
| 1306 |
-
"typescript.types-are-erased-at-runtime":
|
| 1307 |
-
"cast the JSON from an API response to my interface",
|
| 1308 |
-
"typescript.derive-type-from-schema":
|
| 1309 |
-
"keep my interface and my validation schema from drifting apart",
|
| 1310 |
-
"typescript.odoo-returns-false-for-every-empty-field":
|
| 1311 |
-
"parsing fails because Odoo sent false for a partner with no email",
|
| 1312 |
-
"typescript.odoo-many2one-is-a-tuple":
|
| 1313 |
-
"read the parent company name off a many2one field from Odoo",
|
| 1314 |
-
"typescript.odoo-read-returns-an-array":
|
| 1315 |
-
"parse the result of an Odoo read call for a single id",
|
| 1316 |
-
"typescript.unknown-not-any-at-boundaries":
|
| 1317 |
-
"what type should the payload of an incoming webhook handler have",
|
| 1318 |
-
"typescript.safeparse-when-failure-is-expected":
|
| 1319 |
-
"skip the bad rows in a CSV import instead of aborting the whole file",
|
| 1320 |
-
"typescript.validate-env-at-startup":
|
| 1321 |
-
"make sure the required environment variables are set before the app runs",
|
| 1322 |
-
"typescript.discriminated-union-for-variants":
|
| 1323 |
-
"parse a webhook whose body shape depends on a status field",
|
| 1324 |
-
"typescript.brand-validated-primitives":
|
| 1325 |
-
"stop a raw string being passed where a checked email address is required",
|
| 1326 |
-
|
| 1327 |
# ---- Python -----------------------------------------------------------
|
| 1328 |
"python.assert-is-not-validation":
|
| 1329 |
"check that an amount is positive before using it",
|
|
@@ -1342,19 +1051,6 @@ PROBES = {
|
|
| 1342 |
"python.naive-and-aware-datetimes-do-not-compare":
|
| 1343 |
"TypeError comparing offset-naive and offset-aware datetimes",
|
| 1344 |
|
| 1345 |
-
# ---- JavaScript -------------------------------------------------------
|
| 1346 |
-
"javascript.json-parse-returns-any":
|
| 1347 |
-
"use the parsed JSON body of an incoming request",
|
| 1348 |
-
"javascript.loose-equality-and-falsy-defaults":
|
| 1349 |
-
"a quantity of zero keeps being replaced by the default value",
|
| 1350 |
-
"javascript.number-parsing-is-lossy":
|
| 1351 |
-
"turn a page number from the query string into a number",
|
| 1352 |
-
"javascript.async-errors-need-explicit-handling":
|
| 1353 |
-
"await inside forEach does not wait and the errors disappear",
|
| 1354 |
-
"javascript.object-injection-via-user-keys":
|
| 1355 |
-
"set a property on an object using a key taken from the request body",
|
| 1356 |
-
"javascript.date-parsing-is-implementation-defined":
|
| 1357 |
-
"turn a date string typed by a user into a Date object",
|
| 1358 |
}
|
| 1359 |
|
| 1360 |
|
|
@@ -1381,23 +1077,10 @@ SEMANTIC_CHECK = {
|
|
| 1381 |
}
|
| 1382 |
|
| 1383 |
|
| 1384 |
-
# Rules with no violation_pattern still have provenance worth recording.
|
| 1385 |
-
EXTRA_SOURCES = {
|
| 1386 |
-
"typescript.safeparse-when-failure-is-expected": ZOD,
|
| 1387 |
-
"typescript.brand-validated-primitives": ZOD,
|
| 1388 |
-
}
|
| 1389 |
-
|
| 1390 |
-
|
| 1391 |
NO_PATTERN_REASON = {
|
| 1392 |
"odoo.schema-change-needs-migration":
|
| 1393 |
"The violation lives in the diff between two module versions plus the presence of a "
|
| 1394 |
"migrations/ directory. Nothing in a single generated snippet reveals it.",
|
| 1395 |
-
"typescript.safeparse-when-failure-is-expected":
|
| 1396 |
-
"Whether parse or safeParse is correct depends on whether the caller can recover, "
|
| 1397 |
-
"which is context the snippet does not contain. Any pattern here would be noise.",
|
| 1398 |
-
"typescript.brand-validated-primitives":
|
| 1399 |
-
"Absence of branding is not detectable: plain string parameters are correct in most "
|
| 1400 |
-
"code. Flagging them all would bury the real findings.",
|
| 1401 |
}
|
| 1402 |
|
| 1403 |
|
|
@@ -1406,12 +1089,18 @@ def main():
|
|
| 1406 |
os.makedirs(out, exist_ok=True)
|
| 1407 |
total = 0
|
| 1408 |
unknown = (set(ENRICH) | set(NO_PATTERN_REASON) | set(SEMANTIC_CHECK)
|
| 1409 |
-
| set(PROBES) | set(
|
| 1410 |
unprobed = []
|
| 1411 |
covered = 0
|
| 1412 |
semantic = 0
|
| 1413 |
-
|
| 1414 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1415 |
for r in rules:
|
| 1416 |
r["stack"] = stack
|
| 1417 |
r.setdefault("bypassed_by", [])
|
|
@@ -1422,8 +1111,6 @@ def main():
|
|
| 1422 |
# deriving it costs a line.
|
| 1423 |
r["likelihood"] = LIKELIHOOD.get(r["id"], LIKELIHOOD_DEFAULT)
|
| 1424 |
r["advisory"] = r["likelihood"] in ADVISORY_LIKELIHOODS
|
| 1425 |
-
if r["id"] in EXTRA_SOURCES:
|
| 1426 |
-
r["source"] = EXTRA_SOURCES[r["id"]]
|
| 1427 |
if r["id"] in PROBES:
|
| 1428 |
r["probe"] = PROBES[r["id"]]
|
| 1429 |
else:
|
|
@@ -1448,6 +1135,8 @@ def main():
|
|
| 1448 |
json.dump(rules, fh, indent=2, ensure_ascii=False)
|
| 1449 |
crit = sum(1 for r in rules if r["severity"] == "critical")
|
| 1450 |
print(" {0:<12} {1:>2} rules ({2} critical) -> {3}.json".format(stack, len(rules), crit, stack))
|
|
|
|
|
|
|
| 1451 |
total += len(rules)
|
| 1452 |
checked = covered + semantic
|
| 1453 |
print("\n {0} rules total, {1} machine-checkable ({2}%)".format(
|
|
|
|
| 1 |
+
"""Generates the validation rule corpus JSON from the definitions below.
|
| 2 |
|
| 3 |
The corpus is the single source of truth. System prompts and the RAG index are
|
| 4 |
both DERIVED from it (corpus.py prompt / corpus.py index) so they cannot drift.
|
| 5 |
|
| 6 |
+
ONE corpus, one stack, and everything in it is for reviewing Odoo code. Two
|
| 7 |
+
lists feed it: ODOO, the ORM rules, and PYTHON, plain-Python rules that apply
|
| 8 |
+
because an Odoo module is a Python module. Both are written to odoo.json and
|
| 9 |
+
both carry stack "odoo". The `python.` id prefix is provenance -- where the
|
| 10 |
+
rule came from and which documentation check_sources verifies it against --
|
| 11 |
+
and not a second mode anybody can select. There is no second mode.
|
| 12 |
+
|
| 13 |
Edit the rules here, then: python build_corpus.py && python ../bin/corpus.py validate
|
| 14 |
"""
|
| 15 |
import io
|
|
|
|
| 28 |
# must be one list, not one here and another in the validator.
|
| 29 |
from corpus import ADVISORY_LIKELIHOODS, LIKELIHOOD_DEFAULT # noqa: E402
|
| 30 |
|
| 31 |
+
# The upstream documentation version each set of rules was read against. Two
|
| 32 |
+
# entries under one stack: the ORM rules are read against an Odoo series, the
|
| 33 |
+
# composed-in Python rules against a language version.
|
| 34 |
+
#
|
| 35 |
# Recorded rather than assumed: a rule verified on 18.0 is not thereby
|
| 36 |
# verified on 16.0, and the context layer needs to be able to say so when it
|
| 37 |
# meets a module pinned to another series.
|
| 38 |
CHECKED_AGAINST = {
|
| 39 |
"odoo": "18.0",
|
| 40 |
"python": "3.12",
|
|
|
|
|
|
|
| 41 |
}
|
| 42 |
|
| 43 |
ODOO = [
|
|
|
|
| 451 |
},
|
| 452 |
]
|
| 453 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 454 |
PYTHON = [
|
| 455 |
{
|
| 456 |
"id": "python.assert-is-not-validation",
|
|
|
|
| 610 |
# page does, and says the thing the rule says.
|
| 611 |
ODOO_GUIDELINES = "https://www.odoo.com/documentation/18.0/contributing/development/coding_guidelines.html"
|
| 612 |
PG_CREATE_TABLE = "https://www.postgresql.org/docs/current/ddl-constraints.html"
|
|
|
|
|
|
|
| 613 |
PY_DOCS = "https://docs.python.org/3"
|
| 614 |
PY_ASSERT = PY_DOCS + "/reference/simple_stmts.html#the-assert-statement"
|
| 615 |
PY_DEFAULTS = PY_DOCS + "/tutorial/controlflow.html#default-argument-values"
|
|
|
|
| 620 |
PY_DECIMAL = PY_DOCS + "/library/decimal.html"
|
| 621 |
PY_DATETIME = PY_DOCS + "/library/datetime.html"
|
| 622 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 623 |
ENRICH = {
|
| 624 |
# ---- Odoo -------------------------------------------------------------
|
| 625 |
# Both decorators are specified on the ORM reference page, which is where the
|
|
|
|
| 772 |
"source": ODOO_ORM,
|
| 773 |
},
|
| 774 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 775 |
# ---- Python -----------------------------------------------------------
|
| 776 |
+
#
|
| 777 |
+
# These eight are selected for the Odoo stack, and were measured there for
|
| 778 |
+
# the first time on 2026-09-16: 50 findings across OpenSPP (226,937 lines)
|
| 779 |
+
# and OCA (33,960), every one read in its own file. 26 true, 24 false.
|
| 780 |
+
# Verdicts and reasons are in classification-python-2026-09.json; the
|
| 781 |
+
# per-rule result is written under each rule below, including the three
|
| 782 |
+
# that fired and were never right and the three that never fired at all.
|
| 783 |
+
#
|
| 784 |
+
# One test decides most of it, so it is stated once. An Odoo Datetime field
|
| 785 |
+
# is naive UTC by contract. A naive LOCAL datetime that reaches one --
|
| 786 |
+
# written to it, compared against it, or handed to something that does --
|
| 787 |
+
# is the defect. One that is only formatted for display or used to name a
|
| 788 |
+
# file is not, and eight of the twelve false positives are that.
|
| 789 |
"python.assert-is-not-validation": {
|
| 790 |
+
# 6 true, 4 false on the two trees. True where the asserted value comes
|
| 791 |
+
# from outside the module -- a jsonb row, self._context, a public
|
| 792 |
+
# method's argument, a user-typed field parsed with literal_eval. False
|
| 793 |
+
# in three shapes, and they are worth naming because a narrowing would
|
| 794 |
+
# have to recognise them: an invariant the module's own callers cannot
|
| 795 |
+
# break (a helper called with two literals), a private helper's contract
|
| 796 |
+
# with its only caller, and an assert whose condition the NEXT line
|
| 797 |
+
# re-enforces anyway (a zip with strict=True).
|
| 798 |
+
#
|
| 799 |
+
# Not narrowed on that. Telling the three apart from the six needs to
|
| 800 |
+
# know where a value came from, which is the same assignment-tracing
|
| 801 |
+
# pass float-comparison is waiting on, and guessing at it from the
|
| 802 |
+
# variable's name is what made the sudo rule 5.8%.
|
| 803 |
# Lookbehind, not a consumed newline. The match's start is what
|
| 804 |
# scan_source turns into a line number, so a pattern that begins
|
| 805 |
# by eating the PRECEDING newline reported the line above the
|
|
|
|
| 808 |
"source": PY_ASSERT,
|
| 809 |
},
|
| 810 |
"python.mutable-default-argument": {
|
| 811 |
+
# Fired on neither tree. Silence is not a pass: it says these 260,000
|
| 812 |
+
# lines contain no mutable default, which is also what a broken checker
|
| 813 |
+
# says. ruff's B006 is selected in OpenSPP's own .ruff.toml, which is
|
| 814 |
+
# the more likely explanation.
|
| 815 |
"violation_pattern": r"def\s+\w+\([^)]*=\s*(?:\[\]|\{\}|set\(\))",
|
| 816 |
"source": PY_DEFAULTS,
|
| 817 |
},
|
| 818 |
"python.bare-except-catches-the-exit": {
|
| 819 |
+
# Fired on neither tree, and E722 is on by default in ruff, so a repo
|
| 820 |
+
# running ruff at all has none of these left to find.
|
| 821 |
+
#
|
| 822 |
# A bare except and nothing else: `except (KeyError,) as exc:` has text
|
| 823 |
# between the keyword and the colon and is not matched.
|
| 824 |
"violation_pattern": r"except\s*:",
|
| 825 |
"source": PY_ERRORS,
|
| 826 |
},
|
| 827 |
"python.eval-and-pickle-execute-their-input": {
|
| 828 |
+
# 0 true, 1 false. The one finding is the word eval() written in a
|
| 829 |
+
# method's DOCSTRING, in a sentence arguing against using it. That is
|
| 830 |
+
# not this rule's fault: scan_source strips # comments before matching
|
| 831 |
+
# and does not strip string literals, so prose in a docstring is
|
| 832 |
+
# matched as code. One sample decides nothing about the rule, and the
|
| 833 |
+
# checker defect it exposes is named in the README.
|
| 834 |
"violation_pattern": r"\beval\(|\bexec\(|pickle\.loads?\(",
|
| 835 |
"source": PY_PICKLE,
|
| 836 |
},
|
| 837 |
"python.subprocess-shell-true-is-injection": {
|
| 838 |
+
# Fired on neither tree. Worth keeping on that evidence rather than in
|
| 839 |
+
# spite of it: ruff's S602 covers it and neither repo selects S, so the
|
| 840 |
+
# silence here is a fact about the code and not about the coverage.
|
| 841 |
"violation_pattern": r"shell\s*=\s*True|os\.system\(|os\.popen\(",
|
| 842 |
"source": PY_SUBPROCESS,
|
| 843 |
},
|
| 844 |
"python.path-join-does-not-contain-a-path": {
|
| 845 |
+
# 0 true, 4 false. All four join a configured directory to a name this
|
| 846 |
+
# module generated -- a timestamped backup filename, a literal from a
|
| 847 |
+
# list declared in the same function, a module path from
|
| 848 |
+
# get_module_path. The name-matching is what fires: `filename` is in
|
| 849 |
+
# the alternation and says nothing about where the value came from.
|
| 850 |
+
# Four findings is not enough to conclude a checker is wrong, so it is
|
| 851 |
+
# reported here rather than acted on.
|
| 852 |
+
#
|
| 853 |
# Word-anchored. Unanchored, `form` matched inside `.format(`, so
|
| 854 |
# every os.path.join(..., "x {0}".format(y)) in a codebase was a hit.
|
| 855 |
"violation_pattern": (r"os\.path\.join\([^)]*"
|
|
|
|
| 862 |
"source": PY_OSPATH,
|
| 863 |
},
|
| 864 |
"python.decimal-for-money-not-float": {
|
| 865 |
+
# 0 true, 3 false, and this one has a reason behind it rather than a
|
| 866 |
+
# sample size. Odoo's Monetary field IS a float -- the ORM defines it
|
| 867 |
+
# that way -- and the framework's answer to the precision problem is
|
| 868 |
+
# float_round and float_compare against the currency's rounding, not
|
| 869 |
+
# Decimal. So inside an Odoo module the rule fires on the idiom and its
|
| 870 |
+
# remedy cannot be taken. That is an argument, not a measurement, and
|
| 871 |
+
# three findings do not settle it either way; it is written down in the
|
| 872 |
+
# README as the composition's open question rather than acted on here.
|
| 873 |
+
#
|
| 874 |
# Money, not arithmetic. A bare \bfloat\( caught `float(successes) /
|
| 875 |
# total` inside a Wilson score interval, which is exactly what binary
|
| 876 |
# floating point is for. The name has to say it is an amount.
|
|
|
|
| 882 |
"source": PY_DECIMAL,
|
| 883 |
},
|
| 884 |
"python.naive-and-aware-datetimes-do-not-compare": {
|
| 885 |
+
# 20 true, 12 false -- 32 of the 50 findings the composition adds, and
|
| 886 |
+
# the reason the composition is worth having at all. The true ones are
|
| 887 |
+
# a naive LOCAL datetime reaching an ORM Datetime: written to
|
| 888 |
+
# ended_date, compared against create_date in an autovacuum cron,
|
| 889 |
+
# subtracted from a field value in an eligibility expression, or
|
| 890 |
+
# labelled with a literal "Z" inside a cryptographic proof. On a server
|
| 891 |
+
# clocked to UTC every one of them is latent; on any other it is wrong
|
| 892 |
+
# by the offset, and OpenSPP's own history carries two fixes of exactly
|
| 893 |
+
# this class.
|
| 894 |
+
#
|
| 895 |
+
# The twelve false ones are one shape, near enough: a timestamp that is
|
| 896 |
+
# formatted and never compared -- a Generated: line in a report, a
|
| 897 |
+
# backup filename, a period label. A narrowing would have to see what
|
| 898 |
+
# happens to the value afterwards, which the regex cannot; two more are
|
| 899 |
+
# the docstring defect named above.
|
| 900 |
+
#
|
| 901 |
# Empty parentheses specifically: datetime.now(timezone.utc) is the fix.
|
| 902 |
"violation_pattern": r"datetime\.now\(\s*\)|utcnow\(\s*\)",
|
| 903 |
"source": PY_DATETIME,
|
| 904 |
},
|
| 905 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 906 |
}
|
| 907 |
|
| 908 |
# ----------------------------------------------------------------- LIKELIHOOD
|
|
|
|
| 997 |
"comparing a price or a quantity to zero gives the wrong answer sometimes",
|
| 998 |
"odoo.constrains-iterate-recordset":
|
| 999 |
"expected singleton error when a batch import writes several records at once",
|
| 1000 |
+
# Reworded 2026-09-16, and re-measured. The old probe was "stop anyone
|
| 1001 |
+
# entering a negative quantity on a sale order line", which is the generic
|
| 1002 |
+
# validation question every constraint rule in the corpus answers -- with
|
| 1003 |
+
# 19 rules in the stack it still came back first, and with 27 it stopped
|
| 1004 |
+
# coming back at all. The rule's own subject is narrower than that: a
|
| 1005 |
+
# Python check that two concurrent transactions both pass. Nothing else
|
| 1006 |
+
# here is about a race.
|
| 1007 |
"odoo.prefer-sql-constraint":
|
| 1008 |
+
"should this check be a database constraint or an api.constrains method",
|
| 1009 |
"odoo.check-constraint-cannot-cross-tables":
|
| 1010 |
"my CHECK constraint will not install because it reads the product stock",
|
| 1011 |
"odoo.check-company-on-relations":
|
|
|
|
| 1033 |
"odoo.except-order-follows-the-hierarchy":
|
| 1034 |
"my except AccessError block never runs and the API returns 409",
|
| 1035 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1036 |
# ---- Python -----------------------------------------------------------
|
| 1037 |
"python.assert-is-not-validation":
|
| 1038 |
"check that an amount is positive before using it",
|
|
|
|
| 1051 |
"python.naive-and-aware-datetimes-do-not-compare":
|
| 1052 |
"TypeError comparing offset-naive and offset-aware datetimes",
|
| 1053 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1054 |
}
|
| 1055 |
|
| 1056 |
|
|
|
|
| 1077 |
}
|
| 1078 |
|
| 1079 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1080 |
NO_PATTERN_REASON = {
|
| 1081 |
"odoo.schema-change-needs-migration":
|
| 1082 |
"The violation lives in the diff between two module versions plus the presence of a "
|
| 1083 |
"migrations/ directory. Nothing in a single generated snippet reveals it.",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1084 |
}
|
| 1085 |
|
| 1086 |
|
|
|
|
| 1089 |
os.makedirs(out, exist_ok=True)
|
| 1090 |
total = 0
|
| 1091 |
unknown = (set(ENRICH) | set(NO_PATTERN_REASON) | set(SEMANTIC_CHECK)
|
| 1092 |
+
| set(PROBES) | set(LIKELIHOOD))
|
| 1093 |
unprobed = []
|
| 1094 |
covered = 0
|
| 1095 |
semantic = 0
|
| 1096 |
+
# One stack, and everything in it is written for reviewing Odoo code. The
|
| 1097 |
+
# `python.` rules are in it because an Odoo module IS Python: selection was
|
| 1098 |
+
# strict equality on this field, so an Odoo file got the 19 ORM rules and
|
| 1099 |
+
# none of the 8 Python ones, and `fieldtest --stack python` over OpenSPP
|
| 1100 |
+
# found 32 hits `--stack odoo` never reported. The id prefix stays as it
|
| 1101 |
+
# is -- it names where the rule came from and which documentation it is
|
| 1102 |
+
# sourced to, which is not the same question as which stack selects it.
|
| 1103 |
+
for stack, rules in (("odoo", ODOO + PYTHON),):
|
| 1104 |
for r in rules:
|
| 1105 |
r["stack"] = stack
|
| 1106 |
r.setdefault("bypassed_by", [])
|
|
|
|
| 1111 |
# deriving it costs a line.
|
| 1112 |
r["likelihood"] = LIKELIHOOD.get(r["id"], LIKELIHOOD_DEFAULT)
|
| 1113 |
r["advisory"] = r["likelihood"] in ADVISORY_LIKELIHOODS
|
|
|
|
|
|
|
| 1114 |
if r["id"] in PROBES:
|
| 1115 |
r["probe"] = PROBES[r["id"]]
|
| 1116 |
else:
|
|
|
|
| 1135 |
json.dump(rules, fh, indent=2, ensure_ascii=False)
|
| 1136 |
crit = sum(1 for r in rules if r["severity"] == "critical")
|
| 1137 |
print(" {0:<12} {1:>2} rules ({2} critical) -> {3}.json".format(stack, len(rules), crit, stack))
|
| 1138 |
+
print(" {0:<12} {1:>2} Odoo ORM rules + {2} plain-Python rules composed in"
|
| 1139 |
+
.format("", len(ODOO), len(PYTHON)))
|
| 1140 |
total += len(rules)
|
| 1141 |
checked = covered + semantic
|
| 1142 |
print("\n {0} rules total, {1} machine-checkable ({2}%)".format(
|
|
@@ -84,9 +84,20 @@ REFERENCE = "https://www.odoo.com/documentation/{0}/developer/reference/backend/
|
|
| 84 |
|
| 85 |
|
| 86 |
def load():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
with io.open(RULES, encoding="utf-8-sig") as fh:
|
| 88 |
data = json.load(fh)
|
| 89 |
-
|
|
|
|
| 90 |
|
| 91 |
|
| 92 |
def fetch(url, cache):
|
|
|
|
| 84 |
|
| 85 |
|
| 86 |
def load():
|
| 87 |
+
"""The rules that make a claim about an Odoo series.
|
| 88 |
+
|
| 89 |
+
odoo.json holds the whole corpus, which since the composition includes
|
| 90 |
+
eight plain-Python rules. Those carry no `odoo_versions` and cite
|
| 91 |
+
docs.python.org, so there is no series claim here to check and no Odoo API
|
| 92 |
+
to look for -- running them through this would report eight rules as
|
| 93 |
+
"unversioned" every time, which is a true sentence that makes the real
|
| 94 |
+
count harder to read. They are excluded by their origin prefix, the same
|
| 95 |
+
thing corpus.ORIGINS names.
|
| 96 |
+
"""
|
| 97 |
with io.open(RULES, encoding="utf-8-sig") as fh:
|
| 98 |
data = json.load(fh)
|
| 99 |
+
rules = data if isinstance(data, list) else data.get("rules", data)
|
| 100 |
+
return [r for r in rules if r["id"].startswith("odoo.")]
|
| 101 |
|
| 102 |
|
| 103 |
def fetch(url, cache):
|
|
@@ -1,14 +1,12 @@
|
|
| 1 |
{
|
| 2 |
-
"built_utc": "2026-09-
|
| 3 |
-
"rules":
|
| 4 |
-
"machine_checkable":
|
| 5 |
-
"by_pattern":
|
| 6 |
"by_semantic_check": 10,
|
| 7 |
"checked_against": {
|
| 8 |
"odoo": "18.0",
|
| 9 |
-
"python": "3.12"
|
| 10 |
-
"typescript": "5.x",
|
| 11 |
-
"javascript": "ES2023"
|
| 12 |
},
|
| 13 |
-
"corpus_sha256": "
|
| 14 |
}
|
|
|
|
| 1 |
{
|
| 2 |
+
"built_utc": "2026-09-16T08:00:31.662425+00:00",
|
| 3 |
+
"rules": 27,
|
| 4 |
+
"machine_checkable": 26,
|
| 5 |
+
"by_pattern": 16,
|
| 6 |
"by_semantic_check": 10,
|
| 7 |
"checked_against": {
|
| 8 |
"odoo": "18.0",
|
| 9 |
+
"python": "3.12"
|
|
|
|
|
|
|
| 10 |
},
|
| 11 |
+
"corpus_sha256": "68dd1fc12d9d546627767f840e3acffb1d0c78fd2ff5571d259261f5312ece03"
|
| 12 |
}
|
|
@@ -19,17 +19,15 @@
|
|
| 19 |
"properties": {
|
| 20 |
"id": {
|
| 21 |
"type": "string",
|
| 22 |
-
"pattern": "^
|
| 23 |
-
"description": "Stable citable identifier '<
|
| 24 |
},
|
| 25 |
"stack": {
|
| 26 |
"type": "string",
|
| 27 |
"enum": [
|
| 28 |
-
"odoo"
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
"javascript"
|
| 32 |
-
]
|
| 33 |
},
|
| 34 |
"severity": {
|
| 35 |
"type": "string",
|
|
|
|
| 19 |
"properties": {
|
| 20 |
"id": {
|
| 21 |
"type": "string",
|
| 22 |
+
"pattern": "^(odoo|python)\\.[a-z0-9]+(-[a-z0-9]+)*$",
|
| 23 |
+
"description": "Stable citable identifier '<origin>.<slug>'. The prefix names where the rule came from -- odoo. for the ORM rules, python. for the plain-Python rules composed into the same stack -- and is not the stack, of which there is one. Models cite this; verify-citations checks it exists."
|
| 24 |
},
|
| 25 |
"stack": {
|
| 26 |
"type": "string",
|
| 27 |
"enum": [
|
| 28 |
+
"odoo"
|
| 29 |
+
],
|
| 30 |
+
"description": "The one stack. It stays a field because prompts, the RAG index and every measurement are keyed on it. It is not the id prefix: python.decimal-for-money-not-float carries stack odoo, because it is selected when reviewing Odoo code while being sourced to docs.python.org."
|
|
|
|
|
|
|
| 31 |
},
|
| 32 |
"severity": {
|
| 33 |
"type": "string",
|
|
@@ -1,136 +0,0 @@
|
|
| 1 |
-
[
|
| 2 |
-
{
|
| 3 |
-
"id": "javascript.json-parse-returns-any",
|
| 4 |
-
"severity": "critical",
|
| 5 |
-
"title": "JSON.parse output is unchecked",
|
| 6 |
-
"rule": "Never use the result of JSON.parse directly. Pass it through a schema or an explicit shape check first.",
|
| 7 |
-
"why": "JSON.parse succeeds for any syntactically valid JSON, including null, a bare number, or an object missing every field you expect. Property access then yields undefined, which propagates through arithmetic as NaN and through templates as the literal string undefined.",
|
| 8 |
-
"enforcement_layer": "runtime-parse",
|
| 9 |
-
"bypassed_by": [],
|
| 10 |
-
"wrong": "const cfg = JSON.parse(raw);\nstartServer(cfg.port);",
|
| 11 |
-
"correct": "const cfg = ConfigSchema.parse(JSON.parse(raw));\nstartServer(cfg.port);",
|
| 12 |
-
"tags": [
|
| 13 |
-
"json",
|
| 14 |
-
"boundary"
|
| 15 |
-
],
|
| 16 |
-
"stack": "javascript",
|
| 17 |
-
"violation_pattern": "=\\s*JSON\\.parse\\(",
|
| 18 |
-
"source": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse",
|
| 19 |
-
"likelihood": "unmeasured",
|
| 20 |
-
"advisory": false,
|
| 21 |
-
"probe": "use the parsed JSON body of an incoming request"
|
| 22 |
-
},
|
| 23 |
-
{
|
| 24 |
-
"id": "javascript.loose-equality-and-falsy-defaults",
|
| 25 |
-
"severity": "high",
|
| 26 |
-
"title": "Falsy defaults discard legitimate zero and empty string",
|
| 27 |
-
"rule": "Use the nullish coalescing operator for defaults and strict equality for comparisons. Reserve the logical OR operator for genuine boolean logic.",
|
| 28 |
-
"why": "The OR operator treats 0, empty string and false as absent, so a quantity of 0 or an intentionally blank note is silently replaced by the default. This is the most common way a validated value is destroyed after it has passed validation.",
|
| 29 |
-
"enforcement_layer": "runtime-parse",
|
| 30 |
-
"bypassed_by": [],
|
| 31 |
-
"wrong": "const qty = input.qty || 1; // qty of 0 becomes 1\nif (status == null) { } // also matches undefined by design, but == elsewhere surprises",
|
| 32 |
-
"correct": "const qty = input.qty ?? 1; // only null or undefined fall back\nif (status === null || status === undefined) { }",
|
| 33 |
-
"tags": [
|
| 34 |
-
"coercion",
|
| 35 |
-
"defaults",
|
| 36 |
-
"silent-failure"
|
| 37 |
-
],
|
| 38 |
-
"stack": "javascript",
|
| 39 |
-
"violation_pattern": "\\w\\s*\\|\\|\\s*(?!0(?!\\d|\\.)|\\\"\\\"|'')(?:\\d|['\\\"])",
|
| 40 |
-
"source": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing",
|
| 41 |
-
"likelihood": "unmeasured",
|
| 42 |
-
"advisory": false,
|
| 43 |
-
"probe": "a quantity of zero keeps being replaced by the default value"
|
| 44 |
-
},
|
| 45 |
-
{
|
| 46 |
-
"id": "javascript.number-parsing-is-lossy",
|
| 47 |
-
"severity": "high",
|
| 48 |
-
"title": "Coercing user input to a number accepts nonsense",
|
| 49 |
-
"rule": "Validate numeric input with an explicit check for finiteness rather than relying on Number, parseInt or the unary plus.",
|
| 50 |
-
"why": "parseInt('12abc') returns 12, Number('') returns 0, and Number(' ') returns 0 as well. Each converts invalid input into a plausible number that passes every downstream check and corrupts totals rather than raising an error.",
|
| 51 |
-
"enforcement_layer": "runtime-parse",
|
| 52 |
-
"bypassed_by": [],
|
| 53 |
-
"wrong": "const qty = parseInt(req.body.qty);",
|
| 54 |
-
"correct": "const qty = Number(req.body.qty);\nif (!Number.isFinite(qty) || !Number.isInteger(qty) || qty <= 0) {\n throw new Error(`qty must be a positive integer, received: ${req.body.qty}`);\n}",
|
| 55 |
-
"tags": [
|
| 56 |
-
"numbers",
|
| 57 |
-
"coercion",
|
| 58 |
-
"input"
|
| 59 |
-
],
|
| 60 |
-
"stack": "javascript",
|
| 61 |
-
"violation_pattern": "parseInt\\(|parseFloat\\(",
|
| 62 |
-
"satisfied_pattern": "Number\\.isFinite|Number\\.isInteger",
|
| 63 |
-
"source": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt",
|
| 64 |
-
"likelihood": "unmeasured",
|
| 65 |
-
"advisory": false,
|
| 66 |
-
"probe": "turn a page number from the query string into a number"
|
| 67 |
-
},
|
| 68 |
-
{
|
| 69 |
-
"id": "javascript.async-errors-need-explicit-handling",
|
| 70 |
-
"severity": "critical",
|
| 71 |
-
"title": "A rejected promise in a callback is not caught by the surrounding try",
|
| 72 |
-
"rule": "Await every promise you create, or attach a catch to it. Never pass an async function where a synchronous callback is expected.",
|
| 73 |
-
"why": "An async callback passed to forEach or to an event emitter returns a promise nobody holds. A rejection inside it escapes the enclosing try/catch entirely and becomes an unhandled rejection, which terminates the process on modern Node and silently skips work before that.",
|
| 74 |
-
"enforcement_layer": "runtime-parse",
|
| 75 |
-
"bypassed_by": [],
|
| 76 |
-
"wrong": "rows.forEach(async (row) => {\n await save(row); // rejections escape, loop does not wait\n});",
|
| 77 |
-
"correct": "await Promise.all(rows.map((row) => save(row)));\n// or, when order and back-pressure matter:\nfor (const row of rows) {\n await save(row);\n}",
|
| 78 |
-
"tags": [
|
| 79 |
-
"async",
|
| 80 |
-
"promises",
|
| 81 |
-
"error-handling"
|
| 82 |
-
],
|
| 83 |
-
"stack": "javascript",
|
| 84 |
-
"violation_pattern": "\\.forEach\\(\\s*async",
|
| 85 |
-
"source": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function",
|
| 86 |
-
"likelihood": "unmeasured",
|
| 87 |
-
"advisory": false,
|
| 88 |
-
"probe": "await inside forEach does not wait and the errors disappear"
|
| 89 |
-
},
|
| 90 |
-
{
|
| 91 |
-
"id": "javascript.object-injection-via-user-keys",
|
| 92 |
-
"severity": "critical",
|
| 93 |
-
"title": "Never index an object with an unvalidated user-supplied key",
|
| 94 |
-
"rule": "Check a user-supplied key against an allowlist, or use a Map, before using it to read or write a property.",
|
| 95 |
-
"why": "A key of __proto__ or constructor reaches the prototype chain. Writing through it pollutes every object in the process, which turns a validation gap into remote code execution in the worst case and into corrupted defaults everywhere in the mildest.",
|
| 96 |
-
"enforcement_layer": "runtime-parse",
|
| 97 |
-
"bypassed_by": [],
|
| 98 |
-
"wrong": "target[req.body.key] = req.body.value;",
|
| 99 |
-
"correct": "const ALLOWED = new Set(['name', 'email', 'phone']);\nif (!ALLOWED.has(req.body.key)) {\n throw new Error(`Unsupported field: ${req.body.key}`);\n}\ntarget[req.body.key] = req.body.value;",
|
| 100 |
-
"tags": [
|
| 101 |
-
"security",
|
| 102 |
-
"prototype-pollution",
|
| 103 |
-
"input"
|
| 104 |
-
],
|
| 105 |
-
"stack": "javascript",
|
| 106 |
-
"violation_pattern": "\\w+\\[\\s*(?:req\\.(?:body|query|params)|input|payload)\\.\\w+\\s*\\]\\s*=",
|
| 107 |
-
"satisfied_pattern": "ALLOWED|allowlist|\\.has\\(|Object\\.hasOwn\\(|Object\\.prototype\\.hasOwnProperty\\.call|Object\\.create\\(null\\)",
|
| 108 |
-
"source": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/proto",
|
| 109 |
-
"likelihood": "unmeasured",
|
| 110 |
-
"advisory": false,
|
| 111 |
-
"probe": "set a property on an object using a key taken from the request body"
|
| 112 |
-
},
|
| 113 |
-
{
|
| 114 |
-
"id": "javascript.date-parsing-is-implementation-defined",
|
| 115 |
-
"severity": "medium",
|
| 116 |
-
"title": "The Date constructor accepts ambiguous strings",
|
| 117 |
-
"rule": "Parse dates from an explicit ISO-8601 string and reject anything that produces an invalid date. Never feed arbitrary user text to new Date.",
|
| 118 |
-
"why": "new Date('01/02/2026') is January the second in one locale's reading and the second of January in another, and non-ISO formats are implementation-defined across engines. new Date('nonsense') returns an Invalid Date object rather than throwing, so the failure travels onward as NaN.",
|
| 119 |
-
"enforcement_layer": "runtime-parse",
|
| 120 |
-
"bypassed_by": [],
|
| 121 |
-
"wrong": "const when = new Date(req.body.date);",
|
| 122 |
-
"correct": "const when = new Date(req.body.date);\nif (Number.isNaN(when.getTime())) {\n throw new Error(`Invalid date: ${req.body.date}`);\n}",
|
| 123 |
-
"tags": [
|
| 124 |
-
"dates",
|
| 125 |
-
"coercion",
|
| 126 |
-
"locale"
|
| 127 |
-
],
|
| 128 |
-
"stack": "javascript",
|
| 129 |
-
"violation_pattern": "new Date\\(\\s*(?:req|input|body|payload|row)\\.",
|
| 130 |
-
"satisfied_pattern": "Number\\.isNaN|isNaN\\(",
|
| 131 |
-
"source": "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date",
|
| 132 |
-
"likelihood": "unmeasured",
|
| 133 |
-
"advisory": false,
|
| 134 |
-
"probe": "turn a date string typed by a user into a Date object"
|
| 135 |
-
}
|
| 136 |
-
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -159,7 +159,7 @@
|
|
| 159 |
"source": "https://www.odoo.com/documentation/18.0/developer/tutorials/server_framework_101/10_constraints.html",
|
| 160 |
"likelihood": "preference",
|
| 161 |
"advisory": true,
|
| 162 |
-
"probe": "
|
| 163 |
},
|
| 164 |
{
|
| 165 |
"id": "odoo.check-constraint-cannot-cross-tables",
|
|
@@ -479,5 +479,200 @@
|
|
| 479 |
"likelihood": "unproven",
|
| 480 |
"advisory": true,
|
| 481 |
"probe": "my except AccessError block never runs and the API returns 409"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 482 |
}
|
| 483 |
]
|
|
|
|
| 159 |
"source": "https://www.odoo.com/documentation/18.0/developer/tutorials/server_framework_101/10_constraints.html",
|
| 160 |
"likelihood": "preference",
|
| 161 |
"advisory": true,
|
| 162 |
+
"probe": "should this check be a database constraint or an api.constrains method"
|
| 163 |
},
|
| 164 |
{
|
| 165 |
"id": "odoo.check-constraint-cannot-cross-tables",
|
|
|
|
| 479 |
"likelihood": "unproven",
|
| 480 |
"advisory": true,
|
| 481 |
"probe": "my except AccessError block never runs and the API returns 409"
|
| 482 |
+
},
|
| 483 |
+
{
|
| 484 |
+
"id": "python.assert-is-not-validation",
|
| 485 |
+
"severity": "critical",
|
| 486 |
+
"title": "assert disappears under -O, so it cannot enforce anything",
|
| 487 |
+
"rule": "Never use assert to validate input or enforce an invariant. Raise an explicit exception. Reserve assert for conditions that are impossible unless the program is already broken.",
|
| 488 |
+
"why": "Python removes every assert statement when the interpreter runs with -O or PYTHONOPTIMIZE set, which is the default in a good many production images. The check is not weakened, it is absent -- so invalid input passes in exactly the environment where it matters, and passes in development, where the flag is off.",
|
| 489 |
+
"enforcement_layer": "runtime-parse",
|
| 490 |
+
"bypassed_by": [
|
| 491 |
+
"python -O",
|
| 492 |
+
"PYTHONOPTIMIZE=1",
|
| 493 |
+
"a .pyc built with optimisation"
|
| 494 |
+
],
|
| 495 |
+
"wrong": "def withdraw(account, amount):\n assert amount > 0, 'amount must be positive'\n account.balance -= amount",
|
| 496 |
+
"correct": "def withdraw(account, amount):\n if amount <= 0:\n raise ValueError('amount must be positive, got {0!r}'.format(amount))\n account.balance -= amount",
|
| 497 |
+
"tags": [
|
| 498 |
+
"assert",
|
| 499 |
+
"input-validation",
|
| 500 |
+
"optimisation",
|
| 501 |
+
"silent-failure"
|
| 502 |
+
],
|
| 503 |
+
"stack": "odoo",
|
| 504 |
+
"violation_pattern": "(?<=\\n)[ \\t]*assert\\s+",
|
| 505 |
+
"source": "https://docs.python.org/3/reference/simple_stmts.html#the-assert-statement",
|
| 506 |
+
"likelihood": "unmeasured",
|
| 507 |
+
"advisory": false,
|
| 508 |
+
"probe": "check that an amount is positive before using it"
|
| 509 |
+
},
|
| 510 |
+
{
|
| 511 |
+
"id": "python.mutable-default-argument",
|
| 512 |
+
"severity": "high",
|
| 513 |
+
"title": "A default argument is evaluated once, not once per call",
|
| 514 |
+
"rule": "Never use a list, dict, set or any other mutable object as a default argument. Default to None and build the object inside the function.",
|
| 515 |
+
"why": "The default expression runs once, when the def statement executes, and every call that omits the argument shares that one object. One request's appended item is visible to the next, so state leaks between callers that never met and the symptom grows with uptime rather than with load.",
|
| 516 |
+
"enforcement_layer": "runtime-parse",
|
| 517 |
+
"bypassed_by": [],
|
| 518 |
+
"wrong": "def collect(item, seen=[]):\n seen.append(item)\n return seen",
|
| 519 |
+
"correct": "def collect(item, seen=None):\n if seen is None:\n seen = []\n seen.append(item)\n return seen",
|
| 520 |
+
"tags": [
|
| 521 |
+
"defaults",
|
| 522 |
+
"shared-state",
|
| 523 |
+
"silent-failure"
|
| 524 |
+
],
|
| 525 |
+
"stack": "odoo",
|
| 526 |
+
"violation_pattern": "def\\s+\\w+\\([^)]*=\\s*(?:\\[\\]|\\{\\}|set\\(\\))",
|
| 527 |
+
"source": "https://docs.python.org/3/tutorial/controlflow.html#default-argument-values",
|
| 528 |
+
"likelihood": "unmeasured",
|
| 529 |
+
"advisory": false,
|
| 530 |
+
"probe": "a list keeps items from the previous call to the same function"
|
| 531 |
+
},
|
| 532 |
+
{
|
| 533 |
+
"id": "python.bare-except-catches-the-exit",
|
| 534 |
+
"severity": "high",
|
| 535 |
+
"title": "A bare except swallows Ctrl-C and sys.exit too",
|
| 536 |
+
"rule": "Catch the exceptions you can actually handle. Never write a bare except, and use except Exception only at a boundary that logs and re-raises.",
|
| 537 |
+
"why": "A bare except catches KeyboardInterrupt and SystemExit as well, so Ctrl-C stops working and sys.exit() is ignored. It also hides a typo exactly as readily as the error it was written for, and the failure then surfaces much later as wrong data rather than as a traceback pointing at its cause.",
|
| 538 |
+
"enforcement_layer": "runtime-parse",
|
| 539 |
+
"bypassed_by": [],
|
| 540 |
+
"wrong": "try:\n qty = int(row['qty'])\nexcept:\n qty = 0",
|
| 541 |
+
"correct": "try:\n qty = int(row['qty'])\nexcept (KeyError, TypeError, ValueError) as exc:\n raise ValueError('bad qty in row {0!r}'.format(row)) from exc",
|
| 542 |
+
"tags": [
|
| 543 |
+
"exceptions",
|
| 544 |
+
"error-handling",
|
| 545 |
+
"silent-failure"
|
| 546 |
+
],
|
| 547 |
+
"stack": "odoo",
|
| 548 |
+
"violation_pattern": "except\\s*:",
|
| 549 |
+
"source": "https://docs.python.org/3/tutorial/errors.html",
|
| 550 |
+
"likelihood": "unmeasured",
|
| 551 |
+
"advisory": false,
|
| 552 |
+
"probe": "ctrl-c stops working and the script will not exit"
|
| 553 |
+
},
|
| 554 |
+
{
|
| 555 |
+
"id": "python.eval-and-pickle-execute-their-input",
|
| 556 |
+
"severity": "critical",
|
| 557 |
+
"title": "eval, exec and pickle run whatever they are handed",
|
| 558 |
+
"rule": "Never pass a value that came from outside the process to eval, exec, or pickle.loads. Use json.loads, ast.literal_eval, or a schema.",
|
| 559 |
+
"why": "eval and exec execute arbitrary code, and unpickling constructs arbitrary objects by calling arbitrary callables named in the byte stream. A payload reaching any of the three is remote code execution rather than a validation gap, and nothing checked afterwards undoes it. Python's own pickle documentation opens with this warning.",
|
| 560 |
+
"enforcement_layer": "runtime-parse",
|
| 561 |
+
"bypassed_by": [],
|
| 562 |
+
"wrong": "config = eval(request.body)",
|
| 563 |
+
"correct": "import json\n\nconfig = json.loads(request.body)\nif not isinstance(config, dict):\n raise ValueError('config must be a JSON object')",
|
| 564 |
+
"tags": [
|
| 565 |
+
"security",
|
| 566 |
+
"deserialisation",
|
| 567 |
+
"remote-code-execution",
|
| 568 |
+
"input"
|
| 569 |
+
],
|
| 570 |
+
"stack": "odoo",
|
| 571 |
+
"violation_pattern": "\\beval\\(|\\bexec\\(|pickle\\.loads?\\(",
|
| 572 |
+
"source": "https://docs.python.org/3/library/pickle.html",
|
| 573 |
+
"likelihood": "unmeasured",
|
| 574 |
+
"advisory": false,
|
| 575 |
+
"probe": "turn a config string sent by a client into a Python object"
|
| 576 |
+
},
|
| 577 |
+
{
|
| 578 |
+
"id": "python.subprocess-shell-true-is-injection",
|
| 579 |
+
"severity": "critical",
|
| 580 |
+
"title": "shell=True hands your string to the shell",
|
| 581 |
+
"rule": "Pass subprocess a list of arguments and leave shell at its default. Never build a command string out of a value you did not write.",
|
| 582 |
+
"why": "With shell=True the string is interpreted by the shell, so a semicolon, a backtick or a $( ) in the middle of a filename becomes a second command running with your privileges. The argument-list form has no shell to interpret anything, which removes the class rather than filtering it.",
|
| 583 |
+
"enforcement_layer": "runtime-parse",
|
| 584 |
+
"bypassed_by": [
|
| 585 |
+
"os.system",
|
| 586 |
+
"os.popen",
|
| 587 |
+
"shlex.split on an already-joined string"
|
| 588 |
+
],
|
| 589 |
+
"wrong": "subprocess.run('convert {0} out.png'.format(path), shell=True)",
|
| 590 |
+
"correct": "subprocess.run(['convert', path, 'out.png'], check=True)",
|
| 591 |
+
"tags": [
|
| 592 |
+
"security",
|
| 593 |
+
"injection",
|
| 594 |
+
"subprocess",
|
| 595 |
+
"input"
|
| 596 |
+
],
|
| 597 |
+
"stack": "odoo",
|
| 598 |
+
"violation_pattern": "shell\\s*=\\s*True|os\\.system\\(|os\\.popen\\(",
|
| 599 |
+
"source": "https://docs.python.org/3/library/subprocess.html#security-considerations",
|
| 600 |
+
"likelihood": "unmeasured",
|
| 601 |
+
"advisory": false,
|
| 602 |
+
"probe": "run a command line built from a filename the user gave me"
|
| 603 |
+
},
|
| 604 |
+
{
|
| 605 |
+
"id": "python.path-join-does-not-contain-a-path",
|
| 606 |
+
"severity": "high",
|
| 607 |
+
"title": "os.path.join discards everything before an absolute component",
|
| 608 |
+
"rule": "Never join a caller-supplied fragment onto a base directory and assume the result is inside it. Resolve both and compare before opening anything.",
|
| 609 |
+
"why": "os.path.join('/srv/uploads', '/etc/passwd') returns '/etc/passwd'. An absolute second argument discards the first entirely, and a fragment of '..' walks out more slowly. Either turns an upload handler into arbitrary filesystem access, and the join itself raises nothing.",
|
| 610 |
+
"enforcement_layer": "runtime-parse",
|
| 611 |
+
"bypassed_by": [
|
| 612 |
+
"symlinks inside the base directory"
|
| 613 |
+
],
|
| 614 |
+
"wrong": "full = os.path.join(UPLOAD_DIR, request.args['name'])\nreturn open(full, 'rb').read()",
|
| 615 |
+
"correct": "base = os.path.realpath(UPLOAD_DIR)\nfull = os.path.realpath(os.path.join(base, request.args['name']))\nif os.path.commonpath([base, full]) != base:\n raise ValueError('path escapes the upload directory')\nreturn open(full, 'rb').read()",
|
| 616 |
+
"tags": [
|
| 617 |
+
"security",
|
| 618 |
+
"paths",
|
| 619 |
+
"traversal",
|
| 620 |
+
"input"
|
| 621 |
+
],
|
| 622 |
+
"stack": "odoo",
|
| 623 |
+
"violation_pattern": "os\\.path\\.join\\([^)]*\\b(?:request|args|params|payload|form|user_input|uploaded|filename)\\b",
|
| 624 |
+
"satisfied_pattern": "commonpath|commonprefix|relpath|is_relative_to",
|
| 625 |
+
"source": "https://docs.python.org/3/library/os.path.html#os.path.join",
|
| 626 |
+
"likelihood": "unmeasured",
|
| 627 |
+
"advisory": false,
|
| 628 |
+
"probe": "open a file the user named inside my uploads directory"
|
| 629 |
+
},
|
| 630 |
+
{
|
| 631 |
+
"id": "python.decimal-for-money-not-float",
|
| 632 |
+
"severity": "high",
|
| 633 |
+
"title": "Binary floats cannot hold a decimal amount",
|
| 634 |
+
"rule": "Use decimal.Decimal for money, built from the original string. Never route the value through float on the way, including inside Decimal().",
|
| 635 |
+
"why": "0.1 + 0.2 is not 0.3 in binary floating point, so a total drifts by fractions of a unit and a reconciliation over a large batch fails for no visible reason. Decimal(0.1) carries the same error, because the float was already wrong before Decimal was given it; Decimal('0.1') is exact.",
|
| 636 |
+
"enforcement_layer": "runtime-parse",
|
| 637 |
+
"bypassed_by": [
|
| 638 |
+
"json.loads, which parses every JSON number as a float"
|
| 639 |
+
],
|
| 640 |
+
"wrong": "price = float(payload['price'])\ntotal = price * qty",
|
| 641 |
+
"correct": "from decimal import Decimal\n\nprice = Decimal(payload['price']) # from the string, never through float\ntotal = price * qty",
|
| 642 |
+
"tags": [
|
| 643 |
+
"money",
|
| 644 |
+
"precision",
|
| 645 |
+
"floats",
|
| 646 |
+
"decimal"
|
| 647 |
+
],
|
| 648 |
+
"stack": "odoo",
|
| 649 |
+
"violation_pattern": "(?:price|amount|total|cost|balance|fee|tax|subtotal|payment|salary)\\w*\\s*=\\s*float\\(|float\\(\\s*[^)]*\\b(?:price|amount|cost|balance|fee|tax|subtotal|payment|salary)\\b",
|
| 650 |
+
"satisfied_pattern": "Decimal",
|
| 651 |
+
"source": "https://docs.python.org/3/library/decimal.html",
|
| 652 |
+
"likelihood": "unmeasured",
|
| 653 |
+
"advisory": false,
|
| 654 |
+
"probe": "totals are off by a fraction of a penny after adding prices up"
|
| 655 |
+
},
|
| 656 |
+
{
|
| 657 |
+
"id": "python.naive-and-aware-datetimes-do-not-compare",
|
| 658 |
+
"severity": "medium",
|
| 659 |
+
"title": "A naive datetime cannot be compared with an aware one",
|
| 660 |
+
"rule": "Work in timezone-aware UTC from the moment a value enters the process. Never call datetime.now() or utcnow() without a tzinfo.",
|
| 661 |
+
"why": "Comparing a naive datetime with an aware one raises TypeError. datetime.now() is naive, and anything parsed from an ISO string carrying an offset is aware, so the two meet for the first time in whichever branch is exercised least -- usually expiry, usually in production.",
|
| 662 |
+
"enforcement_layer": "runtime-parse",
|
| 663 |
+
"bypassed_by": [],
|
| 664 |
+
"wrong": "if expires_at < datetime.datetime.now():\n raise ValueError('token expired')",
|
| 665 |
+
"correct": "from datetime import datetime, timezone\n\nif expires_at < datetime.now(timezone.utc):\n raise ValueError('token expired')",
|
| 666 |
+
"tags": [
|
| 667 |
+
"datetime",
|
| 668 |
+
"timezone",
|
| 669 |
+
"comparison"
|
| 670 |
+
],
|
| 671 |
+
"stack": "odoo",
|
| 672 |
+
"violation_pattern": "datetime\\.now\\(\\s*\\)|utcnow\\(\\s*\\)",
|
| 673 |
+
"source": "https://docs.python.org/3/library/datetime.html",
|
| 674 |
+
"likelihood": "unmeasured",
|
| 675 |
+
"advisory": false,
|
| 676 |
+
"probe": "TypeError comparing offset-naive and offset-aware datetimes"
|
| 677 |
}
|
| 678 |
]
|
|
@@ -1,197 +0,0 @@
|
|
| 1 |
-
[
|
| 2 |
-
{
|
| 3 |
-
"id": "python.assert-is-not-validation",
|
| 4 |
-
"severity": "critical",
|
| 5 |
-
"title": "assert disappears under -O, so it cannot enforce anything",
|
| 6 |
-
"rule": "Never use assert to validate input or enforce an invariant. Raise an explicit exception. Reserve assert for conditions that are impossible unless the program is already broken.",
|
| 7 |
-
"why": "Python removes every assert statement when the interpreter runs with -O or PYTHONOPTIMIZE set, which is the default in a good many production images. The check is not weakened, it is absent -- so invalid input passes in exactly the environment where it matters, and passes in development, where the flag is off.",
|
| 8 |
-
"enforcement_layer": "runtime-parse",
|
| 9 |
-
"bypassed_by": [
|
| 10 |
-
"python -O",
|
| 11 |
-
"PYTHONOPTIMIZE=1",
|
| 12 |
-
"a .pyc built with optimisation"
|
| 13 |
-
],
|
| 14 |
-
"wrong": "def withdraw(account, amount):\n assert amount > 0, 'amount must be positive'\n account.balance -= amount",
|
| 15 |
-
"correct": "def withdraw(account, amount):\n if amount <= 0:\n raise ValueError('amount must be positive, got {0!r}'.format(amount))\n account.balance -= amount",
|
| 16 |
-
"tags": [
|
| 17 |
-
"assert",
|
| 18 |
-
"input-validation",
|
| 19 |
-
"optimisation",
|
| 20 |
-
"silent-failure"
|
| 21 |
-
],
|
| 22 |
-
"stack": "python",
|
| 23 |
-
"violation_pattern": "(?<=\\n)[ \\t]*assert\\s+",
|
| 24 |
-
"source": "https://docs.python.org/3/reference/simple_stmts.html#the-assert-statement",
|
| 25 |
-
"likelihood": "unmeasured",
|
| 26 |
-
"advisory": false,
|
| 27 |
-
"probe": "check that an amount is positive before using it"
|
| 28 |
-
},
|
| 29 |
-
{
|
| 30 |
-
"id": "python.mutable-default-argument",
|
| 31 |
-
"severity": "high",
|
| 32 |
-
"title": "A default argument is evaluated once, not once per call",
|
| 33 |
-
"rule": "Never use a list, dict, set or any other mutable object as a default argument. Default to None and build the object inside the function.",
|
| 34 |
-
"why": "The default expression runs once, when the def statement executes, and every call that omits the argument shares that one object. One request's appended item is visible to the next, so state leaks between callers that never met and the symptom grows with uptime rather than with load.",
|
| 35 |
-
"enforcement_layer": "runtime-parse",
|
| 36 |
-
"bypassed_by": [],
|
| 37 |
-
"wrong": "def collect(item, seen=[]):\n seen.append(item)\n return seen",
|
| 38 |
-
"correct": "def collect(item, seen=None):\n if seen is None:\n seen = []\n seen.append(item)\n return seen",
|
| 39 |
-
"tags": [
|
| 40 |
-
"defaults",
|
| 41 |
-
"shared-state",
|
| 42 |
-
"silent-failure"
|
| 43 |
-
],
|
| 44 |
-
"stack": "python",
|
| 45 |
-
"violation_pattern": "def\\s+\\w+\\([^)]*=\\s*(?:\\[\\]|\\{\\}|set\\(\\))",
|
| 46 |
-
"source": "https://docs.python.org/3/tutorial/controlflow.html#default-argument-values",
|
| 47 |
-
"likelihood": "unmeasured",
|
| 48 |
-
"advisory": false,
|
| 49 |
-
"probe": "a list keeps items from the previous call to the same function"
|
| 50 |
-
},
|
| 51 |
-
{
|
| 52 |
-
"id": "python.bare-except-catches-the-exit",
|
| 53 |
-
"severity": "high",
|
| 54 |
-
"title": "A bare except swallows Ctrl-C and sys.exit too",
|
| 55 |
-
"rule": "Catch the exceptions you can actually handle. Never write a bare except, and use except Exception only at a boundary that logs and re-raises.",
|
| 56 |
-
"why": "A bare except catches KeyboardInterrupt and SystemExit as well, so Ctrl-C stops working and sys.exit() is ignored. It also hides a typo exactly as readily as the error it was written for, and the failure then surfaces much later as wrong data rather than as a traceback pointing at its cause.",
|
| 57 |
-
"enforcement_layer": "runtime-parse",
|
| 58 |
-
"bypassed_by": [],
|
| 59 |
-
"wrong": "try:\n qty = int(row['qty'])\nexcept:\n qty = 0",
|
| 60 |
-
"correct": "try:\n qty = int(row['qty'])\nexcept (KeyError, TypeError, ValueError) as exc:\n raise ValueError('bad qty in row {0!r}'.format(row)) from exc",
|
| 61 |
-
"tags": [
|
| 62 |
-
"exceptions",
|
| 63 |
-
"error-handling",
|
| 64 |
-
"silent-failure"
|
| 65 |
-
],
|
| 66 |
-
"stack": "python",
|
| 67 |
-
"violation_pattern": "except\\s*:",
|
| 68 |
-
"source": "https://docs.python.org/3/tutorial/errors.html",
|
| 69 |
-
"likelihood": "unmeasured",
|
| 70 |
-
"advisory": false,
|
| 71 |
-
"probe": "ctrl-c stops working and the script will not exit"
|
| 72 |
-
},
|
| 73 |
-
{
|
| 74 |
-
"id": "python.eval-and-pickle-execute-their-input",
|
| 75 |
-
"severity": "critical",
|
| 76 |
-
"title": "eval, exec and pickle run whatever they are handed",
|
| 77 |
-
"rule": "Never pass a value that came from outside the process to eval, exec, or pickle.loads. Use json.loads, ast.literal_eval, or a schema.",
|
| 78 |
-
"why": "eval and exec execute arbitrary code, and unpickling constructs arbitrary objects by calling arbitrary callables named in the byte stream. A payload reaching any of the three is remote code execution rather than a validation gap, and nothing checked afterwards undoes it. Python's own pickle documentation opens with this warning.",
|
| 79 |
-
"enforcement_layer": "runtime-parse",
|
| 80 |
-
"bypassed_by": [],
|
| 81 |
-
"wrong": "config = eval(request.body)",
|
| 82 |
-
"correct": "import json\n\nconfig = json.loads(request.body)\nif not isinstance(config, dict):\n raise ValueError('config must be a JSON object')",
|
| 83 |
-
"tags": [
|
| 84 |
-
"security",
|
| 85 |
-
"deserialisation",
|
| 86 |
-
"remote-code-execution",
|
| 87 |
-
"input"
|
| 88 |
-
],
|
| 89 |
-
"stack": "python",
|
| 90 |
-
"violation_pattern": "\\beval\\(|\\bexec\\(|pickle\\.loads?\\(",
|
| 91 |
-
"source": "https://docs.python.org/3/library/pickle.html",
|
| 92 |
-
"likelihood": "unmeasured",
|
| 93 |
-
"advisory": false,
|
| 94 |
-
"probe": "turn a config string sent by a client into a Python object"
|
| 95 |
-
},
|
| 96 |
-
{
|
| 97 |
-
"id": "python.subprocess-shell-true-is-injection",
|
| 98 |
-
"severity": "critical",
|
| 99 |
-
"title": "shell=True hands your string to the shell",
|
| 100 |
-
"rule": "Pass subprocess a list of arguments and leave shell at its default. Never build a command string out of a value you did not write.",
|
| 101 |
-
"why": "With shell=True the string is interpreted by the shell, so a semicolon, a backtick or a $( ) in the middle of a filename becomes a second command running with your privileges. The argument-list form has no shell to interpret anything, which removes the class rather than filtering it.",
|
| 102 |
-
"enforcement_layer": "runtime-parse",
|
| 103 |
-
"bypassed_by": [
|
| 104 |
-
"os.system",
|
| 105 |
-
"os.popen",
|
| 106 |
-
"shlex.split on an already-joined string"
|
| 107 |
-
],
|
| 108 |
-
"wrong": "subprocess.run('convert {0} out.png'.format(path), shell=True)",
|
| 109 |
-
"correct": "subprocess.run(['convert', path, 'out.png'], check=True)",
|
| 110 |
-
"tags": [
|
| 111 |
-
"security",
|
| 112 |
-
"injection",
|
| 113 |
-
"subprocess",
|
| 114 |
-
"input"
|
| 115 |
-
],
|
| 116 |
-
"stack": "python",
|
| 117 |
-
"violation_pattern": "shell\\s*=\\s*True|os\\.system\\(|os\\.popen\\(",
|
| 118 |
-
"source": "https://docs.python.org/3/library/subprocess.html#security-considerations",
|
| 119 |
-
"likelihood": "unmeasured",
|
| 120 |
-
"advisory": false,
|
| 121 |
-
"probe": "run a command line built from a filename the user gave me"
|
| 122 |
-
},
|
| 123 |
-
{
|
| 124 |
-
"id": "python.path-join-does-not-contain-a-path",
|
| 125 |
-
"severity": "high",
|
| 126 |
-
"title": "os.path.join discards everything before an absolute component",
|
| 127 |
-
"rule": "Never join a caller-supplied fragment onto a base directory and assume the result is inside it. Resolve both and compare before opening anything.",
|
| 128 |
-
"why": "os.path.join('/srv/uploads', '/etc/passwd') returns '/etc/passwd'. An absolute second argument discards the first entirely, and a fragment of '..' walks out more slowly. Either turns an upload handler into arbitrary filesystem access, and the join itself raises nothing.",
|
| 129 |
-
"enforcement_layer": "runtime-parse",
|
| 130 |
-
"bypassed_by": [
|
| 131 |
-
"symlinks inside the base directory"
|
| 132 |
-
],
|
| 133 |
-
"wrong": "full = os.path.join(UPLOAD_DIR, request.args['name'])\nreturn open(full, 'rb').read()",
|
| 134 |
-
"correct": "base = os.path.realpath(UPLOAD_DIR)\nfull = os.path.realpath(os.path.join(base, request.args['name']))\nif os.path.commonpath([base, full]) != base:\n raise ValueError('path escapes the upload directory')\nreturn open(full, 'rb').read()",
|
| 135 |
-
"tags": [
|
| 136 |
-
"security",
|
| 137 |
-
"paths",
|
| 138 |
-
"traversal",
|
| 139 |
-
"input"
|
| 140 |
-
],
|
| 141 |
-
"stack": "python",
|
| 142 |
-
"violation_pattern": "os\\.path\\.join\\([^)]*\\b(?:request|args|params|payload|form|user_input|uploaded|filename)\\b",
|
| 143 |
-
"satisfied_pattern": "commonpath|commonprefix|relpath|is_relative_to",
|
| 144 |
-
"source": "https://docs.python.org/3/library/os.path.html#os.path.join",
|
| 145 |
-
"likelihood": "unmeasured",
|
| 146 |
-
"advisory": false,
|
| 147 |
-
"probe": "open a file the user named inside my uploads directory"
|
| 148 |
-
},
|
| 149 |
-
{
|
| 150 |
-
"id": "python.decimal-for-money-not-float",
|
| 151 |
-
"severity": "high",
|
| 152 |
-
"title": "Binary floats cannot hold a decimal amount",
|
| 153 |
-
"rule": "Use decimal.Decimal for money, built from the original string. Never route the value through float on the way, including inside Decimal().",
|
| 154 |
-
"why": "0.1 + 0.2 is not 0.3 in binary floating point, so a total drifts by fractions of a unit and a reconciliation over a large batch fails for no visible reason. Decimal(0.1) carries the same error, because the float was already wrong before Decimal was given it; Decimal('0.1') is exact.",
|
| 155 |
-
"enforcement_layer": "runtime-parse",
|
| 156 |
-
"bypassed_by": [
|
| 157 |
-
"json.loads, which parses every JSON number as a float"
|
| 158 |
-
],
|
| 159 |
-
"wrong": "price = float(payload['price'])\ntotal = price * qty",
|
| 160 |
-
"correct": "from decimal import Decimal\n\nprice = Decimal(payload['price']) # from the string, never through float\ntotal = price * qty",
|
| 161 |
-
"tags": [
|
| 162 |
-
"money",
|
| 163 |
-
"precision",
|
| 164 |
-
"floats",
|
| 165 |
-
"decimal"
|
| 166 |
-
],
|
| 167 |
-
"stack": "python",
|
| 168 |
-
"violation_pattern": "(?:price|amount|total|cost|balance|fee|tax|subtotal|payment|salary)\\w*\\s*=\\s*float\\(|float\\(\\s*[^)]*\\b(?:price|amount|cost|balance|fee|tax|subtotal|payment|salary)\\b",
|
| 169 |
-
"satisfied_pattern": "Decimal",
|
| 170 |
-
"source": "https://docs.python.org/3/library/decimal.html",
|
| 171 |
-
"likelihood": "unmeasured",
|
| 172 |
-
"advisory": false,
|
| 173 |
-
"probe": "totals are off by a fraction of a penny after adding prices up"
|
| 174 |
-
},
|
| 175 |
-
{
|
| 176 |
-
"id": "python.naive-and-aware-datetimes-do-not-compare",
|
| 177 |
-
"severity": "medium",
|
| 178 |
-
"title": "A naive datetime cannot be compared with an aware one",
|
| 179 |
-
"rule": "Work in timezone-aware UTC from the moment a value enters the process. Never call datetime.now() or utcnow() without a tzinfo.",
|
| 180 |
-
"why": "Comparing a naive datetime with an aware one raises TypeError. datetime.now() is naive, and anything parsed from an ISO string carrying an offset is aware, so the two meet for the first time in whichever branch is exercised least -- usually expiry, usually in production.",
|
| 181 |
-
"enforcement_layer": "runtime-parse",
|
| 182 |
-
"bypassed_by": [],
|
| 183 |
-
"wrong": "if expires_at < datetime.datetime.now():\n raise ValueError('token expired')",
|
| 184 |
-
"correct": "from datetime import datetime, timezone\n\nif expires_at < datetime.now(timezone.utc):\n raise ValueError('token expired')",
|
| 185 |
-
"tags": [
|
| 186 |
-
"datetime",
|
| 187 |
-
"timezone",
|
| 188 |
-
"comparison"
|
| 189 |
-
],
|
| 190 |
-
"stack": "python",
|
| 191 |
-
"violation_pattern": "datetime\\.now\\(\\s*\\)|utcnow\\(\\s*\\)",
|
| 192 |
-
"source": "https://docs.python.org/3/library/datetime.html",
|
| 193 |
-
"likelihood": "unmeasured",
|
| 194 |
-
"advisory": false,
|
| 195 |
-
"probe": "TypeError comparing offset-naive and offset-aware datetimes"
|
| 196 |
-
}
|
| 197 |
-
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,238 +0,0 @@
|
|
| 1 |
-
[
|
| 2 |
-
{
|
| 3 |
-
"id": "typescript.types-are-erased-at-runtime",
|
| 4 |
-
"severity": "critical",
|
| 5 |
-
"title": "Static types guarantee nothing about external data",
|
| 6 |
-
"rule": "Every value crossing a boundary must be parsed by a runtime schema. A type assertion on external data is a lie the compiler cannot catch.",
|
| 7 |
-
"why": "Interfaces and type annotations are erased during compilation. Casting an API response with 'as User' produces zero runtime checks, so a changed or malformed payload propagates as undefined deep inside unrelated code, far from its origin.",
|
| 8 |
-
"enforcement_layer": "runtime-parse",
|
| 9 |
-
"bypassed_by": [
|
| 10 |
-
"type assertions (as T)",
|
| 11 |
-
"any",
|
| 12 |
-
"unchecked JSON.parse"
|
| 13 |
-
],
|
| 14 |
-
"wrong": "const user = await res.json() as User;",
|
| 15 |
-
"correct": "const UserSchema = z.object({ id: z.number(), name: z.string() });\ntype User = z.infer<typeof UserSchema>;\n\nconst user = UserSchema.parse(await res.json());",
|
| 16 |
-
"tags": [
|
| 17 |
-
"zod",
|
| 18 |
-
"boundary",
|
| 19 |
-
"type-erasure"
|
| 20 |
-
],
|
| 21 |
-
"stack": "typescript",
|
| 22 |
-
"violation_pattern": "\\.json\\(\\)\\s+as\\s+\\w+|as\\s+unknown\\s+as\\s+\\w+",
|
| 23 |
-
"source": "https://www.typescriptlang.org/docs/handbook/2/everyday-types.html",
|
| 24 |
-
"likelihood": "unmeasured",
|
| 25 |
-
"advisory": false,
|
| 26 |
-
"probe": "cast the JSON from an API response to my interface"
|
| 27 |
-
},
|
| 28 |
-
{
|
| 29 |
-
"id": "typescript.derive-type-from-schema",
|
| 30 |
-
"severity": "high",
|
| 31 |
-
"title": "Derive the type from the schema, never maintain both",
|
| 32 |
-
"rule": "Write the schema first and derive the static type with z.infer. Never hand-write an interface alongside a schema for the same data.",
|
| 33 |
-
"why": "Two hand-maintained definitions drift. The compiler cannot detect the drift because each is internally consistent, so the type says one thing while the parser enforces another and the mismatch only appears at runtime.",
|
| 34 |
-
"enforcement_layer": "compile-time",
|
| 35 |
-
"bypassed_by": [],
|
| 36 |
-
"wrong": "interface Order { id: number; total: number; }\nconst OrderSchema = z.object({ id: z.number(), total: z.number(), currency: z.string() });",
|
| 37 |
-
"correct": "const OrderSchema = z.object({\n id: z.number(),\n total: z.number(),\n currency: z.string(),\n});\ntype Order = z.infer<typeof OrderSchema>;",
|
| 38 |
-
"tags": [
|
| 39 |
-
"zod",
|
| 40 |
-
"single-source-of-truth",
|
| 41 |
-
"drift"
|
| 42 |
-
],
|
| 43 |
-
"stack": "typescript",
|
| 44 |
-
"violation_pattern": "interface\\s+\\w+\\s*\\{[\\s\\S]*?\\}[\\s\\S]*?z\\.object\\(",
|
| 45 |
-
"source": "https://zod.dev/api",
|
| 46 |
-
"likelihood": "unmeasured",
|
| 47 |
-
"advisory": false,
|
| 48 |
-
"probe": "keep my interface and my validation schema from drifting apart"
|
| 49 |
-
},
|
| 50 |
-
{
|
| 51 |
-
"id": "typescript.odoo-returns-false-for-every-empty-field",
|
| 52 |
-
"severity": "critical",
|
| 53 |
-
"title": "Odoo JSON-RPC returns false for every empty field",
|
| 54 |
-
"rule": "Model every optional Odoo scalar as a union with literal false, normalised through one shared helper. This applies to Char, Text, Date, Datetime, Binary and Many2one alike, not only to relations.",
|
| 55 |
-
"why": "Odoo returns boolean false for any empty value, never null, never an empty string. Typing an optional email as string().nullable() throws at the parse boundary the first time a partner has no email, which is ordinary production data rather than an edge case.",
|
| 56 |
-
"enforcement_layer": "runtime-parse",
|
| 57 |
-
"bypassed_by": [],
|
| 58 |
-
"wrong": "const PartnerSchema = z.object({\n email: z.string().nullable(),\n parent_id: z.array(z.number()),\n});",
|
| 59 |
-
"correct": "const odooNullable = <T extends z.ZodTypeAny>(inner: T) =>\n z.union([inner, z.literal(false)]).transform((v) => (v === false ? null : v));\n\nconst PartnerSchema = z.object({\n id: z.number().int(),\n name: z.string(),\n email: odooNullable(z.string().email()),\n parent_id: odooNullable(z.tuple([z.number().int(), z.string()])),\n});",
|
| 60 |
-
"tags": [
|
| 61 |
-
"odoo",
|
| 62 |
-
"json-rpc",
|
| 63 |
-
"boundary",
|
| 64 |
-
"zod"
|
| 65 |
-
],
|
| 66 |
-
"violation_pattern": "z\\.(?:string|number|date|coerce)[^\\n]*\\.nullable\\(\\)",
|
| 67 |
-
"satisfied_pattern": "odooNullable|z\\.literal\\(false\\)",
|
| 68 |
-
"stack": "typescript",
|
| 69 |
-
"source": "https://www.odoo.com/documentation/18.0/developer/reference/external_api.html",
|
| 70 |
-
"likelihood": "unmeasured",
|
| 71 |
-
"advisory": false,
|
| 72 |
-
"probe": "parsing fails because Odoo sent false for a partner with no email"
|
| 73 |
-
},
|
| 74 |
-
{
|
| 75 |
-
"id": "typescript.odoo-many2one-is-a-tuple",
|
| 76 |
-
"severity": "high",
|
| 77 |
-
"title": "A set Many2one is a two-element tuple, not an array of ids",
|
| 78 |
-
"rule": "Model a populated Many2one as z.tuple([z.number(), z.string()]) holding id and display_name. Model an empty one as false.",
|
| 79 |
-
"why": "Odoo read() returns [id, display_name] for a set Many2one. Typing it as an array of numbers parses the id but discards display_name and fails the moment the schema meets real data, because element one is a string.",
|
| 80 |
-
"enforcement_layer": "runtime-parse",
|
| 81 |
-
"bypassed_by": [],
|
| 82 |
-
"wrong": "parent_id: z.array(z.number())",
|
| 83 |
-
"correct": "parent_id: odooNullable(z.tuple([z.number().int(), z.string()]))",
|
| 84 |
-
"tags": [
|
| 85 |
-
"odoo",
|
| 86 |
-
"json-rpc",
|
| 87 |
-
"many2one"
|
| 88 |
-
],
|
| 89 |
-
"stack": "typescript",
|
| 90 |
-
"violation_pattern": "_id\\s*:\\s*(?:\\w+\\()?z\\.array\\(\\s*z\\.number",
|
| 91 |
-
"source": "https://www.odoo.com/documentation/18.0/developer/reference/external_api.html",
|
| 92 |
-
"likelihood": "unmeasured",
|
| 93 |
-
"advisory": false,
|
| 94 |
-
"probe": "read the parent company name off a many2one field from Odoo"
|
| 95 |
-
},
|
| 96 |
-
{
|
| 97 |
-
"id": "typescript.odoo-read-returns-an-array",
|
| 98 |
-
"severity": "medium",
|
| 99 |
-
"title": "Odoo read returns an array even for one id",
|
| 100 |
-
"rule": "Parse the response of read() with z.array(Schema) and index into it, rather than parsing it as a single object.",
|
| 101 |
-
"why": "read() always returns a list of dictionaries. Parsing it directly against an object schema fails, and defensively coercing with Array.isArray hides whether the call actually returned the record or an empty list for a missing id.",
|
| 102 |
-
"enforcement_layer": "runtime-parse",
|
| 103 |
-
"bypassed_by": [],
|
| 104 |
-
"wrong": "const partner = PartnerSchema.parse(await callOdoo('res.partner', 'read', [id]));",
|
| 105 |
-
"correct": "const rows = z.array(PartnerSchema).parse(\n await callOdoo('res.partner', 'read', [id]),\n);\nif (rows.length === 0) throw new Error(`res.partner ${id} not found or not readable`);\nconst partner = rows[0];",
|
| 106 |
-
"tags": [
|
| 107 |
-
"odoo",
|
| 108 |
-
"json-rpc",
|
| 109 |
-
"read"
|
| 110 |
-
],
|
| 111 |
-
"stack": "typescript",
|
| 112 |
-
"violation_pattern": "\\w+Schema\\.(?:safe)?[Pp]arse\\(\\s*await[^)]*(?:callOdoo|jsonrpc|'read'|\\\"read\\\")",
|
| 113 |
-
"satisfied_pattern": "z\\.array\\(",
|
| 114 |
-
"source": "https://www.odoo.com/documentation/18.0/developer/reference/external_api.html",
|
| 115 |
-
"likelihood": "unmeasured",
|
| 116 |
-
"advisory": false,
|
| 117 |
-
"probe": "parse the result of an Odoo read call for a single id"
|
| 118 |
-
},
|
| 119 |
-
{
|
| 120 |
-
"id": "typescript.unknown-not-any-at-boundaries",
|
| 121 |
-
"severity": "high",
|
| 122 |
-
"title": "Use unknown at boundaries, never any",
|
| 123 |
-
"rule": "Type every value that arrives unparsed as unknown: the body of an incoming webhook, a request payload, a handler parameter, the result of JSON.parse, anything from an untyped import. Reserve any for nothing.",
|
| 124 |
-
"why": "any silently disables checking for every expression derived from it, so one any on a webhook handler's payload removes type safety from the whole call chain downstream, and the compiler reports nothing. unknown refuses every use until the value has been parsed or narrowed, which is the behaviour that was wanted from the annotation in the first place.",
|
| 125 |
-
"enforcement_layer": "compile-time",
|
| 126 |
-
"bypassed_by": [
|
| 127 |
-
"explicit any",
|
| 128 |
-
"implicit any from untyped imports"
|
| 129 |
-
],
|
| 130 |
-
"wrong": "function handle(payload: any) { return payload.order.total; }",
|
| 131 |
-
"correct": "function handle(payload: unknown) {\n const { order } = WebhookSchema.parse(payload);\n return order.total;\n}",
|
| 132 |
-
"tags": [
|
| 133 |
-
"any",
|
| 134 |
-
"unknown",
|
| 135 |
-
"boundary",
|
| 136 |
-
"webhook",
|
| 137 |
-
"payload",
|
| 138 |
-
"request-body",
|
| 139 |
-
"handler",
|
| 140 |
-
"external-input"
|
| 141 |
-
],
|
| 142 |
-
"stack": "typescript",
|
| 143 |
-
"violation_pattern": ":\\s*any\\b",
|
| 144 |
-
"source": "https://www.typescriptlang.org/docs/handbook/2/everyday-types.html",
|
| 145 |
-
"likelihood": "unmeasured",
|
| 146 |
-
"advisory": false,
|
| 147 |
-
"probe": "what type should the payload of an incoming webhook handler have"
|
| 148 |
-
},
|
| 149 |
-
{
|
| 150 |
-
"id": "typescript.safeparse-when-failure-is-expected",
|
| 151 |
-
"severity": "medium",
|
| 152 |
-
"title": "Choose parse or safeParse deliberately",
|
| 153 |
-
"rule": "Use safeParse where a malformed value is expected and recoverable; use parse only where throwing is the correct response. State which and why.",
|
| 154 |
-
"why": "parse throws a ZodError that an unprepared caller turns into a 500 and an opaque stack trace. safeParse returns a discriminated result carrying the field-level issues, which is what a user-facing error or a per-row import skip actually needs.",
|
| 155 |
-
"enforcement_layer": "runtime-parse",
|
| 156 |
-
"bypassed_by": [],
|
| 157 |
-
"wrong": "const row = RowSchema.parse(csvRow); // aborts the whole import on row 4000",
|
| 158 |
-
"correct": "const result = RowSchema.safeParse(csvRow);\nif (!result.success) {\n errors.push({ line, issues: result.error.issues });\n continue;\n}\nconst row = result.data;",
|
| 159 |
-
"tags": [
|
| 160 |
-
"zod",
|
| 161 |
-
"error-handling"
|
| 162 |
-
],
|
| 163 |
-
"stack": "typescript",
|
| 164 |
-
"likelihood": "unmeasured",
|
| 165 |
-
"advisory": false,
|
| 166 |
-
"source": "https://zod.dev/api",
|
| 167 |
-
"probe": "skip the bad rows in a CSV import instead of aborting the whole file",
|
| 168 |
-
"no_check_reason": "Whether parse or safeParse is correct depends on whether the caller can recover, which is context the snippet does not contain. Any pattern here would be noise."
|
| 169 |
-
},
|
| 170 |
-
{
|
| 171 |
-
"id": "typescript.validate-env-at-startup",
|
| 172 |
-
"severity": "high",
|
| 173 |
-
"title": "Parse environment variables once at startup",
|
| 174 |
-
"rule": "Validate the whole environment against a schema at process start and export the parsed object. Never read process.env deep inside application code.",
|
| 175 |
-
"why": "process.env values are string or undefined and are unchecked. A missing variable read lazily inside a rarely-exercised branch fails in production at the worst moment, whereas a startup parse turns the same fault into a deployment that refuses to boot.",
|
| 176 |
-
"enforcement_layer": "startup",
|
| 177 |
-
"bypassed_by": [],
|
| 178 |
-
"wrong": "const url = process.env.ODOO_URL; // string | undefined, used 40 files away",
|
| 179 |
-
"correct": "const EnvSchema = z.object({\n ODOO_URL: z.string().url(),\n ODOO_DB: z.string().min(1),\n PORT: z.coerce.number().int().positive().default(3000),\n});\nexport const env = EnvSchema.parse(process.env);",
|
| 180 |
-
"tags": [
|
| 181 |
-
"config",
|
| 182 |
-
"startup",
|
| 183 |
-
"fail-fast"
|
| 184 |
-
],
|
| 185 |
-
"stack": "typescript",
|
| 186 |
-
"violation_pattern": "process\\.env\\.[A-Z][A-Z0-9_]*",
|
| 187 |
-
"source": "https://nodejs.org/api/process.html",
|
| 188 |
-
"likelihood": "unmeasured",
|
| 189 |
-
"advisory": false,
|
| 190 |
-
"probe": "make sure the required environment variables are set before the app runs"
|
| 191 |
-
},
|
| 192 |
-
{
|
| 193 |
-
"id": "typescript.discriminated-union-for-variants",
|
| 194 |
-
"severity": "medium",
|
| 195 |
-
"title": "Use discriminatedUnion for tagged variant payloads",
|
| 196 |
-
"rule": "When a payload has a literal tag field selecting its shape, use z.discriminatedUnion on that field rather than z.union.",
|
| 197 |
-
"why": "A plain union tries every member and reports the failure of all of them, producing an unreadable error and slower parsing. discriminatedUnion selects the branch by tag, reports issues only for the intended shape, and narrows correctly in TypeScript afterwards.",
|
| 198 |
-
"enforcement_layer": "runtime-parse",
|
| 199 |
-
"bypassed_by": [],
|
| 200 |
-
"wrong": "z.union([PaymentSucceeded, PaymentFailed, PaymentPending])",
|
| 201 |
-
"correct": "z.discriminatedUnion('status', [\n z.object({ status: z.literal('succeeded'), chargeId: z.string() }),\n z.object({ status: z.literal('failed'), reason: z.string() }),\n z.object({ status: z.literal('pending') }),\n])",
|
| 202 |
-
"tags": [
|
| 203 |
-
"zod",
|
| 204 |
-
"unions",
|
| 205 |
-
"narrowing"
|
| 206 |
-
],
|
| 207 |
-
"stack": "typescript",
|
| 208 |
-
"violation_pattern": "z\\.union\\(\\[\\s*[A-Z]",
|
| 209 |
-
"satisfied_pattern": "z\\.discriminatedUnion",
|
| 210 |
-
"source": "https://zod.dev/api",
|
| 211 |
-
"likelihood": "unmeasured",
|
| 212 |
-
"advisory": false,
|
| 213 |
-
"probe": "parse a webhook whose body shape depends on a status field"
|
| 214 |
-
},
|
| 215 |
-
{
|
| 216 |
-
"id": "typescript.brand-validated-primitives",
|
| 217 |
-
"severity": "medium",
|
| 218 |
-
"title": "Brand validated primitives so they cannot be swapped for raw ones",
|
| 219 |
-
"rule": "Give checked primitives a branded type so a validated value is not assignable from an arbitrary string or number.",
|
| 220 |
-
"why": "Without branding, every string is assignable to every string-typed parameter, so a raw user input can be passed where a validated email or a trusted id is required and the compiler accepts it. Branding makes that a compile error.",
|
| 221 |
-
"enforcement_layer": "compile-time",
|
| 222 |
-
"bypassed_by": [
|
| 223 |
-
"casting through as"
|
| 224 |
-
],
|
| 225 |
-
"wrong": "function send(to: string) { /* any string at all */ }",
|
| 226 |
-
"correct": "const Email = z.string().email().brand<'Email'>();\ntype Email = z.infer<typeof Email>;\n\nfunction send(to: Email) { /* only a parsed Email */ }",
|
| 227 |
-
"tags": [
|
| 228 |
-
"branding",
|
| 229 |
-
"nominal-typing"
|
| 230 |
-
],
|
| 231 |
-
"stack": "typescript",
|
| 232 |
-
"likelihood": "unmeasured",
|
| 233 |
-
"advisory": false,
|
| 234 |
-
"source": "https://zod.dev/api",
|
| 235 |
-
"probe": "stop a raw string being passed where a checked email address is required",
|
| 236 |
-
"no_check_reason": "Absence of branding is not detectable: plain string parameters are correct in most code. Flagging them all would bury the real findings."
|
| 237 |
-
}
|
| 238 |
-
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,451 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"stack": "javascript",
|
| 3 |
-
"roots": [
|
| 4 |
-
"OpenSPP2-19.0",
|
| 5 |
-
"server-tools",
|
| 6 |
-
"account-financial-tools"
|
| 7 |
-
],
|
| 8 |
-
"files": 62,
|
| 9 |
-
"lines": 9304,
|
| 10 |
-
"checkers": 5,
|
| 11 |
-
"triggers": {
|
| 12 |
-
"files": 3144,
|
| 13 |
-
"models_declared": 631,
|
| 14 |
-
"models_deleted": 526,
|
| 15 |
-
"declared_and_deleted": 516,
|
| 16 |
-
"never_deleted": 115,
|
| 17 |
-
"multi_company": true
|
| 18 |
-
},
|
| 19 |
-
"hits": [
|
| 20 |
-
{
|
| 21 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 22 |
-
"severity": "high",
|
| 23 |
-
"line": 11,
|
| 24 |
-
"evidence": "const docUrl = session.spp_documentation_url || \"https://docs.openspp.org\";",
|
| 25 |
-
"suppressed": false,
|
| 26 |
-
"advisory": false,
|
| 27 |
-
"likelihood": "unmeasured",
|
| 28 |
-
"path": "OpenSPP2-19.0/spp_branding_kit/static/src/js/user_menu.js"
|
| 29 |
-
},
|
| 30 |
-
{
|
| 31 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 32 |
-
"severity": "high",
|
| 33 |
-
"line": 12,
|
| 34 |
-
"evidence": "const supportUrl = session.spp_support_url || \"https://openspp.org\";",
|
| 35 |
-
"suppressed": false,
|
| 36 |
-
"advisory": false,
|
| 37 |
-
"likelihood": "unmeasured",
|
| 38 |
-
"path": "OpenSPP2-19.0/spp_branding_kit/static/src/js/user_menu.js"
|
| 39 |
-
},
|
| 40 |
-
{
|
| 41 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 42 |
-
"severity": "high",
|
| 43 |
-
"line": 9,
|
| 44 |
-
"evidence": "const systemName = session.spp_system_name || \"OpenSPP Platform\";",
|
| 45 |
-
"suppressed": false,
|
| 46 |
-
"advisory": false,
|
| 47 |
-
"likelihood": "unmeasured",
|
| 48 |
-
"path": "OpenSPP2-19.0/spp_branding_kit/static/src/js/webclient.js"
|
| 49 |
-
},
|
| 50 |
-
{
|
| 51 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 52 |
-
"severity": "high",
|
| 53 |
-
"line": 128,
|
| 54 |
-
"evidence": "return this._currentProfile || this.props.profile || \"registry_individuals\";",
|
| 55 |
-
"suppressed": false,
|
| 56 |
-
"advisory": false,
|
| 57 |
-
"likelihood": "unmeasured",
|
| 58 |
-
"path": "OpenSPP2-19.0/spp_cel_widget/static/src/js/cel_editor.js"
|
| 59 |
-
},
|
| 60 |
-
{
|
| 61 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 62 |
-
"severity": "high",
|
| 63 |
-
"line": 243,
|
| 64 |
-
"evidence": "minHeight: this.props.minHeight || \"80px\",",
|
| 65 |
-
"suppressed": false,
|
| 66 |
-
"advisory": false,
|
| 67 |
-
"likelihood": "unmeasured",
|
| 68 |
-
"path": "OpenSPP2-19.0/spp_cel_widget/static/src/js/cel_editor.js"
|
| 69 |
-
},
|
| 70 |
-
{
|
| 71 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 72 |
-
"severity": "high",
|
| 73 |
-
"line": 467,
|
| 74 |
-
"evidence": "return firstError?.message || \"Invalid expression\";",
|
| 75 |
-
"suppressed": false,
|
| 76 |
-
"advisory": false,
|
| 77 |
-
"likelihood": "unmeasured",
|
| 78 |
-
"path": "OpenSPP2-19.0/spp_cel_widget/static/src/js/cel_editor.js"
|
| 79 |
-
},
|
| 80 |
-
{
|
| 81 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 82 |
-
"severity": "high",
|
| 83 |
-
"line": 78,
|
| 84 |
-
"evidence": "message: error.message || \"Validation failed\",",
|
| 85 |
-
"suppressed": false,
|
| 86 |
-
"advisory": false,
|
| 87 |
-
"likelihood": "unmeasured",
|
| 88 |
-
"path": "OpenSPP2-19.0/spp_cel_widget/static/src/js/cel_symbol_service.js"
|
| 89 |
-
},
|
| 90 |
-
{
|
| 91 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 92 |
-
"severity": "high",
|
| 93 |
-
"line": 198,
|
| 94 |
-
"evidence": "if (!prefix || \"andornot\".includes(prefix)) {",
|
| 95 |
-
"suppressed": false,
|
| 96 |
-
"advisory": false,
|
| 97 |
-
"likelihood": "unmeasured",
|
| 98 |
-
"path": "OpenSPP2-19.0/spp_cel_widget/static/src/js/cel_language/autocomplete.js"
|
| 99 |
-
},
|
| 100 |
-
{
|
| 101 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 102 |
-
"severity": "high",
|
| 103 |
-
"line": 105,
|
| 104 |
-
"evidence": "message: error.message || \"Unknown error\",",
|
| 105 |
-
"suppressed": false,
|
| 106 |
-
"advisory": false,
|
| 107 |
-
"likelihood": "unmeasured",
|
| 108 |
-
"path": "OpenSPP2-19.0/spp_cel_widget/static/src/js/cel_language/lint.js"
|
| 109 |
-
},
|
| 110 |
-
{
|
| 111 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 112 |
-
"severity": "high",
|
| 113 |
-
"line": 143,
|
| 114 |
-
"evidence": "currentTier: pendingReview.current_tier || 1,",
|
| 115 |
-
"suppressed": false,
|
| 116 |
-
"advisory": false,
|
| 117 |
-
"likelihood": "unmeasured",
|
| 118 |
-
"path": "OpenSPP2-19.0/spp_change_request_v2/static/src/components/review_panel/review_panel.js"
|
| 119 |
-
},
|
| 120 |
-
{
|
| 121 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 122 |
-
"severity": "high",
|
| 123 |
-
"line": 144,
|
| 124 |
-
"evidence": "totalTiers: pendingReview.tier_review_ids?.length || 1,",
|
| 125 |
-
"suppressed": false,
|
| 126 |
-
"advisory": false,
|
| 127 |
-
"likelihood": "unmeasured",
|
| 128 |
-
"path": "OpenSPP2-19.0/spp_change_request_v2/static/src/components/review_panel/review_panel.js"
|
| 129 |
-
},
|
| 130 |
-
{
|
| 131 |
-
"rule": "javascript.number-parsing-is-lossy",
|
| 132 |
-
"severity": "high",
|
| 133 |
-
"line": 37,
|
| 134 |
-
"evidence": "const docId = parseInt(el.dataset.docId, 10);",
|
| 135 |
-
"suppressed": false,
|
| 136 |
-
"advisory": false,
|
| 137 |
-
"likelihood": "unmeasured",
|
| 138 |
-
"path": "OpenSPP2-19.0/spp_change_request_v2/static/src/js/cr_review_documents.js"
|
| 139 |
-
},
|
| 140 |
-
{
|
| 141 |
-
"rule": "javascript.number-parsing-is-lossy",
|
| 142 |
-
"severity": "high",
|
| 143 |
-
"line": 36,
|
| 144 |
-
"evidence": "const partnerId = parseInt(row.dataset.partnerId, 10);",
|
| 145 |
-
"suppressed": false,
|
| 146 |
-
"advisory": false,
|
| 147 |
-
"likelihood": "unmeasured",
|
| 148 |
-
"path": "OpenSPP2-19.0/spp_change_request_v2/static/src/js/cr_search_results_field.js"
|
| 149 |
-
},
|
| 150 |
-
{
|
| 151 |
-
"rule": "javascript.number-parsing-is-lossy",
|
| 152 |
-
"severity": "high",
|
| 153 |
-
"line": 47,
|
| 154 |
-
"evidence": "const page = parseInt(link.dataset.page);",
|
| 155 |
-
"suppressed": false,
|
| 156 |
-
"advisory": false,
|
| 157 |
-
"likelihood": "unmeasured",
|
| 158 |
-
"path": "OpenSPP2-19.0/spp_change_request_v2/static/src/js/cr_search_results_field.js"
|
| 159 |
-
},
|
| 160 |
-
{
|
| 161 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 162 |
-
"severity": "high",
|
| 163 |
-
"line": 27,
|
| 164 |
-
"evidence": "name: params.name || \"Change Request Details\",",
|
| 165 |
-
"suppressed": false,
|
| 166 |
-
"advisory": false,
|
| 167 |
-
"likelihood": "unmeasured",
|
| 168 |
-
"path": "OpenSPP2-19.0/spp_change_request_v2/static/src/js/create_change_request.js"
|
| 169 |
-
},
|
| 170 |
-
{
|
| 171 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 172 |
-
"severity": "high",
|
| 173 |
-
"line": 51,
|
| 174 |
-
"evidence": "name: params.name || \"Change Request\",",
|
| 175 |
-
"suppressed": false,
|
| 176 |
-
"advisory": false,
|
| 177 |
-
"likelihood": "unmeasured",
|
| 178 |
-
"path": "OpenSPP2-19.0/spp_change_request_v2/static/src/js/create_change_request.js"
|
| 179 |
-
},
|
| 180 |
-
{
|
| 181 |
-
"rule": "javascript.number-parsing-is-lossy",
|
| 182 |
-
"severity": "high",
|
| 183 |
-
"line": 73,
|
| 184 |
-
"evidence": "limit: limit ? parseInt(limit, 10) : null,",
|
| 185 |
-
"suppressed": false,
|
| 186 |
-
"advisory": false,
|
| 187 |
-
"likelihood": "unmeasured",
|
| 188 |
-
"path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_arch_parser.esm.js"
|
| 189 |
-
},
|
| 190 |
-
{
|
| 191 |
-
"rule": "javascript.number-parsing-is-lossy",
|
| 192 |
-
"severity": "high",
|
| 193 |
-
"line": 74,
|
| 194 |
-
"evidence": "countLimit: countLimit ? parseInt(countLimit, 10) : null,",
|
| 195 |
-
"suppressed": false,
|
| 196 |
-
"advisory": false,
|
| 197 |
-
"likelihood": "unmeasured",
|
| 198 |
-
"path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_arch_parser.esm.js"
|
| 199 |
-
},
|
| 200 |
-
{
|
| 201 |
-
"rule": "javascript.json-parse-returns-any",
|
| 202 |
-
"severity": "critical",
|
| 203 |
-
"line": 397,
|
| 204 |
-
"evidence": "defaultCenter = JSON.parse(editInfo.default_center);",
|
| 205 |
-
"suppressed": false,
|
| 206 |
-
"advisory": false,
|
| 207 |
-
"likelihood": "unmeasured",
|
| 208 |
-
"path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_renderer/gis_renderer.esm.js"
|
| 209 |
-
},
|
| 210 |
-
{
|
| 211 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 212 |
-
"severity": "high",
|
| 213 |
-
"line": 606,
|
| 214 |
-
"evidence": "return layer.begin_color || \"#3388ff\";",
|
| 215 |
-
"suppressed": false,
|
| 216 |
-
"advisory": false,
|
| 217 |
-
"likelihood": "unmeasured",
|
| 218 |
-
"path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_renderer/gis_renderer.esm.js"
|
| 219 |
-
},
|
| 220 |
-
{
|
| 221 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 222 |
-
"severity": "high",
|
| 223 |
-
"line": 611,
|
| 224 |
-
"evidence": "const maxValue = config.max_value || 100;",
|
| 225 |
-
"suppressed": false,
|
| 226 |
-
"advisory": false,
|
| 227 |
-
"likelihood": "unmeasured",
|
| 228 |
-
"path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_renderer/gis_renderer.esm.js"
|
| 229 |
-
},
|
| 230 |
-
{
|
| 231 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 232 |
-
"severity": "high",
|
| 233 |
-
"line": 649,
|
| 234 |
-
"evidence": "return layer.begin_color || \"#3388ff\";",
|
| 235 |
-
"suppressed": false,
|
| 236 |
-
"advisory": false,
|
| 237 |
-
"likelihood": "unmeasured",
|
| 238 |
-
"path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_renderer/gis_renderer.esm.js"
|
| 239 |
-
},
|
| 240 |
-
{
|
| 241 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 242 |
-
"severity": "high",
|
| 243 |
-
"line": 665,
|
| 244 |
-
"evidence": "const classCount = config.class_count || 5;",
|
| 245 |
-
"suppressed": false,
|
| 246 |
-
"advisory": false,
|
| 247 |
-
"likelihood": "unmeasured",
|
| 248 |
-
"path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_renderer/gis_renderer.esm.js"
|
| 249 |
-
},
|
| 250 |
-
{
|
| 251 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 252 |
-
"severity": "high",
|
| 253 |
-
"line": 702,
|
| 254 |
-
"evidence": "fillColor = layer.begin_color || \"#3388ff\";",
|
| 255 |
-
"suppressed": false,
|
| 256 |
-
"advisory": false,
|
| 257 |
-
"likelihood": "unmeasured",
|
| 258 |
-
"path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_renderer/gis_renderer.esm.js"
|
| 259 |
-
},
|
| 260 |
-
{
|
| 261 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 262 |
-
"severity": "high",
|
| 263 |
-
"line": 913,
|
| 264 |
-
"evidence": "fillColor = layer.begin_color || \"#3388ff\";",
|
| 265 |
-
"suppressed": false,
|
| 266 |
-
"advisory": false,
|
| 267 |
-
"likelihood": "unmeasured",
|
| 268 |
-
"path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_renderer/gis_renderer.esm.js"
|
| 269 |
-
},
|
| 270 |
-
{
|
| 271 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 272 |
-
"severity": "high",
|
| 273 |
-
"line": 1040,
|
| 274 |
-
"evidence": "const maxValue = config.max_value || 100;",
|
| 275 |
-
"suppressed": false,
|
| 276 |
-
"advisory": false,
|
| 277 |
-
"likelihood": "unmeasured",
|
| 278 |
-
"path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_renderer/gis_renderer.esm.js"
|
| 279 |
-
},
|
| 280 |
-
{
|
| 281 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 282 |
-
"severity": "high",
|
| 283 |
-
"line": 1087,
|
| 284 |
-
"evidence": "const classCount = config.class_count || 5;",
|
| 285 |
-
"suppressed": false,
|
| 286 |
-
"advisory": false,
|
| 287 |
-
"likelihood": "unmeasured",
|
| 288 |
-
"path": "OpenSPP2-19.0/spp_gis/static/src/js/views/gis/gis_renderer/gis_renderer.esm.js"
|
| 289 |
-
},
|
| 290 |
-
{
|
| 291 |
-
"rule": "javascript.json-parse-returns-any",
|
| 292 |
-
"severity": "critical",
|
| 293 |
-
"line": 46,
|
| 294 |
-
"evidence": "const default_center = JSON.parse(editInfo.default_center);",
|
| 295 |
-
"suppressed": false,
|
| 296 |
-
"advisory": false,
|
| 297 |
-
"likelihood": "unmeasured",
|
| 298 |
-
"path": "OpenSPP2-19.0/spp_gis/static/src/js/widgets/gis_edit_map/field_gis_edit_map.esm.js"
|
| 299 |
-
},
|
| 300 |
-
{
|
| 301 |
-
"rule": "javascript.json-parse-returns-any",
|
| 302 |
-
"severity": "critical",
|
| 303 |
-
"line": 96,
|
| 304 |
-
"evidence": "const obj = JSON.parse(this.props.record.data[this.props.name]);",
|
| 305 |
-
"suppressed": false,
|
| 306 |
-
"advisory": false,
|
| 307 |
-
"likelihood": "unmeasured",
|
| 308 |
-
"path": "OpenSPP2-19.0/spp_gis/static/src/js/widgets/gis_edit_map/field_gis_edit_map.esm.js"
|
| 309 |
-
},
|
| 310 |
-
{
|
| 311 |
-
"rule": "javascript.json-parse-returns-any",
|
| 312 |
-
"severity": "critical",
|
| 313 |
-
"line": 140,
|
| 314 |
-
"evidence": "const obj = JSON.parse(record.data[fieldName]);",
|
| 315 |
-
"suppressed": false,
|
| 316 |
-
"advisory": false,
|
| 317 |
-
"likelihood": "unmeasured",
|
| 318 |
-
"path": "OpenSPP2-19.0/spp_gis/static/src/js/widgets/gis_edit_map/field_gis_edit_map.esm.js"
|
| 319 |
-
},
|
| 320 |
-
{
|
| 321 |
-
"rule": "javascript.json-parse-returns-any",
|
| 322 |
-
"severity": "critical",
|
| 323 |
-
"line": 220,
|
| 324 |
-
"evidence": "const geom = JSON.parse(this.props.record.data[this.props.name]);",
|
| 325 |
-
"suppressed": false,
|
| 326 |
-
"advisory": false,
|
| 327 |
-
"likelihood": "unmeasured",
|
| 328 |
-
"path": "OpenSPP2-19.0/spp_gis/static/src/js/widgets/gis_edit_map/field_gis_edit_map.esm.js"
|
| 329 |
-
},
|
| 330 |
-
{
|
| 331 |
-
"rule": "javascript.number-parsing-is-lossy",
|
| 332 |
-
"severity": "high",
|
| 333 |
-
"line": 85,
|
| 334 |
-
"evidence": "const matchId = parseInt(ev.target.value, 10) || false;",
|
| 335 |
-
"suppressed": false,
|
| 336 |
-
"advisory": false,
|
| 337 |
-
"likelihood": "unmeasured",
|
| 338 |
-
"path": "OpenSPP2-19.0/spp_import_match/static/src/import_match_selector/import_match_selector.js"
|
| 339 |
-
},
|
| 340 |
-
{
|
| 341 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 342 |
-
"severity": "high",
|
| 343 |
-
"line": 117,
|
| 344 |
-
"evidence": "return result || \"••••••••\";",
|
| 345 |
-
"suppressed": false,
|
| 346 |
-
"advisory": false,
|
| 347 |
-
"likelihood": "unmeasured",
|
| 348 |
-
"path": "OpenSPP2-19.0/spp_pii_encryption/static/src/js/masked_field.js"
|
| 349 |
-
},
|
| 350 |
-
{
|
| 351 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 352 |
-
"severity": "high",
|
| 353 |
-
"line": 18,
|
| 354 |
-
"evidence": "const programName = action.params?.name || \"Program\";",
|
| 355 |
-
"suppressed": false,
|
| 356 |
-
"advisory": false,
|
| 357 |
-
"likelihood": "unmeasured",
|
| 358 |
-
"path": "OpenSPP2-19.0/spp_programs/static/src/js/create_program.js"
|
| 359 |
-
},
|
| 360 |
-
{
|
| 361 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 362 |
-
"severity": "high",
|
| 363 |
-
"line": 15,
|
| 364 |
-
"evidence": "type: params.type || \"info\",",
|
| 365 |
-
"suppressed": false,
|
| 366 |
-
"advisory": false,
|
| 367 |
-
"likelihood": "unmeasured",
|
| 368 |
-
"path": "OpenSPP2-19.0/spp_registry/static/src/js/custom_client_action.js"
|
| 369 |
-
},
|
| 370 |
-
{
|
| 371 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 372 |
-
"severity": "high",
|
| 373 |
-
"line": 89,
|
| 374 |
-
"evidence": "this.state.searchMode = config.search_mode || \"unified\";",
|
| 375 |
-
"suppressed": false,
|
| 376 |
-
"advisory": false,
|
| 377 |
-
"likelihood": "unmeasured",
|
| 378 |
-
"path": "OpenSPP2-19.0/spp_registry_search/static/src/js/registry_search_portal.js"
|
| 379 |
-
},
|
| 380 |
-
{
|
| 381 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 382 |
-
"severity": "high",
|
| 383 |
-
"line": 90,
|
| 384 |
-
"evidence": "this.state.searchField = config.target_field || \"name\";",
|
| 385 |
-
"suppressed": false,
|
| 386 |
-
"advisory": false,
|
| 387 |
-
"likelihood": "unmeasured",
|
| 388 |
-
"path": "OpenSPP2-19.0/spp_registry_search/static/src/js/registry_search_portal.js"
|
| 389 |
-
},
|
| 390 |
-
{
|
| 391 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 392 |
-
"severity": "high",
|
| 393 |
-
"line": 91,
|
| 394 |
-
"evidence": "this.resultLimit = config.result_limit || 50;",
|
| 395 |
-
"suppressed": false,
|
| 396 |
-
"advisory": false,
|
| 397 |
-
"likelihood": "unmeasured",
|
| 398 |
-
"path": "OpenSPP2-19.0/spp_registry_search/static/src/js/registry_search_portal.js"
|
| 399 |
-
},
|
| 400 |
-
{
|
| 401 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 402 |
-
"severity": "high",
|
| 403 |
-
"line": 92,
|
| 404 |
-
"evidence": "this.minSearchChars = config.min_chars || 3;",
|
| 405 |
-
"suppressed": false,
|
| 406 |
-
"advisory": false,
|
| 407 |
-
"likelihood": "unmeasured",
|
| 408 |
-
"path": "OpenSPP2-19.0/spp_registry_search/static/src/js/registry_search_portal.js"
|
| 409 |
-
},
|
| 410 |
-
{
|
| 411 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 412 |
-
"severity": "high",
|
| 413 |
-
"line": 289,
|
| 414 |
-
"evidence": "const scope = this.state.scopeFilter || \"all\";",
|
| 415 |
-
"suppressed": false,
|
| 416 |
-
"advisory": false,
|
| 417 |
-
"likelihood": "unmeasured",
|
| 418 |
-
"path": "OpenSPP2-19.0/spp_studio/static/src/js/logic_editor/variable_picker.js"
|
| 419 |
-
},
|
| 420 |
-
{
|
| 421 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 422 |
-
"severity": "high",
|
| 423 |
-
"line": 295,
|
| 424 |
-
"evidence": "const applies = v.applies_to || \"both\";",
|
| 425 |
-
"suppressed": false,
|
| 426 |
-
"advisory": false,
|
| 427 |
-
"likelihood": "unmeasured",
|
| 428 |
-
"path": "OpenSPP2-19.0/spp_studio/static/src/js/logic_editor/variable_picker.js"
|
| 429 |
-
},
|
| 430 |
-
{
|
| 431 |
-
"rule": "javascript.loose-equality-and-falsy-defaults",
|
| 432 |
-
"severity": "high",
|
| 433 |
-
"line": 313,
|
| 434 |
-
"evidence": "const applies = variable.applies_to || \"both\";",
|
| 435 |
-
"suppressed": false,
|
| 436 |
-
"advisory": false,
|
| 437 |
-
"likelihood": "unmeasured",
|
| 438 |
-
"path": "OpenSPP2-19.0/spp_studio/static/src/js/logic_editor/variable_picker.js"
|
| 439 |
-
},
|
| 440 |
-
{
|
| 441 |
-
"rule": "javascript.json-parse-returns-any",
|
| 442 |
-
"severity": "critical",
|
| 443 |
-
"line": 25,
|
| 444 |
-
"evidence": "const excData = JSON.parse(originalError.data.message);",
|
| 445 |
-
"suppressed": false,
|
| 446 |
-
"advisory": false,
|
| 447 |
-
"likelihood": "unmeasured",
|
| 448 |
-
"path": "server-tools/base_exception/static/src/js/base_exception.esm.js"
|
| 449 |
-
}
|
| 450 |
-
]
|
| 451 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -0,0 +1,1260 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"stack": "odoo",
|
| 3 |
+
"roots": [
|
| 4 |
+
"server-tools",
|
| 5 |
+
"account-financial-tools"
|
| 6 |
+
],
|
| 7 |
+
"files": 606,
|
| 8 |
+
"lines": 33960,
|
| 9 |
+
"checkers": 26,
|
| 10 |
+
"triggers": {
|
| 11 |
+
"files": 879,
|
| 12 |
+
"models_declared": 139,
|
| 13 |
+
"models_deleted": 123,
|
| 14 |
+
"declared_and_deleted": 113,
|
| 15 |
+
"never_deleted": 26,
|
| 16 |
+
"multi_company": true
|
| 17 |
+
},
|
| 18 |
+
"hits": [
|
| 19 |
+
{
|
| 20 |
+
"rule": "odoo.sudo-bypasses-record-rules",
|
| 21 |
+
"severity": "critical",
|
| 22 |
+
"line": 71,
|
| 23 |
+
"evidence": "sudo().search() on ir.model runs with record rules off -- ir.rule filtering, including multi-company isolation, does not apply and the result looks correct in single-company testing",
|
| 24 |
+
"suppressed": false,
|
| 25 |
+
"advisory": false,
|
| 26 |
+
"likelihood": "defect",
|
| 27 |
+
"path": "server-tools/attachment_delete_restrict/models/ir_attachment.py"
|
| 28 |
+
},
|
| 29 |
+
{
|
| 30 |
+
"rule": "odoo.ondelete-is-a-decision",
|
| 31 |
+
"severity": "high",
|
| 32 |
+
"line": 14,
|
| 33 |
+
"evidence": "task_id declares no ondelete, so the ORM defaults it to 'set null' -- and task_id.filepath on line 37 reads through it, which yields False rather than raising once the parent is deleted, and this tree does delete attachment.synchronize.task",
|
| 34 |
+
"suppressed": false,
|
| 35 |
+
"advisory": false,
|
| 36 |
+
"likelihood": "defect",
|
| 37 |
+
"path": "server-tools/attachment_synchronize/models/attachment_queue.py"
|
| 38 |
+
},
|
| 39 |
+
{
|
| 40 |
+
"rule": "odoo.ondelete-is-a-decision",
|
| 41 |
+
"severity": "high",
|
| 42 |
+
"line": 64,
|
| 43 |
+
"evidence": "backend_id declares no ondelete, so the ORM defaults it to 'set null' -- and backend_id.fs on line 166 reads through it, which yields False rather than raising once the parent is deleted",
|
| 44 |
+
"suppressed": false,
|
| 45 |
+
"advisory": false,
|
| 46 |
+
"likelihood": "construct",
|
| 47 |
+
"path": "server-tools/attachment_synchronize/models/attachment_synchronize_task.py"
|
| 48 |
+
},
|
| 49 |
+
{
|
| 50 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 51 |
+
"severity": "medium",
|
| 52 |
+
"line": 25,
|
| 53 |
+
"evidence": "deadline = datetime.now() - timedelta(days=days)",
|
| 54 |
+
"suppressed": false,
|
| 55 |
+
"advisory": false,
|
| 56 |
+
"likelihood": "unmeasured",
|
| 57 |
+
"path": "server-tools/auditlog/models/autovacuum.py"
|
| 58 |
+
},
|
| 59 |
+
{
|
| 60 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 61 |
+
"severity": "critical",
|
| 62 |
+
"line": 29,
|
| 63 |
+
"evidence": "_compute_display_name() reads 'user_id.name' but @api.depends does not declare it -- the stored value goes stale when 'user_id.name' changes",
|
| 64 |
+
"suppressed": false,
|
| 65 |
+
"advisory": false,
|
| 66 |
+
"likelihood": "construct",
|
| 67 |
+
"path": "server-tools/auditlog/models/http_session.py"
|
| 68 |
+
},
|
| 69 |
+
{
|
| 70 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 71 |
+
"severity": "medium",
|
| 72 |
+
"line": 146,
|
| 73 |
+
"evidence": "filename = self.filename(datetime.now(), ext=rec.backup_format)",
|
| 74 |
+
"suppressed": false,
|
| 75 |
+
"advisory": false,
|
| 76 |
+
"likelihood": "unmeasured",
|
| 77 |
+
"path": "server-tools/auto_backup/models/db_backup.py"
|
| 78 |
+
},
|
| 79 |
+
{
|
| 80 |
+
"rule": "python.path-join-does-not-contain-a-path",
|
| 81 |
+
"severity": "high",
|
| 82 |
+
"line": 154,
|
| 83 |
+
"evidence": "with open(os.path.join(rec.folder, filename), \"wb\") as destiny:",
|
| 84 |
+
"suppressed": false,
|
| 85 |
+
"advisory": false,
|
| 86 |
+
"likelihood": "unmeasured",
|
| 87 |
+
"path": "server-tools/auto_backup/models/db_backup.py"
|
| 88 |
+
},
|
| 89 |
+
{
|
| 90 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 91 |
+
"severity": "medium",
|
| 92 |
+
"line": 171,
|
| 93 |
+
"evidence": "filename = self.filename(datetime.now(), ext=rec.backup_format)",
|
| 94 |
+
"suppressed": false,
|
| 95 |
+
"advisory": false,
|
| 96 |
+
"likelihood": "unmeasured",
|
| 97 |
+
"path": "server-tools/auto_backup/models/db_backup.py"
|
| 98 |
+
},
|
| 99 |
+
{
|
| 100 |
+
"rule": "python.path-join-does-not-contain-a-path",
|
| 101 |
+
"severity": "high",
|
| 102 |
+
"line": 186,
|
| 103 |
+
"evidence": "os.path.join(rec.folder, filename), \"wb\"",
|
| 104 |
+
"suppressed": false,
|
| 105 |
+
"advisory": false,
|
| 106 |
+
"likelihood": "unmeasured",
|
| 107 |
+
"path": "server-tools/auto_backup/models/db_backup.py"
|
| 108 |
+
},
|
| 109 |
+
{
|
| 110 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 111 |
+
"severity": "medium",
|
| 112 |
+
"line": 218,
|
| 113 |
+
"evidence": "now = datetime.now()",
|
| 114 |
+
"suppressed": false,
|
| 115 |
+
"advisory": false,
|
| 116 |
+
"likelihood": "unmeasured",
|
| 117 |
+
"path": "server-tools/auto_backup/models/db_backup.py"
|
| 118 |
+
},
|
| 119 |
+
{
|
| 120 |
+
"rule": "odoo.constrains-must-list-every-field-read",
|
| 121 |
+
"severity": "critical",
|
| 122 |
+
"line": 100,
|
| 123 |
+
"evidence": "_check_inheriting_model() reads 'ttype' but @api.constrains declares only inheriting_model -- writing 'ttype' alone skips the check entirely",
|
| 124 |
+
"suppressed": false,
|
| 125 |
+
"advisory": false,
|
| 126 |
+
"likelihood": "unmeasured",
|
| 127 |
+
"path": "server-tools/autovacuum_message_attachment/models/vacuum_rule.py"
|
| 128 |
+
},
|
| 129 |
+
{
|
| 130 |
+
"rule": "odoo.constrains-must-list-every-field-read",
|
| 131 |
+
"severity": "critical",
|
| 132 |
+
"line": 59,
|
| 133 |
+
"evidence": "check_exception_type_consistency() reads 'method' but @api.constrains declares only code, domain, exception_type, model -- writing 'method' alone skips the check entirely",
|
| 134 |
+
"suppressed": false,
|
| 135 |
+
"advisory": false,
|
| 136 |
+
"likelihood": "unmeasured",
|
| 137 |
+
"path": "server-tools/base_exception/models/exception_rule.py"
|
| 138 |
+
},
|
| 139 |
+
{
|
| 140 |
+
"rule": "odoo.sql-needs-query-parameters",
|
| 141 |
+
"severity": "critical",
|
| 142 |
+
"line": 47,
|
| 143 |
+
"evidence": "the query handed to cr.execute() is built by an f-string -- pass the values as the second argument so the driver quotes them",
|
| 144 |
+
"suppressed": true,
|
| 145 |
+
"advisory": false,
|
| 146 |
+
"likelihood": "unmeasured",
|
| 147 |
+
"path": "server-tools/base_multi_image/hooks.py"
|
| 148 |
+
},
|
| 149 |
+
{
|
| 150 |
+
"rule": "odoo.depends-must-not-name-unread-fields",
|
| 151 |
+
"severity": "medium",
|
| 152 |
+
"line": 103,
|
| 153 |
+
"evidence": "_compute_show_technical() declares 'owner_id', 'owner_model' in @api.depends and never reads them -- the value recomputes on writes that cannot change it, and the decorator stops describing the method",
|
| 154 |
+
"suppressed": false,
|
| 155 |
+
"advisory": false,
|
| 156 |
+
"likelihood": "unmeasured",
|
| 157 |
+
"path": "server-tools/base_multi_image/models/image.py"
|
| 158 |
+
},
|
| 159 |
+
{
|
| 160 |
+
"rule": "odoo.sudo-bypasses-record-rules",
|
| 161 |
+
"severity": "critical",
|
| 162 |
+
"line": 95,
|
| 163 |
+
"evidence": "sudo().search() runs with record rules off -- ir.rule filtering, including multi-company isolation, does not apply and the result looks correct in single-company testing",
|
| 164 |
+
"suppressed": false,
|
| 165 |
+
"advisory": false,
|
| 166 |
+
"likelihood": "defect",
|
| 167 |
+
"path": "server-tools/base_sequence_option/models/ir_sequence_option.py"
|
| 168 |
+
},
|
| 169 |
+
{
|
| 170 |
+
"rule": "python.assert-is-not-validation",
|
| 171 |
+
"severity": "critical",
|
| 172 |
+
"line": 19,
|
| 173 |
+
"evidence": "assert type(date) is datetime.date or date is None, \"Wrong date\"",
|
| 174 |
+
"suppressed": false,
|
| 175 |
+
"advisory": false,
|
| 176 |
+
"likelihood": "unmeasured",
|
| 177 |
+
"path": "server-tools/base_time_parameter/models/base.py"
|
| 178 |
+
},
|
| 179 |
+
{
|
| 180 |
+
"rule": "odoo.sql-needs-query-parameters",
|
| 181 |
+
"severity": "critical",
|
| 182 |
+
"line": 24,
|
| 183 |
+
"evidence": "the query handed to cr.execute() is built by an f-string -- pass the values as the second argument so the driver quotes them",
|
| 184 |
+
"suppressed": false,
|
| 185 |
+
"advisory": false,
|
| 186 |
+
"likelihood": "unmeasured",
|
| 187 |
+
"path": "server-tools/database_autovacuum_tuning/models/database_autovacuum_tuning.py"
|
| 188 |
+
},
|
| 189 |
+
{
|
| 190 |
+
"rule": "odoo.commit-inside-a-loop-splits-the-transaction",
|
| 191 |
+
"severity": "high",
|
| 192 |
+
"line": 58,
|
| 193 |
+
"evidence": "cr.commit() inside a loop -- an exception on a later iteration leaves the earlier ones written, and the caller cannot roll the batch back",
|
| 194 |
+
"suppressed": true,
|
| 195 |
+
"advisory": false,
|
| 196 |
+
"likelihood": "unmeasured",
|
| 197 |
+
"path": "server-tools/database_cleanup/models/purge_columns.py"
|
| 198 |
+
},
|
| 199 |
+
{
|
| 200 |
+
"rule": "odoo.sudo-bypasses-record-rules",
|
| 201 |
+
"severity": "critical",
|
| 202 |
+
"line": 87,
|
| 203 |
+
"evidence": "sudo().search() on ir.model runs with record rules off -- ir.rule filtering, including multi-company isolation, does not apply and the result looks correct in single-company testing",
|
| 204 |
+
"suppressed": false,
|
| 205 |
+
"advisory": false,
|
| 206 |
+
"likelihood": "defect",
|
| 207 |
+
"path": "server-tools/database_size/models/ir_model_size.py"
|
| 208 |
+
},
|
| 209 |
+
{
|
| 210 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 211 |
+
"severity": "medium",
|
| 212 |
+
"line": 268,
|
| 213 |
+
"evidence": "stamp = dt.utcnow().strftime(\"%H%M%S%f\")[:-3]",
|
| 214 |
+
"suppressed": false,
|
| 215 |
+
"advisory": false,
|
| 216 |
+
"likelihood": "unmeasured",
|
| 217 |
+
"path": "server-tools/excel_import_export/models/xlsx_export.py"
|
| 218 |
+
},
|
| 219 |
+
{
|
| 220 |
+
"rule": "odoo.ondelete-is-a-decision",
|
| 221 |
+
"severity": "high",
|
| 222 |
+
"line": 108,
|
| 223 |
+
"evidence": "result_model_id declares no ondelete, so the ORM defaults it to 'set null' -- and result_model_id.model on line 199 reads through it, which yields False rather than raising once the parent is deleted, and this tree does delete ir.model",
|
| 224 |
+
"suppressed": false,
|
| 225 |
+
"advisory": false,
|
| 226 |
+
"likelihood": "defect",
|
| 227 |
+
"path": "server-tools/excel_import_export/models/xlsx_template.py"
|
| 228 |
+
},
|
| 229 |
+
{
|
| 230 |
+
"rule": "odoo.ondelete-is-a-decision",
|
| 231 |
+
"severity": "high",
|
| 232 |
+
"line": 25,
|
| 233 |
+
"evidence": "server_id declares no ondelete, so the ORM defaults it to 'set null' -- and server_id.connect on line 139 reads through it, which yields False rather than raising once the parent is deleted",
|
| 234 |
+
"suppressed": false,
|
| 235 |
+
"advisory": false,
|
| 236 |
+
"likelihood": "construct",
|
| 237 |
+
"path": "server-tools/fetchmail_attach_from_folder/models/fetchmail_server_folder.py"
|
| 238 |
+
},
|
| 239 |
+
{
|
| 240 |
+
"rule": "python.assert-is-not-validation",
|
| 241 |
+
"severity": "critical",
|
| 242 |
+
"line": 42,
|
| 243 |
+
"evidence": "assert len(param) == len(records)",
|
| 244 |
+
"suppressed": false,
|
| 245 |
+
"advisory": false,
|
| 246 |
+
"likelihood": "unmeasured",
|
| 247 |
+
"path": "server-tools/jsonifier/models/ir_exports_resolver.py"
|
| 248 |
+
},
|
| 249 |
+
{
|
| 250 |
+
"rule": "python.assert-is-not-validation",
|
| 251 |
+
"severity": "critical",
|
| 252 |
+
"line": 3,
|
| 253 |
+
"evidence": "assert isinstance(parser, list)",
|
| 254 |
+
"suppressed": false,
|
| 255 |
+
"advisory": false,
|
| 256 |
+
"likelihood": "unmeasured",
|
| 257 |
+
"path": "server-tools/jsonifier/models/utils.py"
|
| 258 |
+
},
|
| 259 |
+
{
|
| 260 |
+
"rule": "odoo.sudo-bypasses-record-rules",
|
| 261 |
+
"severity": "critical",
|
| 262 |
+
"line": 34,
|
| 263 |
+
"evidence": "sudo().search() runs with record rules off -- ir.rule filtering, including multi-company isolation, does not apply and the result looks correct in single-company testing",
|
| 264 |
+
"suppressed": false,
|
| 265 |
+
"advisory": false,
|
| 266 |
+
"likelihood": "defect",
|
| 267 |
+
"path": "server-tools/tracking_manager/models/ir_model.py"
|
| 268 |
+
},
|
| 269 |
+
{
|
| 270 |
+
"rule": "odoo.depends-must-not-name-unread-fields",
|
| 271 |
+
"severity": "medium",
|
| 272 |
+
"line": 101,
|
| 273 |
+
"evidence": "_compute_automatic_custom_tracking() declares 'active_custom_tracking' in @api.depends and never reads it -- the value recomputes on writes that cannot change it, and the decorator stops describing the method",
|
| 274 |
+
"suppressed": false,
|
| 275 |
+
"advisory": false,
|
| 276 |
+
"likelihood": "unmeasured",
|
| 277 |
+
"path": "server-tools/tracking_manager/models/ir_model.py"
|
| 278 |
+
},
|
| 279 |
+
{
|
| 280 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 281 |
+
"severity": "critical",
|
| 282 |
+
"line": 135,
|
| 283 |
+
"evidence": "_compute_automatic_custom_tracking_domain() reads 'model' but @api.depends does not declare it -- the stored value goes stale when 'model' changes",
|
| 284 |
+
"suppressed": false,
|
| 285 |
+
"advisory": false,
|
| 286 |
+
"likelihood": "construct",
|
| 287 |
+
"path": "server-tools/tracking_manager/models/ir_model.py"
|
| 288 |
+
},
|
| 289 |
+
{
|
| 290 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 291 |
+
"severity": "critical",
|
| 292 |
+
"line": 30,
|
| 293 |
+
"evidence": "_compute_custom_tracking() reads 'model_id' but @api.depends does not declare it -- the stored value goes stale when 'model_id' changes",
|
| 294 |
+
"suppressed": false,
|
| 295 |
+
"advisory": false,
|
| 296 |
+
"likelihood": "construct",
|
| 297 |
+
"path": "server-tools/tracking_manager/models/ir_model_fields.py"
|
| 298 |
+
},
|
| 299 |
+
{
|
| 300 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 301 |
+
"severity": "critical",
|
| 302 |
+
"line": 30,
|
| 303 |
+
"evidence": "_compute_custom_tracking() reads 'model_id.automatic_custom_tracking' but @api.depends does not declare it -- the stored value goes stale when 'model_id.automatic_custom_tracking' changes",
|
| 304 |
+
"suppressed": false,
|
| 305 |
+
"advisory": false,
|
| 306 |
+
"likelihood": "construct",
|
| 307 |
+
"path": "server-tools/tracking_manager/models/ir_model_fields.py"
|
| 308 |
+
},
|
| 309 |
+
{
|
| 310 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 311 |
+
"severity": "critical",
|
| 312 |
+
"line": 31,
|
| 313 |
+
"evidence": "_compute_custom_tracking() reads 'model_id.automatic_custom_tracking_domain' but @api.depends does not declare it -- the stored value goes stale when 'model_id.automatic_custom_tracking_domain' changes",
|
| 314 |
+
"suppressed": false,
|
| 315 |
+
"advisory": false,
|
| 316 |
+
"likelihood": "construct",
|
| 317 |
+
"path": "server-tools/tracking_manager/models/ir_model_fields.py"
|
| 318 |
+
},
|
| 319 |
+
{
|
| 320 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 321 |
+
"severity": "critical",
|
| 322 |
+
"line": 52,
|
| 323 |
+
"evidence": "_compute_trackable() reads 'name' but @api.depends does not declare it -- the stored value goes stale when 'name' changes",
|
| 324 |
+
"suppressed": false,
|
| 325 |
+
"advisory": false,
|
| 326 |
+
"likelihood": "construct",
|
| 327 |
+
"path": "server-tools/tracking_manager/models/ir_model_fields.py"
|
| 328 |
+
},
|
| 329 |
+
{
|
| 330 |
+
"rule": "python.path-join-does-not-contain-a-path",
|
| 331 |
+
"severity": "high",
|
| 332 |
+
"line": 106,
|
| 333 |
+
"evidence": "logfile = os.path.join(full_path, filename)",
|
| 334 |
+
"suppressed": false,
|
| 335 |
+
"advisory": false,
|
| 336 |
+
"likelihood": "unmeasured",
|
| 337 |
+
"path": "server-tools/upgrade_analysis/models/upgrade_analysis.py"
|
| 338 |
+
},
|
| 339 |
+
{
|
| 340 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 341 |
+
"severity": "critical",
|
| 342 |
+
"line": 60,
|
| 343 |
+
"evidence": "_compute_account_asset_id() reads 'state' but @api.depends does not declare it -- the stored value goes stale when 'state' changes",
|
| 344 |
+
"suppressed": false,
|
| 345 |
+
"advisory": false,
|
| 346 |
+
"likelihood": "construct",
|
| 347 |
+
"path": "account-financial-tools/account_asset_force_account/models/account_asset.py"
|
| 348 |
+
},
|
| 349 |
+
{
|
| 350 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 351 |
+
"severity": "critical",
|
| 352 |
+
"line": 64,
|
| 353 |
+
"evidence": "_compute_account_asset_id() reads 'account_move_line_ids.account_id' but @api.depends does not declare it -- the stored value goes stale when 'account_move_line_ids.account_id' changes",
|
| 354 |
+
"suppressed": false,
|
| 355 |
+
"advisory": false,
|
| 356 |
+
"likelihood": "construct",
|
| 357 |
+
"path": "account-financial-tools/account_asset_force_account/models/account_asset.py"
|
| 358 |
+
},
|
| 359 |
+
{
|
| 360 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 361 |
+
"severity": "critical",
|
| 362 |
+
"line": 72,
|
| 363 |
+
"evidence": "_compute_account_asset_id() reads 'profile_id.account_asset_id' but @api.depends does not declare it -- the stored value goes stale when 'profile_id.account_asset_id' changes",
|
| 364 |
+
"suppressed": false,
|
| 365 |
+
"advisory": false,
|
| 366 |
+
"likelihood": "construct",
|
| 367 |
+
"path": "account-financial-tools/account_asset_force_account/models/account_asset.py"
|
| 368 |
+
},
|
| 369 |
+
{
|
| 370 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 371 |
+
"severity": "critical",
|
| 372 |
+
"line": 311,
|
| 373 |
+
"evidence": "_compute_group_ids() reads 'profile_id.group_ids' but @api.depends does not declare it -- the stored value goes stale when 'profile_id.group_ids' changes",
|
| 374 |
+
"suppressed": false,
|
| 375 |
+
"advisory": false,
|
| 376 |
+
"likelihood": "construct",
|
| 377 |
+
"path": "account-financial-tools/account_asset_management/models/account_asset.py"
|
| 378 |
+
},
|
| 379 |
+
{
|
| 380 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 381 |
+
"severity": "critical",
|
| 382 |
+
"line": 316,
|
| 383 |
+
"evidence": "_compute_method() reads 'profile_id.method' but @api.depends does not declare it -- the stored value goes stale when 'profile_id.method' changes",
|
| 384 |
+
"suppressed": false,
|
| 385 |
+
"advisory": false,
|
| 386 |
+
"likelihood": "construct",
|
| 387 |
+
"path": "account-financial-tools/account_asset_management/models/account_asset.py"
|
| 388 |
+
},
|
| 389 |
+
{
|
| 390 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 391 |
+
"severity": "critical",
|
| 392 |
+
"line": 324,
|
| 393 |
+
"evidence": "_compute_method_number() reads 'profile_id.method_number' but @api.depends does not declare it -- the stored value goes stale when 'profile_id.method_number' changes",
|
| 394 |
+
"suppressed": false,
|
| 395 |
+
"advisory": false,
|
| 396 |
+
"likelihood": "construct",
|
| 397 |
+
"path": "account-financial-tools/account_asset_management/models/account_asset.py"
|
| 398 |
+
},
|
| 399 |
+
{
|
| 400 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 401 |
+
"severity": "critical",
|
| 402 |
+
"line": 329,
|
| 403 |
+
"evidence": "_compute_method_period() reads 'profile_id.method_period' but @api.depends does not declare it -- the stored value goes stale when 'profile_id.method_period' changes",
|
| 404 |
+
"suppressed": false,
|
| 405 |
+
"advisory": false,
|
| 406 |
+
"likelihood": "construct",
|
| 407 |
+
"path": "account-financial-tools/account_asset_management/models/account_asset.py"
|
| 408 |
+
},
|
| 409 |
+
{
|
| 410 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 411 |
+
"severity": "critical",
|
| 412 |
+
"line": 340,
|
| 413 |
+
"evidence": "_compute_method_progress_factor() reads 'profile_id.method_progress_factor' but @api.depends does not declare it -- the stored value goes stale when 'profile_id.method_progress_factor' changes",
|
| 414 |
+
"suppressed": false,
|
| 415 |
+
"advisory": false,
|
| 416 |
+
"likelihood": "construct",
|
| 417 |
+
"path": "account-financial-tools/account_asset_management/models/account_asset.py"
|
| 418 |
+
},
|
| 419 |
+
{
|
| 420 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 421 |
+
"severity": "critical",
|
| 422 |
+
"line": 345,
|
| 423 |
+
"evidence": "_compute_method_time() reads 'profile_id.method_time' but @api.depends does not declare it -- the stored value goes stale when 'profile_id.method_time' changes",
|
| 424 |
+
"suppressed": false,
|
| 425 |
+
"advisory": false,
|
| 426 |
+
"likelihood": "construct",
|
| 427 |
+
"path": "account-financial-tools/account_asset_management/models/account_asset.py"
|
| 428 |
+
},
|
| 429 |
+
{
|
| 430 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 431 |
+
"severity": "critical",
|
| 432 |
+
"line": 350,
|
| 433 |
+
"evidence": "_compute_days_calc() reads 'profile_id.days_calc' but @api.depends does not declare it -- the stored value goes stale when 'profile_id.days_calc' changes",
|
| 434 |
+
"suppressed": false,
|
| 435 |
+
"advisory": false,
|
| 436 |
+
"likelihood": "construct",
|
| 437 |
+
"path": "account-financial-tools/account_asset_management/models/account_asset.py"
|
| 438 |
+
},
|
| 439 |
+
{
|
| 440 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 441 |
+
"severity": "critical",
|
| 442 |
+
"line": 355,
|
| 443 |
+
"evidence": "_compute_use_leap_years() reads 'profile_id.use_leap_years' but @api.depends does not declare it -- the stored value goes stale when 'profile_id.use_leap_years' changes",
|
| 444 |
+
"suppressed": false,
|
| 445 |
+
"advisory": false,
|
| 446 |
+
"likelihood": "construct",
|
| 447 |
+
"path": "account-financial-tools/account_asset_management/models/account_asset.py"
|
| 448 |
+
},
|
| 449 |
+
{
|
| 450 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 451 |
+
"severity": "critical",
|
| 452 |
+
"line": 363,
|
| 453 |
+
"evidence": "_compute_prorrata() reads 'profile_id.prorata' but @api.depends does not declare it -- the stored value goes stale when 'profile_id.prorata' changes",
|
| 454 |
+
"suppressed": false,
|
| 455 |
+
"advisory": false,
|
| 456 |
+
"likelihood": "construct",
|
| 457 |
+
"path": "account-financial-tools/account_asset_management/models/account_asset.py"
|
| 458 |
+
},
|
| 459 |
+
{
|
| 460 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 461 |
+
"severity": "critical",
|
| 462 |
+
"line": 368,
|
| 463 |
+
"evidence": "_compute_account_analytic_id() reads 'profile_id.account_analytic_id' but @api.depends does not declare it -- the stored value goes stale when 'profile_id.account_analytic_id' changes",
|
| 464 |
+
"suppressed": false,
|
| 465 |
+
"advisory": false,
|
| 466 |
+
"likelihood": "construct",
|
| 467 |
+
"path": "account-financial-tools/account_asset_management/models/account_asset.py"
|
| 468 |
+
},
|
| 469 |
+
{
|
| 470 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 471 |
+
"severity": "critical",
|
| 472 |
+
"line": 373,
|
| 473 |
+
"evidence": "_compute_analytic_distribution() reads 'profile_id.analytic_distribution' but @api.depends does not declare it -- the stored value goes stale when 'profile_id.analytic_distribution' changes",
|
| 474 |
+
"suppressed": false,
|
| 475 |
+
"advisory": false,
|
| 476 |
+
"likelihood": "construct",
|
| 477 |
+
"path": "account-financial-tools/account_asset_management/models/account_asset.py"
|
| 478 |
+
},
|
| 479 |
+
{
|
| 480 |
+
"rule": "odoo.constrains-must-list-every-field-read",
|
| 481 |
+
"severity": "critical",
|
| 482 |
+
"line": 398,
|
| 483 |
+
"evidence": "_check_profile_change() reads 'depreciation_line_ids' but @api.constrains declares only profile_id -- writing 'depreciation_line_ids' alone skips the check entirely",
|
| 484 |
+
"suppressed": false,
|
| 485 |
+
"advisory": false,
|
| 486 |
+
"likelihood": "unmeasured",
|
| 487 |
+
"path": "account-financial-tools/account_asset_management/models/account_asset.py"
|
| 488 |
+
},
|
| 489 |
+
{
|
| 490 |
+
"rule": "odoo.sudo-bypasses-record-rules",
|
| 491 |
+
"severity": "critical",
|
| 492 |
+
"line": 1190,
|
| 493 |
+
"evidence": "sudo().search() runs with record rules off -- ir.rule filtering, including multi-company isolation, does not apply and the result looks correct in single-company testing",
|
| 494 |
+
"suppressed": false,
|
| 495 |
+
"advisory": false,
|
| 496 |
+
"likelihood": "defect",
|
| 497 |
+
"path": "account-financial-tools/account_asset_management/models/account_asset.py"
|
| 498 |
+
},
|
| 499 |
+
{
|
| 500 |
+
"rule": "odoo.recursion-check-on-hierarchy",
|
| 501 |
+
"severity": "high",
|
| 502 |
+
"line": 27,
|
| 503 |
+
"evidence": "parent_id = fields.Many2one(",
|
| 504 |
+
"suppressed": false,
|
| 505 |
+
"advisory": false,
|
| 506 |
+
"likelihood": "unmeasured",
|
| 507 |
+
"path": "account-financial-tools/account_asset_management/models/account_asset_group.py"
|
| 508 |
+
},
|
| 509 |
+
{
|
| 510 |
+
"rule": "odoo.ondelete-is-a-decision",
|
| 511 |
+
"severity": "high",
|
| 512 |
+
"line": 174,
|
| 513 |
+
"evidence": "asset_profile_id declares no ondelete, so the ORM defaults it to 'set null' -- and asset_profile_id.account_asset_id on line 200 reads through it, which yields False rather than raising once the parent is deleted, and this tree does delete account.asset.profile",
|
| 514 |
+
"suppressed": false,
|
| 515 |
+
"advisory": false,
|
| 516 |
+
"likelihood": "defect",
|
| 517 |
+
"path": "account-financial-tools/account_asset_management/models/account_move.py"
|
| 518 |
+
},
|
| 519 |
+
{
|
| 520 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 521 |
+
"severity": "critical",
|
| 522 |
+
"line": 192,
|
| 523 |
+
"evidence": "_compute_asset_profile() reads 'account_id.asset_profile_id' but @api.depends does not declare it -- the stored value goes stale when 'account_id.asset_profile_id' changes",
|
| 524 |
+
"suppressed": false,
|
| 525 |
+
"advisory": false,
|
| 526 |
+
"likelihood": "construct",
|
| 527 |
+
"path": "account-financial-tools/account_asset_management/models/account_move.py"
|
| 528 |
+
},
|
| 529 |
+
{
|
| 530 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 531 |
+
"severity": "critical",
|
| 532 |
+
"line": 195,
|
| 533 |
+
"evidence": "_compute_asset_profile() reads 'asset_id.profile_id' but @api.depends does not declare it -- the stored value goes stale when 'asset_id.profile_id' changes",
|
| 534 |
+
"suppressed": false,
|
| 535 |
+
"advisory": false,
|
| 536 |
+
"likelihood": "construct",
|
| 537 |
+
"path": "account-financial-tools/account_asset_management/models/account_move.py"
|
| 538 |
+
},
|
| 539 |
+
{
|
| 540 |
+
"rule": "odoo.constrains-iterate-recordset",
|
| 541 |
+
"severity": "high",
|
| 542 |
+
"line": 80,
|
| 543 |
+
"evidence": "@api.constrains(\"sale_value\", \"company_id\")",
|
| 544 |
+
"suppressed": false,
|
| 545 |
+
"advisory": false,
|
| 546 |
+
"likelihood": "unmeasured",
|
| 547 |
+
"path": "account-financial-tools/account_asset_management/wizard/account_asset_remove.py"
|
| 548 |
+
},
|
| 549 |
+
{
|
| 550 |
+
"rule": "odoo.constrains-must-list-every-field-read",
|
| 551 |
+
"severity": "critical",
|
| 552 |
+
"line": 130,
|
| 553 |
+
"evidence": "_check_deposit() reads 'company_id' but @api.constrains declares only cash_journal_id, currency_id -- writing 'company_id' alone skips the check entirely",
|
| 554 |
+
"suppressed": false,
|
| 555 |
+
"advisory": false,
|
| 556 |
+
"likelihood": "unmeasured",
|
| 557 |
+
"path": "account-financial-tools/account_cash_deposit/models/account_cash_deposit.py"
|
| 558 |
+
},
|
| 559 |
+
{
|
| 560 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 561 |
+
"severity": "critical",
|
| 562 |
+
"line": 203,
|
| 563 |
+
"evidence": "_compute_is_reconcile() reads 'cash_journal_id' but @api.depends does not declare it -- the stored value goes stale when 'cash_journal_id' changes",
|
| 564 |
+
"suppressed": false,
|
| 565 |
+
"advisory": false,
|
| 566 |
+
"likelihood": "construct",
|
| 567 |
+
"path": "account-financial-tools/account_cash_deposit/models/account_cash_deposit.py"
|
| 568 |
+
},
|
| 569 |
+
{
|
| 570 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 571 |
+
"severity": "critical",
|
| 572 |
+
"line": 203,
|
| 573 |
+
"evidence": "_compute_is_reconcile() reads 'cash_journal_id.default_account_id' but @api.depends does not declare it -- the stored value goes stale when 'cash_journal_id.default_account_id' changes",
|
| 574 |
+
"suppressed": false,
|
| 575 |
+
"advisory": false,
|
| 576 |
+
"likelihood": "construct",
|
| 577 |
+
"path": "account-financial-tools/account_cash_deposit/models/account_cash_deposit.py"
|
| 578 |
+
},
|
| 579 |
+
{
|
| 580 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 581 |
+
"severity": "critical",
|
| 582 |
+
"line": 203,
|
| 583 |
+
"evidence": "_compute_is_reconcile() reads 'move_id.line_ids.account_id' but @api.depends does not declare it -- the stored value goes stale when 'move_id.line_ids.account_id' changes",
|
| 584 |
+
"suppressed": false,
|
| 585 |
+
"advisory": false,
|
| 586 |
+
"likelihood": "construct",
|
| 587 |
+
"path": "account-financial-tools/account_cash_deposit/models/account_cash_deposit.py"
|
| 588 |
+
},
|
| 589 |
+
{
|
| 590 |
+
"rule": "python.assert-is-not-validation",
|
| 591 |
+
"severity": "critical",
|
| 592 |
+
"line": 265,
|
| 593 |
+
"evidence": "assert self.operation_type == \"order\", \"Wrong operation type\"",
|
| 594 |
+
"suppressed": false,
|
| 595 |
+
"advisory": false,
|
| 596 |
+
"likelihood": "unmeasured",
|
| 597 |
+
"path": "account-financial-tools/account_cash_deposit/models/account_cash_deposit.py"
|
| 598 |
+
},
|
| 599 |
+
{
|
| 600 |
+
"rule": "odoo.float-comparison-needs-float-utils",
|
| 601 |
+
"severity": "high",
|
| 602 |
+
"line": 271,
|
| 603 |
+
"evidence": "self.line_ids.filtered(lambda x: x.qty == 0).unlink()",
|
| 604 |
+
"suppressed": false,
|
| 605 |
+
"advisory": false,
|
| 606 |
+
"likelihood": "unmeasured",
|
| 607 |
+
"path": "account-financial-tools/account_cash_deposit/models/account_cash_deposit.py"
|
| 608 |
+
},
|
| 609 |
+
{
|
| 610 |
+
"rule": "odoo.recursion-check-on-hierarchy",
|
| 611 |
+
"severity": "high",
|
| 612 |
+
"line": 381,
|
| 613 |
+
"evidence": "parent_id = fields.Many2one(\"account.cash.deposit\", ondelete=\"cascade\")",
|
| 614 |
+
"suppressed": false,
|
| 615 |
+
"advisory": false,
|
| 616 |
+
"likelihood": "unmeasured",
|
| 617 |
+
"path": "account-financial-tools/account_cash_deposit/models/account_cash_deposit.py"
|
| 618 |
+
},
|
| 619 |
+
{
|
| 620 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 621 |
+
"severity": "critical",
|
| 622 |
+
"line": 420,
|
| 623 |
+
"evidence": "_compute_subtotal() reads 'cash_unit_id.total_value' but @api.depends does not declare it -- the stored value goes stale when 'cash_unit_id.total_value' changes",
|
| 624 |
+
"suppressed": false,
|
| 625 |
+
"advisory": false,
|
| 626 |
+
"likelihood": "construct",
|
| 627 |
+
"path": "account-financial-tools/account_cash_deposit/models/account_cash_deposit.py"
|
| 628 |
+
},
|
| 629 |
+
{
|
| 630 |
+
"rule": "odoo.prefer-sql-constraint",
|
| 631 |
+
"severity": "high",
|
| 632 |
+
"line": 68,
|
| 633 |
+
"evidence": "_check_cash_unit() compares its own declared column against a literal and does nothing else -- the same rule as a CHECK in models.Constraint holds for every writer, including imports, XML-RPC and sudo",
|
| 634 |
+
"suppressed": false,
|
| 635 |
+
"advisory": true,
|
| 636 |
+
"likelihood": "preference",
|
| 637 |
+
"path": "account-financial-tools/account_cash_deposit/models/cash_unit.py"
|
| 638 |
+
},
|
| 639 |
+
{
|
| 640 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 641 |
+
"severity": "critical",
|
| 642 |
+
"line": 114,
|
| 643 |
+
"evidence": "_compute_display_name() reads 'total_value' but @api.depends does not declare it -- the stored value goes stale when 'total_value' changes",
|
| 644 |
+
"suppressed": false,
|
| 645 |
+
"advisory": false,
|
| 646 |
+
"likelihood": "construct",
|
| 647 |
+
"path": "account-financial-tools/account_cash_deposit/models/cash_unit.py"
|
| 648 |
+
},
|
| 649 |
+
{
|
| 650 |
+
"rule": "python.assert-is-not-validation",
|
| 651 |
+
"severity": "critical",
|
| 652 |
+
"line": 27,
|
| 653 |
+
"evidence": "assert self._context.get(\"active_model\") == \"account.cash.deposit\"",
|
| 654 |
+
"suppressed": false,
|
| 655 |
+
"advisory": false,
|
| 656 |
+
"likelihood": "unmeasured",
|
| 657 |
+
"path": "account-financial-tools/account_cash_deposit/wizards/account_cash_order_reception.py"
|
| 658 |
+
},
|
| 659 |
+
{
|
| 660 |
+
"rule": "odoo.check-company-on-relations",
|
| 661 |
+
"severity": "critical",
|
| 662 |
+
"line": 41,
|
| 663 |
+
"evidence": "relation to account.account with no check_company, on a model that has its own company_id -- a record in one company can reference another company's account.account, and this codebase runs multi-company",
|
| 664 |
+
"suppressed": false,
|
| 665 |
+
"advisory": false,
|
| 666 |
+
"likelihood": "defect",
|
| 667 |
+
"path": "account-financial-tools/account_check_deposit/models/account_check_deposit.py"
|
| 668 |
+
},
|
| 669 |
+
{
|
| 670 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 671 |
+
"severity": "critical",
|
| 672 |
+
"line": 132,
|
| 673 |
+
"evidence": "_compute_check_deposit() reads 'company_id.currency_id' but @api.depends does not declare it -- the stored value goes stale when 'company_id.currency_id' changes",
|
| 674 |
+
"suppressed": false,
|
| 675 |
+
"advisory": false,
|
| 676 |
+
"likelihood": "construct",
|
| 677 |
+
"path": "account-financial-tools/account_check_deposit/models/account_check_deposit.py"
|
| 678 |
+
},
|
| 679 |
+
{
|
| 680 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 681 |
+
"severity": "critical",
|
| 682 |
+
"line": 145,
|
| 683 |
+
"evidence": "_compute_currency_id() reads 'company_id' but @api.depends does not declare it -- the stored value goes stale when 'company_id' changes",
|
| 684 |
+
"suppressed": false,
|
| 685 |
+
"advisory": false,
|
| 686 |
+
"likelihood": "construct",
|
| 687 |
+
"path": "account-financial-tools/account_check_deposit/models/account_check_deposit.py"
|
| 688 |
+
},
|
| 689 |
+
{
|
| 690 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 691 |
+
"severity": "critical",
|
| 692 |
+
"line": 146,
|
| 693 |
+
"evidence": "_compute_currency_id() reads 'journal_id.currency_id' but @api.depends does not declare it -- the stored value goes stale when 'journal_id.currency_id' changes",
|
| 694 |
+
"suppressed": false,
|
| 695 |
+
"advisory": false,
|
| 696 |
+
"likelihood": "construct",
|
| 697 |
+
"path": "account-financial-tools/account_check_deposit/models/account_check_deposit.py"
|
| 698 |
+
},
|
| 699 |
+
{
|
| 700 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 701 |
+
"severity": "critical",
|
| 702 |
+
"line": 151,
|
| 703 |
+
"evidence": "_compute_in_hand_check_account_id() reads 'journal_id.inbound_payment_method_line_ids' but @api.depends does not declare it -- the stored value goes stale when 'journal_id.inbound_payment_method_line_ids' changes",
|
| 704 |
+
"suppressed": false,
|
| 705 |
+
"advisory": false,
|
| 706 |
+
"likelihood": "construct",
|
| 707 |
+
"path": "account-financial-tools/account_check_deposit/models/account_check_deposit.py"
|
| 708 |
+
},
|
| 709 |
+
{
|
| 710 |
+
"rule": "python.assert-is-not-validation",
|
| 711 |
+
"severity": "critical",
|
| 712 |
+
"line": 289,
|
| 713 |
+
"evidence": "assert sign in (1, -1)",
|
| 714 |
+
"suppressed": false,
|
| 715 |
+
"advisory": false,
|
| 716 |
+
"likelihood": "unmeasured",
|
| 717 |
+
"path": "account-financial-tools/account_dashboard_banner/models/account_dashboard_banner_cell.py"
|
| 718 |
+
},
|
| 719 |
+
{
|
| 720 |
+
"rule": "odoo.ondelete-is-a-decision",
|
| 721 |
+
"severity": "high",
|
| 722 |
+
"line": 17,
|
| 723 |
+
"evidence": "mis_report_instance_id declares no ondelete, so the ORM defaults it to 'set null' -- and mis_report_instance_id.currency_id on line 105 reads through it, which yields False rather than raising once the parent is deleted",
|
| 724 |
+
"suppressed": false,
|
| 725 |
+
"advisory": false,
|
| 726 |
+
"likelihood": "construct",
|
| 727 |
+
"path": "account-financial-tools/account_dashboard_banner_mis_builder/models/account_dashboard_banner_cell.py"
|
| 728 |
+
},
|
| 729 |
+
{
|
| 730 |
+
"rule": "odoo.ondelete-is-a-decision",
|
| 731 |
+
"severity": "high",
|
| 732 |
+
"line": 35,
|
| 733 |
+
"evidence": "mis_report_instance_period_id declares no ondelete, so the ORM defaults it to 'set null' -- and mis_report_instance_period_id.date_from on line 103 reads through it, which yields False rather than raising once the parent is deleted",
|
| 734 |
+
"suppressed": false,
|
| 735 |
+
"advisory": false,
|
| 736 |
+
"likelihood": "construct",
|
| 737 |
+
"path": "account-financial-tools/account_dashboard_banner_mis_builder/models/account_dashboard_banner_cell.py"
|
| 738 |
+
},
|
| 739 |
+
{
|
| 740 |
+
"rule": "odoo.constrains-must-list-every-field-read",
|
| 741 |
+
"severity": "critical",
|
| 742 |
+
"line": 47,
|
| 743 |
+
"evidence": "_check_mis_builder() reads 'cell_type' but @api.constrains declares only mis_report_instance_id, mis_report_kpi_id -- writing 'cell_type' alone skips the check entirely",
|
| 744 |
+
"suppressed": false,
|
| 745 |
+
"advisory": false,
|
| 746 |
+
"likelihood": "unmeasured",
|
| 747 |
+
"path": "account-financial-tools/account_dashboard_banner_mis_builder/models/account_dashboard_banner_cell.py"
|
| 748 |
+
},
|
| 749 |
+
{
|
| 750 |
+
"rule": "odoo.constrains-must-list-every-field-read",
|
| 751 |
+
"severity": "critical",
|
| 752 |
+
"line": 74,
|
| 753 |
+
"evidence": "_check_mis_builder() reads 'mis_report_instance_period_id' but @api.constrains declares only mis_report_instance_id, mis_report_kpi_id -- writing 'mis_report_instance_period_id' alone skips the check entirely",
|
| 754 |
+
"suppressed": false,
|
| 755 |
+
"advisory": false,
|
| 756 |
+
"likelihood": "unmeasured",
|
| 757 |
+
"path": "account-financial-tools/account_dashboard_banner_mis_builder/models/account_dashboard_banner_cell.py"
|
| 758 |
+
},
|
| 759 |
+
{
|
| 760 |
+
"rule": "odoo.prefer-sql-constraint",
|
| 761 |
+
"severity": "high",
|
| 762 |
+
"line": 46,
|
| 763 |
+
"evidence": "_check_dates() tests uniqueness in Python -- two concurrent transactions can both pass it and both insert; a UNIQUE models.Constraint cannot race",
|
| 764 |
+
"suppressed": false,
|
| 765 |
+
"advisory": true,
|
| 766 |
+
"likelihood": "preference",
|
| 767 |
+
"path": "account-financial-tools/account_fiscal_year/models/account_fiscal_year.py"
|
| 768 |
+
},
|
| 769 |
+
{
|
| 770 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 771 |
+
"severity": "medium",
|
| 772 |
+
"line": 23,
|
| 773 |
+
"evidence": "last_fiscal_year.date_to < datetime.now().date() + relativedelta(days=1)",
|
| 774 |
+
"suppressed": false,
|
| 775 |
+
"advisory": false,
|
| 776 |
+
"likelihood": "unmeasured",
|
| 777 |
+
"path": "account-financial-tools/account_fiscal_year_auto_create/models/account_fiscal_year.py"
|
| 778 |
+
},
|
| 779 |
+
{
|
| 780 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 781 |
+
"severity": "critical",
|
| 782 |
+
"line": 42,
|
| 783 |
+
"evidence": "_compute_entry_number_sequence() reads 'company_id.name' but @api.depends does not declare it -- the stored value goes stale when 'company_id.name' changes",
|
| 784 |
+
"suppressed": false,
|
| 785 |
+
"advisory": false,
|
| 786 |
+
"likelihood": "construct",
|
| 787 |
+
"path": "account-financial-tools/account_journal_general_sequence/models/account_journal.py"
|
| 788 |
+
},
|
| 789 |
+
{
|
| 790 |
+
"rule": "odoo.check-company-on-relations",
|
| 791 |
+
"severity": "critical",
|
| 792 |
+
"line": 144,
|
| 793 |
+
"evidence": "relation to account.journal with no check_company, on a model that has its own company_id -- a record in one company can reference another company's account.journal, and this codebase runs multi-company",
|
| 794 |
+
"suppressed": false,
|
| 795 |
+
"advisory": false,
|
| 796 |
+
"likelihood": "defect",
|
| 797 |
+
"path": "account-financial-tools/account_loan/models/account_loan.py"
|
| 798 |
+
},
|
| 799 |
+
{
|
| 800 |
+
"rule": "odoo.check-company-on-relations",
|
| 801 |
+
"severity": "critical",
|
| 802 |
+
"line": 149,
|
| 803 |
+
"evidence": "relation to account.account with no check_company, on a model that has its own company_id -- a record in one company can reference another company's account.account, and this codebase runs multi-company",
|
| 804 |
+
"suppressed": false,
|
| 805 |
+
"advisory": false,
|
| 806 |
+
"likelihood": "defect",
|
| 807 |
+
"path": "account-financial-tools/account_loan/models/account_loan.py"
|
| 808 |
+
},
|
| 809 |
+
{
|
| 810 |
+
"rule": "odoo.check-company-on-relations",
|
| 811 |
+
"severity": "critical",
|
| 812 |
+
"line": 156,
|
| 813 |
+
"evidence": "relation to account.account with no check_company, on a model that has its own company_id -- a record in one company can reference another company's account.account, and this codebase runs multi-company",
|
| 814 |
+
"suppressed": false,
|
| 815 |
+
"advisory": false,
|
| 816 |
+
"likelihood": "defect",
|
| 817 |
+
"path": "account-financial-tools/account_loan/models/account_loan.py"
|
| 818 |
+
},
|
| 819 |
+
{
|
| 820 |
+
"rule": "odoo.check-company-on-relations",
|
| 821 |
+
"severity": "critical",
|
| 822 |
+
"line": 162,
|
| 823 |
+
"evidence": "relation to account.account with no check_company, on a model that has its own company_id -- a record in one company can reference another company's account.account, and this codebase runs multi-company",
|
| 824 |
+
"suppressed": false,
|
| 825 |
+
"advisory": false,
|
| 826 |
+
"likelihood": "defect",
|
| 827 |
+
"path": "account-financial-tools/account_loan/models/account_loan.py"
|
| 828 |
+
},
|
| 829 |
+
{
|
| 830 |
+
"rule": "odoo.check-company-on-relations",
|
| 831 |
+
"severity": "critical",
|
| 832 |
+
"line": 170,
|
| 833 |
+
"evidence": "relation to account.account with no check_company, on a model that has its own company_id -- a record in one company can reference another company's account.account, and this codebase runs multi-company",
|
| 834 |
+
"suppressed": false,
|
| 835 |
+
"advisory": false,
|
| 836 |
+
"likelihood": "defect",
|
| 837 |
+
"path": "account-financial-tools/account_loan/models/account_loan.py"
|
| 838 |
+
},
|
| 839 |
+
{
|
| 840 |
+
"rule": "odoo.depends-must-not-name-unread-fields",
|
| 841 |
+
"severity": "medium",
|
| 842 |
+
"line": 246,
|
| 843 |
+
"evidence": "_compute_total_amounts() declares 'currency_id' in @api.depends and never reads it -- the value recomputes on writes that cannot change it, and the decorator stops describing the method",
|
| 844 |
+
"suppressed": false,
|
| 845 |
+
"advisory": false,
|
| 846 |
+
"likelihood": "unmeasured",
|
| 847 |
+
"path": "account-financial-tools/account_loan/models/account_loan.py"
|
| 848 |
+
},
|
| 849 |
+
{
|
| 850 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 851 |
+
"severity": "critical",
|
| 852 |
+
"line": 132,
|
| 853 |
+
"evidence": "_compute_rate() reads 'pending_principal_amount' but @api.depends does not declare it -- the stored value goes stale when 'pending_principal_amount' changes",
|
| 854 |
+
"suppressed": false,
|
| 855 |
+
"advisory": false,
|
| 856 |
+
"likelihood": "construct",
|
| 857 |
+
"path": "account-financial-tools/account_loan/models/account_loan_line.py"
|
| 858 |
+
},
|
| 859 |
+
{
|
| 860 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 861 |
+
"severity": "critical",
|
| 862 |
+
"line": 139,
|
| 863 |
+
"evidence": "_compute_interests_amount() reads 'pending_principal_amount' but @api.depends does not declare it -- the stored value goes stale when 'pending_principal_amount' changes",
|
| 864 |
+
"suppressed": false,
|
| 865 |
+
"advisory": false,
|
| 866 |
+
"likelihood": "construct",
|
| 867 |
+
"path": "account-financial-tools/account_loan/models/account_loan_line.py"
|
| 868 |
+
},
|
| 869 |
+
{
|
| 870 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 871 |
+
"severity": "critical",
|
| 872 |
+
"line": 174,
|
| 873 |
+
"evidence": "_compute_principal_amount() reads 'interests_amount' but @api.depends does not declare it -- the stored value goes stale when 'interests_amount' changes",
|
| 874 |
+
"suppressed": false,
|
| 875 |
+
"advisory": false,
|
| 876 |
+
"likelihood": "construct",
|
| 877 |
+
"path": "account-financial-tools/account_loan/models/account_loan_line.py"
|
| 878 |
+
},
|
| 879 |
+
{
|
| 880 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 881 |
+
"severity": "critical",
|
| 882 |
+
"line": 174,
|
| 883 |
+
"evidence": "_compute_principal_amount() reads 'payment_amount' but @api.depends does not declare it -- the stored value goes stale when 'payment_amount' changes",
|
| 884 |
+
"suppressed": false,
|
| 885 |
+
"advisory": false,
|
| 886 |
+
"likelihood": "construct",
|
| 887 |
+
"path": "account-financial-tools/account_loan/models/account_loan_line.py"
|
| 888 |
+
},
|
| 889 |
+
{
|
| 890 |
+
"rule": "odoo.sudo-bypasses-record-rules",
|
| 891 |
+
"severity": "critical",
|
| 892 |
+
"line": 60,
|
| 893 |
+
"evidence": "sudo().search_read() on ir.model.fields runs with record rules off -- ir.rule filtering, including multi-company isolation, does not apply and the result looks correct in single-company testing",
|
| 894 |
+
"suppressed": false,
|
| 895 |
+
"advisory": false,
|
| 896 |
+
"likelihood": "defect",
|
| 897 |
+
"path": "account-financial-tools/account_lock_date_update/wizards/account_update_lock_date.py"
|
| 898 |
+
},
|
| 899 |
+
{
|
| 900 |
+
"rule": "odoo.ondelete-is-a-decision",
|
| 901 |
+
"severity": "high",
|
| 902 |
+
"line": 59,
|
| 903 |
+
"evidence": "oca_purchase_line_id declares no ondelete, so the ORM defaults it to 'set null' -- and oca_purchase_line_id.order_id on line 84 reads through it, which yields False rather than raising once the parent is deleted",
|
| 904 |
+
"suppressed": false,
|
| 905 |
+
"advisory": false,
|
| 906 |
+
"likelihood": "construct",
|
| 907 |
+
"path": "account-financial-tools/account_move_line_purchase_info/models/account_move.py"
|
| 908 |
+
},
|
| 909 |
+
{
|
| 910 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 911 |
+
"severity": "critical",
|
| 912 |
+
"line": 84,
|
| 913 |
+
"evidence": "_compute_purchase_id() reads 'oca_purchase_line_id.order_id' but @api.depends does not declare it -- the stored value goes stale when 'oca_purchase_line_id.order_id' changes",
|
| 914 |
+
"suppressed": false,
|
| 915 |
+
"advisory": false,
|
| 916 |
+
"likelihood": "construct",
|
| 917 |
+
"path": "account-financial-tools/account_move_line_purchase_info/models/account_move.py"
|
| 918 |
+
},
|
| 919 |
+
{
|
| 920 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 921 |
+
"severity": "critical",
|
| 922 |
+
"line": 84,
|
| 923 |
+
"evidence": "_compute_purchase_id() reads 'purchase_line_id.order_id' but @api.depends does not declare it -- the stored value goes stale when 'purchase_line_id.order_id' changes",
|
| 924 |
+
"suppressed": false,
|
| 925 |
+
"advisory": false,
|
| 926 |
+
"likelihood": "construct",
|
| 927 |
+
"path": "account-financial-tools/account_move_line_purchase_info/models/account_move.py"
|
| 928 |
+
},
|
| 929 |
+
{
|
| 930 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 931 |
+
"severity": "critical",
|
| 932 |
+
"line": 17,
|
| 933 |
+
"evidence": "_compute_display_name() reads 'order_id' but @api.depends does not declare it -- the stored value goes stale when 'order_id' changes",
|
| 934 |
+
"suppressed": false,
|
| 935 |
+
"advisory": false,
|
| 936 |
+
"likelihood": "construct",
|
| 937 |
+
"path": "account-financial-tools/account_move_line_sale_info/models/sale_order_line.py"
|
| 938 |
+
},
|
| 939 |
+
{
|
| 940 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 941 |
+
"severity": "critical",
|
| 942 |
+
"line": 17,
|
| 943 |
+
"evidence": "_compute_display_name() reads 'order_id.name' but @api.depends does not declare it -- the stored value goes stale when 'order_id.name' changes",
|
| 944 |
+
"suppressed": false,
|
| 945 |
+
"advisory": false,
|
| 946 |
+
"likelihood": "construct",
|
| 947 |
+
"path": "account-financial-tools/account_move_line_sale_info/models/sale_order_line.py"
|
| 948 |
+
},
|
| 949 |
+
{
|
| 950 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 951 |
+
"severity": "critical",
|
| 952 |
+
"line": 17,
|
| 953 |
+
"evidence": "_compute_display_name() reads 'product_id' but @api.depends does not declare it -- the stored value goes stale when 'product_id' changes",
|
| 954 |
+
"suppressed": false,
|
| 955 |
+
"advisory": false,
|
| 956 |
+
"likelihood": "construct",
|
| 957 |
+
"path": "account-financial-tools/account_move_line_sale_info/models/sale_order_line.py"
|
| 958 |
+
},
|
| 959 |
+
{
|
| 960 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 961 |
+
"severity": "critical",
|
| 962 |
+
"line": 17,
|
| 963 |
+
"evidence": "_compute_display_name() reads 'product_id.name' but @api.depends does not declare it -- the stored value goes stale when 'product_id.name' changes",
|
| 964 |
+
"suppressed": false,
|
| 965 |
+
"advisory": false,
|
| 966 |
+
"likelihood": "construct",
|
| 967 |
+
"path": "account-financial-tools/account_move_line_sale_info/models/sale_order_line.py"
|
| 968 |
+
},
|
| 969 |
+
{
|
| 970 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 971 |
+
"severity": "critical",
|
| 972 |
+
"line": 18,
|
| 973 |
+
"evidence": "_compute_display_name() reads 'order_id.state' but @api.depends does not declare it -- the stored value goes stale when 'order_id.state' changes",
|
| 974 |
+
"suppressed": false,
|
| 975 |
+
"advisory": false,
|
| 976 |
+
"likelihood": "construct",
|
| 977 |
+
"path": "account-financial-tools/account_move_line_sale_info/models/sale_order_line.py"
|
| 978 |
+
},
|
| 979 |
+
{
|
| 980 |
+
"rule": "odoo.ondelete-is-a-decision",
|
| 981 |
+
"severity": "high",
|
| 982 |
+
"line": 14,
|
| 983 |
+
"evidence": "tax_line_id declares no ondelete, so the ORM defaults it to 'set null' -- and tax_line_id.refund_repartition_line_ids on line 27 reads through it, which yields False rather than raising once the parent is deleted",
|
| 984 |
+
"suppressed": false,
|
| 985 |
+
"advisory": false,
|
| 986 |
+
"likelihood": "construct",
|
| 987 |
+
"path": "account-financial-tools/account_move_line_tax_editable/models/account_move_line.py"
|
| 988 |
+
},
|
| 989 |
+
{
|
| 990 |
+
"rule": "odoo.ondelete-is-a-decision",
|
| 991 |
+
"severity": "high",
|
| 992 |
+
"line": 132,
|
| 993 |
+
"evidence": "partner_id declares no ondelete, so the ORM defaults it to 'set null' -- and partner_id.category_id on line 206 reads through it, which yields False rather than raising once the parent is deleted",
|
| 994 |
+
"suppressed": false,
|
| 995 |
+
"advisory": false,
|
| 996 |
+
"likelihood": "construct",
|
| 997 |
+
"path": "account-financial-tools/account_move_template/models/account_move_template.py"
|
| 998 |
+
},
|
| 999 |
+
{
|
| 1000 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 1001 |
+
"severity": "critical",
|
| 1002 |
+
"line": 206,
|
| 1003 |
+
"evidence": "_compute_analytic_distribution() reads 'partner_id.category_id' but @api.depends does not declare it -- the stored value goes stale when 'partner_id.category_id' changes",
|
| 1004 |
+
"suppressed": false,
|
| 1005 |
+
"advisory": false,
|
| 1006 |
+
"likelihood": "construct",
|
| 1007 |
+
"path": "account-financial-tools/account_move_template/models/account_move_template.py"
|
| 1008 |
+
},
|
| 1009 |
+
{
|
| 1010 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 1011 |
+
"severity": "critical",
|
| 1012 |
+
"line": 209,
|
| 1013 |
+
"evidence": "_compute_analytic_distribution() reads 'account_id.code' but @api.depends does not declare it -- the stored value goes stale when 'account_id.code' changes",
|
| 1014 |
+
"suppressed": false,
|
| 1015 |
+
"advisory": false,
|
| 1016 |
+
"likelihood": "construct",
|
| 1017 |
+
"path": "account-financial-tools/account_move_template/models/account_move_template.py"
|
| 1018 |
+
},
|
| 1019 |
+
{
|
| 1020 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 1021 |
+
"severity": "critical",
|
| 1022 |
+
"line": 210,
|
| 1023 |
+
"evidence": "_compute_analytic_distribution() reads 'template_id' but @api.depends does not declare it -- the stored value goes stale when 'template_id' changes",
|
| 1024 |
+
"suppressed": false,
|
| 1025 |
+
"advisory": false,
|
| 1026 |
+
"likelihood": "construct",
|
| 1027 |
+
"path": "account-financial-tools/account_move_template/models/account_move_template.py"
|
| 1028 |
+
},
|
| 1029 |
+
{
|
| 1030 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 1031 |
+
"severity": "critical",
|
| 1032 |
+
"line": 210,
|
| 1033 |
+
"evidence": "_compute_analytic_distribution() reads 'template_id.company_id' but @api.depends does not declare it -- the stored value goes stale when 'template_id.company_id' changes",
|
| 1034 |
+
"suppressed": false,
|
| 1035 |
+
"advisory": false,
|
| 1036 |
+
"likelihood": "construct",
|
| 1037 |
+
"path": "account-financial-tools/account_move_template/models/account_move_template.py"
|
| 1038 |
+
},
|
| 1039 |
+
{
|
| 1040 |
+
"rule": "python.assert-is-not-validation",
|
| 1041 |
+
"severity": "critical",
|
| 1042 |
+
"line": 117,
|
| 1043 |
+
"evidence": "assert isinstance(overwrite_vals, dict)",
|
| 1044 |
+
"suppressed": false,
|
| 1045 |
+
"advisory": false,
|
| 1046 |
+
"likelihood": "unmeasured",
|
| 1047 |
+
"path": "account-financial-tools/account_move_template/wizard/account_move_template_run.py"
|
| 1048 |
+
},
|
| 1049 |
+
{
|
| 1050 |
+
"rule": "odoo.check-company-on-relations",
|
| 1051 |
+
"severity": "critical",
|
| 1052 |
+
"line": 12,
|
| 1053 |
+
"evidence": "relation to account.journal with no check_company, on a model that has its own company_id -- a record in one company can reference another company's account.journal, and this codebase runs multi-company",
|
| 1054 |
+
"suppressed": false,
|
| 1055 |
+
"advisory": false,
|
| 1056 |
+
"likelihood": "defect",
|
| 1057 |
+
"path": "account-financial-tools/account_payroll_sheet_import/models/payroll_import_mapping.py"
|
| 1058 |
+
},
|
| 1059 |
+
{
|
| 1060 |
+
"rule": "odoo.check-company-on-relations",
|
| 1061 |
+
"severity": "critical",
|
| 1062 |
+
"line": 36,
|
| 1063 |
+
"evidence": "relation to account.account with no check_company, on a model that has its own company_id -- a record in one company can reference another company's account.account, and this codebase runs multi-company",
|
| 1064 |
+
"suppressed": false,
|
| 1065 |
+
"advisory": false,
|
| 1066 |
+
"likelihood": "defect",
|
| 1067 |
+
"path": "account-financial-tools/account_payroll_sheet_import/models/payroll_import_mapping.py"
|
| 1068 |
+
},
|
| 1069 |
+
{
|
| 1070 |
+
"rule": "python.decimal-for-money-not-float",
|
| 1071 |
+
"severity": "high",
|
| 1072 |
+
"line": 110,
|
| 1073 |
+
"evidence": "amount = float(raw_value)",
|
| 1074 |
+
"suppressed": false,
|
| 1075 |
+
"advisory": false,
|
| 1076 |
+
"likelihood": "unmeasured",
|
| 1077 |
+
"path": "account-financial-tools/account_payroll_sheet_import/wizard/payroll_import_wizard.py"
|
| 1078 |
+
},
|
| 1079 |
+
{
|
| 1080 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 1081 |
+
"severity": "critical",
|
| 1082 |
+
"line": 53,
|
| 1083 |
+
"evidence": "_compute_name() reads 'create_date' but @api.depends does not declare it -- the stored value goes stale when 'create_date' changes",
|
| 1084 |
+
"suppressed": false,
|
| 1085 |
+
"advisory": false,
|
| 1086 |
+
"likelihood": "construct",
|
| 1087 |
+
"path": "account-financial-tools/account_sequence_option/models/account_move.py"
|
| 1088 |
+
},
|
| 1089 |
+
{
|
| 1090 |
+
"rule": "odoo.constrains-must-list-every-field-read",
|
| 1091 |
+
"severity": "critical",
|
| 1092 |
+
"line": 71,
|
| 1093 |
+
"evidence": "_check_spread_account_balance_sheet() reads 'move_id' but @api.constrains declares only account_id, spread_id -- writing 'move_id' alone skips the check entirely",
|
| 1094 |
+
"suppressed": false,
|
| 1095 |
+
"advisory": false,
|
| 1096 |
+
"likelihood": "unmeasured",
|
| 1097 |
+
"path": "account-financial-tools/account_spread_cost_revenue/models/account_move_line.py"
|
| 1098 |
+
},
|
| 1099 |
+
{
|
| 1100 |
+
"rule": "odoo.ondelete-is-a-decision",
|
| 1101 |
+
"severity": "high",
|
| 1102 |
+
"line": 22,
|
| 1103 |
+
"evidence": "template_id declares no ondelete, so the ORM defaults it to 'set null' -- and template_id.spread_type on line 310 reads through it, which yields False rather than raising once the parent is deleted, and this tree does delete account.spread.template",
|
| 1104 |
+
"suppressed": false,
|
| 1105 |
+
"advisory": false,
|
| 1106 |
+
"likelihood": "defect",
|
| 1107 |
+
"path": "account-financial-tools/account_spread_cost_revenue/models/account_spread.py"
|
| 1108 |
+
},
|
| 1109 |
+
{
|
| 1110 |
+
"rule": "odoo.check-company-on-relations",
|
| 1111 |
+
"severity": "critical",
|
| 1112 |
+
"line": 55,
|
| 1113 |
+
"evidence": "relation to account.account with no check_company, on a model that has its own company_id -- a record in one company can reference another company's account.account, and this codebase runs multi-company",
|
| 1114 |
+
"suppressed": false,
|
| 1115 |
+
"advisory": false,
|
| 1116 |
+
"likelihood": "defect",
|
| 1117 |
+
"path": "account-financial-tools/account_spread_cost_revenue/models/account_spread.py"
|
| 1118 |
+
},
|
| 1119 |
+
{
|
| 1120 |
+
"rule": "odoo.check-company-on-relations",
|
| 1121 |
+
"severity": "critical",
|
| 1122 |
+
"line": 62,
|
| 1123 |
+
"evidence": "relation to account.account with no check_company, on a model that has its own company_id -- a record in one company can reference another company's account.account, and this codebase runs multi-company",
|
| 1124 |
+
"suppressed": false,
|
| 1125 |
+
"advisory": false,
|
| 1126 |
+
"likelihood": "defect",
|
| 1127 |
+
"path": "account-financial-tools/account_spread_cost_revenue/models/account_spread.py"
|
| 1128 |
+
},
|
| 1129 |
+
{
|
| 1130 |
+
"rule": "odoo.ondelete-is-a-decision",
|
| 1131 |
+
"severity": "high",
|
| 1132 |
+
"line": 113,
|
| 1133 |
+
"evidence": "invoice_line_id declares no ondelete, so the ORM defaults it to 'set null' -- and invoice_line_id.name on line 537 reads through it, which yields False rather than raising once the parent is deleted",
|
| 1134 |
+
"suppressed": false,
|
| 1135 |
+
"advisory": false,
|
| 1136 |
+
"likelihood": "construct",
|
| 1137 |
+
"path": "account-financial-tools/account_spread_cost_revenue/models/account_spread.py"
|
| 1138 |
+
},
|
| 1139 |
+
{
|
| 1140 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 1141 |
+
"severity": "critical",
|
| 1142 |
+
"line": 218,
|
| 1143 |
+
"evidence": "_compute_all_posted() reads 'currency_id' but @api.depends does not declare it -- the stored value goes stale when 'currency_id' changes",
|
| 1144 |
+
"suppressed": false,
|
| 1145 |
+
"advisory": false,
|
| 1146 |
+
"likelihood": "construct",
|
| 1147 |
+
"path": "account-financial-tools/account_spread_cost_revenue/models/account_spread.py"
|
| 1148 |
+
},
|
| 1149 |
+
{
|
| 1150 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 1151 |
+
"severity": "critical",
|
| 1152 |
+
"line": 218,
|
| 1153 |
+
"evidence": "_compute_all_posted() reads 'currency_id.rounding' but @api.depends does not declare it -- the stored value goes stale when 'currency_id.rounding' changes",
|
| 1154 |
+
"suppressed": false,
|
| 1155 |
+
"advisory": false,
|
| 1156 |
+
"likelihood": "construct",
|
| 1157 |
+
"path": "account-financial-tools/account_spread_cost_revenue/models/account_spread.py"
|
| 1158 |
+
},
|
| 1159 |
+
{
|
| 1160 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 1161 |
+
"severity": "critical",
|
| 1162 |
+
"line": 268,
|
| 1163 |
+
"evidence": "_compute_journal_id() reads 'company_id.default_spread_expense_journal_id' but @api.depends does not declare it -- the stored value goes stale when 'company_id.default_spread_expense_journal_id' changes",
|
| 1164 |
+
"suppressed": false,
|
| 1165 |
+
"advisory": false,
|
| 1166 |
+
"likelihood": "construct",
|
| 1167 |
+
"path": "account-financial-tools/account_spread_cost_revenue/models/account_spread.py"
|
| 1168 |
+
},
|
| 1169 |
+
{
|
| 1170 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 1171 |
+
"severity": "critical",
|
| 1172 |
+
"line": 270,
|
| 1173 |
+
"evidence": "_compute_journal_id() reads 'company_id.default_spread_revenue_journal_id' but @api.depends does not declare it -- the stored value goes stale when 'company_id.default_spread_revenue_journal_id' changes",
|
| 1174 |
+
"suppressed": false,
|
| 1175 |
+
"advisory": false,
|
| 1176 |
+
"likelihood": "construct",
|
| 1177 |
+
"path": "account-financial-tools/account_spread_cost_revenue/models/account_spread.py"
|
| 1178 |
+
},
|
| 1179 |
+
{
|
| 1180 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 1181 |
+
"severity": "critical",
|
| 1182 |
+
"line": 280,
|
| 1183 |
+
"evidence": "_compute_debit_account_id() reads 'company_id.default_spread_revenue_account_id' but @api.depends does not declare it -- the stored value goes stale when 'company_id.default_spread_revenue_account_id' changes",
|
| 1184 |
+
"suppressed": false,
|
| 1185 |
+
"advisory": false,
|
| 1186 |
+
"likelihood": "construct",
|
| 1187 |
+
"path": "account-financial-tools/account_spread_cost_revenue/models/account_spread.py"
|
| 1188 |
+
},
|
| 1189 |
+
{
|
| 1190 |
+
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 1191 |
+
"severity": "critical",
|
| 1192 |
+
"line": 288,
|
| 1193 |
+
"evidence": "_compute_credit_account_id() reads 'company_id.default_spread_expense_account_id' but @api.depends does not declare it -- the stored value goes stale when 'company_id.default_spread_expense_account_id' changes",
|
| 1194 |
+
"suppressed": false,
|
| 1195 |
+
"advisory": false,
|
| 1196 |
+
"likelihood": "construct",
|
| 1197 |
+
"path": "account-financial-tools/account_spread_cost_revenue/models/account_spread.py"
|
| 1198 |
+
},
|
| 1199 |
+
{
|
| 1200 |
+
"rule": "odoo.check-company-on-relations",
|
| 1201 |
+
"severity": "critical",
|
| 1202 |
+
"line": 20,
|
| 1203 |
+
"evidence": "relation to account.journal with no check_company, on a model that has its own company_id -- a record in one company can reference another company's account.journal, and this codebase runs multi-company",
|
| 1204 |
+
"suppressed": false,
|
| 1205 |
+
"advisory": false,
|
| 1206 |
+
"likelihood": "defect",
|
| 1207 |
+
"path": "account-financial-tools/account_spread_cost_revenue/models/account_spread_template.py"
|
| 1208 |
+
},
|
| 1209 |
+
{
|
| 1210 |
+
"rule": "odoo.check-company-on-relations",
|
| 1211 |
+
"severity": "critical",
|
| 1212 |
+
"line": 33,
|
| 1213 |
+
"evidence": "relation to account.account with no check_company, on a model that has its own company_id -- a record in one company can reference another company's account.account, and this codebase runs multi-company",
|
| 1214 |
+
"suppressed": false,
|
| 1215 |
+
"advisory": false,
|
| 1216 |
+
"likelihood": "defect",
|
| 1217 |
+
"path": "account-financial-tools/account_spread_cost_revenue/models/account_spread_template.py"
|
| 1218 |
+
},
|
| 1219 |
+
{
|
| 1220 |
+
"rule": "odoo.check-company-on-relations",
|
| 1221 |
+
"severity": "critical",
|
| 1222 |
+
"line": 41,
|
| 1223 |
+
"evidence": "relation to account.account with no check_company, on a model that has its own company_id -- a record in one company can reference another company's account.account, and this codebase runs multi-company",
|
| 1224 |
+
"suppressed": false,
|
| 1225 |
+
"advisory": false,
|
| 1226 |
+
"likelihood": "defect",
|
| 1227 |
+
"path": "account-financial-tools/account_spread_cost_revenue/models/account_spread_template.py"
|
| 1228 |
+
},
|
| 1229 |
+
{
|
| 1230 |
+
"rule": "odoo.check-company-on-relations",
|
| 1231 |
+
"severity": "critical",
|
| 1232 |
+
"line": 176,
|
| 1233 |
+
"evidence": "relation to account.account with no check_company, on a model that has its own company_id -- a record in one company can reference another company's account.account, and this codebase runs multi-company",
|
| 1234 |
+
"suppressed": false,
|
| 1235 |
+
"advisory": false,
|
| 1236 |
+
"likelihood": "defect",
|
| 1237 |
+
"path": "account-financial-tools/account_spread_cost_revenue/models/account_spread_template.py"
|
| 1238 |
+
},
|
| 1239 |
+
{
|
| 1240 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 1241 |
+
"severity": "medium",
|
| 1242 |
+
"line": 45,
|
| 1243 |
+
"evidence": "move_date = writeoff_vals.get(\"date\", datetime.now())",
|
| 1244 |
+
"suppressed": false,
|
| 1245 |
+
"advisory": false,
|
| 1246 |
+
"likelihood": "unmeasured",
|
| 1247 |
+
"path": "account-financial-tools/purchase_unreconciled/models/account_move_line.py"
|
| 1248 |
+
},
|
| 1249 |
+
{
|
| 1250 |
+
"rule": "odoo.sudo-bypasses-record-rules",
|
| 1251 |
+
"severity": "critical",
|
| 1252 |
+
"line": 14,
|
| 1253 |
+
"evidence": "sudo().search() on ir.model runs with record rules off -- ir.rule filtering, including multi-company isolation, does not apply and the result looks correct in single-company testing",
|
| 1254 |
+
"suppressed": false,
|
| 1255 |
+
"advisory": false,
|
| 1256 |
+
"likelihood": "defect",
|
| 1257 |
+
"path": "account-financial-tools/purchase_unreconciled/wizards/purchase_unreconciled_exceeded.py"
|
| 1258 |
+
}
|
| 1259 |
+
]
|
| 1260 |
+
}
|
|
@@ -5,7 +5,7 @@
|
|
| 5 |
],
|
| 6 |
"files": 1351,
|
| 7 |
"lines": 226937,
|
| 8 |
-
"checkers":
|
| 9 |
"triggers": {
|
| 10 |
"files": 2265,
|
| 11 |
"models_declared": 492,
|
|
@@ -15,6 +15,26 @@
|
|
| 15 |
"multi_company": true
|
| 16 |
},
|
| 17 |
"hits": [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
{
|
| 19 |
"rule": "odoo.sudo-bypasses-record-rules",
|
| 20 |
"severity": "critical",
|
|
@@ -55,6 +75,26 @@
|
|
| 55 |
"likelihood": "defect",
|
| 56 |
"path": "fastapi/models/fastapi_endpoint_demo.py"
|
| 57 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
{
|
| 59 |
"rule": "odoo.ondelete-is-a-decision",
|
| 60 |
"severity": "high",
|
|
@@ -135,6 +175,16 @@
|
|
| 135 |
"likelihood": "defect",
|
| 136 |
"path": "spp_analytics/models/indicator_registry.py"
|
| 137 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
{
|
| 139 |
"rule": "odoo.sudo-bypasses-record-rules",
|
| 140 |
"severity": "critical",
|
|
@@ -225,6 +275,46 @@
|
|
| 225 |
"likelihood": "preference",
|
| 226 |
"path": "spp_api_v2/models/api_path_filter.py"
|
| 227 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
{
|
| 229 |
"rule": "odoo.sudo-bypasses-record-rules",
|
| 230 |
"severity": "critical",
|
|
@@ -235,6 +325,26 @@
|
|
| 235 |
"likelihood": "defect",
|
| 236 |
"path": "spp_api_v2/services/group_service.py"
|
| 237 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
{
|
| 239 |
"rule": "odoo.sudo-bypasses-record-rules",
|
| 240 |
"severity": "critical",
|
|
@@ -245,6 +355,16 @@
|
|
| 245 |
"likelihood": "defect",
|
| 246 |
"path": "spp_api_v2/services/group_service.py"
|
| 247 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
{
|
| 249 |
"rule": "odoo.sudo-bypasses-record-rules",
|
| 250 |
"severity": "critical",
|
|
@@ -725,6 +845,46 @@
|
|
| 725 |
"likelihood": "defect",
|
| 726 |
"path": "spp_attendance/controllers/controllers.py"
|
| 727 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 728 |
{
|
| 729 |
"rule": "odoo.recursion-check-on-hierarchy",
|
| 730 |
"severity": "high",
|
|
@@ -825,6 +985,16 @@
|
|
| 825 |
"likelihood": "unmeasured",
|
| 826 |
"path": "spp_cel_domain/models/cel_expression.py"
|
| 827 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 828 |
{
|
| 829 |
"rule": "odoo.constrains-must-list-every-field-read",
|
| 830 |
"severity": "critical",
|
|
@@ -895,6 +1065,36 @@
|
|
| 895 |
"likelihood": "preference",
|
| 896 |
"path": "spp_cel_domain/models/data_provider.py"
|
| 897 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 898 |
{
|
| 899 |
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 900 |
"severity": "critical",
|
|
@@ -1255,6 +1455,16 @@
|
|
| 1255 |
"likelihood": "construct",
|
| 1256 |
"path": "spp_dci_client_sr/models/sr_record.py"
|
| 1257 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1258 |
{
|
| 1259 |
"rule": "odoo.prefer-sql-constraint",
|
| 1260 |
"severity": "high",
|
|
@@ -1605,6 +1815,26 @@
|
|
| 1605 |
"likelihood": "unmeasured",
|
| 1606 |
"path": "spp_drims/wizard/report_4w_wizard.py"
|
| 1607 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1608 |
{
|
| 1609 |
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 1610 |
"severity": "critical",
|
|
@@ -1715,6 +1945,16 @@
|
|
| 1715 |
"likelihood": "unmeasured",
|
| 1716 |
"path": "spp_farmer_registry_demo/models/farmer_demo_generator.py"
|
| 1717 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1718 |
{
|
| 1719 |
"rule": "odoo.sudo-bypasses-record-rules",
|
| 1720 |
"severity": "critical",
|
|
@@ -1995,6 +2235,16 @@
|
|
| 1995 |
"likelihood": "preference",
|
| 1996 |
"path": "spp_grm_demo/models/generate_tickets.py"
|
| 1997 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1998 |
{
|
| 1999 |
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 2000 |
"severity": "critical",
|
|
@@ -2165,6 +2415,46 @@
|
|
| 2165 |
"likelihood": "unmeasured",
|
| 2166 |
"path": "spp_mis_demo_v2/models/mis_demo_generator.py"
|
| 2167 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2168 |
{
|
| 2169 |
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 2170 |
"severity": "critical",
|
|
@@ -2355,6 +2645,26 @@
|
|
| 2355 |
"likelihood": "unmeasured",
|
| 2356 |
"path": "spp_programs/models/accounting/fund_report.py"
|
| 2357 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2358 |
{
|
| 2359 |
"rule": "odoo.sudo-bypasses-record-rules",
|
| 2360 |
"severity": "critical",
|
|
@@ -2465,6 +2775,16 @@
|
|
| 2465 |
"likelihood": "unmeasured",
|
| 2466 |
"path": "spp_programs/models/managers/payment_manager.py"
|
| 2467 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2468 |
{
|
| 2469 |
"rule": "odoo.onchange-is-not-validation",
|
| 2470 |
"severity": "critical",
|
|
@@ -2565,6 +2885,16 @@
|
|
| 2565 |
"likelihood": "preference",
|
| 2566 |
"path": "spp_registry/models/reg_id.py"
|
| 2567 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2568 |
{
|
| 2569 |
"rule": "odoo.constrains-must-list-every-field-read",
|
| 2570 |
"severity": "critical",
|
|
|
|
| 5 |
],
|
| 6 |
"files": 1351,
|
| 7 |
"lines": 226937,
|
| 8 |
+
"checkers": 26,
|
| 9 |
"triggers": {
|
| 10 |
"files": 2265,
|
| 11 |
"models_declared": 492,
|
|
|
|
| 15 |
"multi_company": true
|
| 16 |
},
|
| 17 |
"hits": [
|
| 18 |
+
{
|
| 19 |
+
"rule": "python.assert-is-not-validation",
|
| 20 |
+
"severity": "critical",
|
| 21 |
+
"line": 314,
|
| 22 |
+
"evidence": "assert \"klass_dotted_path\" in value[\"handler\"]",
|
| 23 |
+
"suppressed": false,
|
| 24 |
+
"advisory": false,
|
| 25 |
+
"likelihood": "unmeasured",
|
| 26 |
+
"path": "endpoint_route_handler/registry.py"
|
| 27 |
+
},
|
| 28 |
+
{
|
| 29 |
+
"rule": "python.assert-is-not-validation",
|
| 30 |
+
"severity": "critical",
|
| 31 |
+
"line": 315,
|
| 32 |
+
"evidence": "assert \"method_name\" in value[\"handler\"]",
|
| 33 |
+
"suppressed": false,
|
| 34 |
+
"advisory": false,
|
| 35 |
+
"likelihood": "unmeasured",
|
| 36 |
+
"path": "endpoint_route_handler/registry.py"
|
| 37 |
+
},
|
| 38 |
{
|
| 39 |
"rule": "odoo.sudo-bypasses-record-rules",
|
| 40 |
"severity": "critical",
|
|
|
|
| 75 |
"likelihood": "defect",
|
| 76 |
"path": "fastapi/models/fastapi_endpoint_demo.py"
|
| 77 |
},
|
| 78 |
+
{
|
| 79 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 80 |
+
"severity": "medium",
|
| 81 |
+
"line": 745,
|
| 82 |
+
"evidence": "f\"**Generated:** {__import__('datetime').datetime.now().isoformat()}\",",
|
| 83 |
+
"suppressed": false,
|
| 84 |
+
"advisory": false,
|
| 85 |
+
"likelihood": "unmeasured",
|
| 86 |
+
"path": "scripts/compliance/checker.py"
|
| 87 |
+
},
|
| 88 |
+
{
|
| 89 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 90 |
+
"severity": "medium",
|
| 91 |
+
"line": 813,
|
| 92 |
+
"evidence": "\"generated\": __import__(\"datetime\").datetime.now().isoformat(),",
|
| 93 |
+
"suppressed": false,
|
| 94 |
+
"advisory": false,
|
| 95 |
+
"likelihood": "unmeasured",
|
| 96 |
+
"path": "scripts/compliance/checker.py"
|
| 97 |
+
},
|
| 98 |
{
|
| 99 |
"rule": "odoo.ondelete-is-a-decision",
|
| 100 |
"severity": "high",
|
|
|
|
| 175 |
"likelihood": "defect",
|
| 176 |
"path": "spp_analytics/models/indicator_registry.py"
|
| 177 |
},
|
| 178 |
+
{
|
| 179 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 180 |
+
"severity": "medium",
|
| 181 |
+
"line": 97,
|
| 182 |
+
"evidence": "\"computed_at\": datetime.now().isoformat(),",
|
| 183 |
+
"suppressed": false,
|
| 184 |
+
"advisory": false,
|
| 185 |
+
"likelihood": "unmeasured",
|
| 186 |
+
"path": "spp_analytics/models/service_aggregation.py"
|
| 187 |
+
},
|
| 188 |
{
|
| 189 |
"rule": "odoo.sudo-bypasses-record-rules",
|
| 190 |
"severity": "critical",
|
|
|
|
| 275 |
"likelihood": "preference",
|
| 276 |
"path": "spp_api_v2/models/api_path_filter.py"
|
| 277 |
},
|
| 278 |
+
{
|
| 279 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 280 |
+
"severity": "medium",
|
| 281 |
+
"line": 178,
|
| 282 |
+
"evidence": "revoked_at=datetime.utcnow(),",
|
| 283 |
+
"suppressed": false,
|
| 284 |
+
"advisory": false,
|
| 285 |
+
"likelihood": "unmeasured",
|
| 286 |
+
"path": "spp_api_v2/routers/consent.py"
|
| 287 |
+
},
|
| 288 |
+
{
|
| 289 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 290 |
+
"severity": "medium",
|
| 291 |
+
"line": 270,
|
| 292 |
+
"evidence": "timestamp=datetime.utcnow(),",
|
| 293 |
+
"suppressed": true,
|
| 294 |
+
"advisory": false,
|
| 295 |
+
"likelihood": "unmeasured",
|
| 296 |
+
"path": "spp_api_v2/routers/consent.py"
|
| 297 |
+
},
|
| 298 |
+
{
|
| 299 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 300 |
+
"severity": "medium",
|
| 301 |
+
"line": 217,
|
| 302 |
+
"evidence": "now = datetime.utcnow()",
|
| 303 |
+
"suppressed": false,
|
| 304 |
+
"advisory": false,
|
| 305 |
+
"likelihood": "unmeasured",
|
| 306 |
+
"path": "spp_api_v2/routers/oauth.py"
|
| 307 |
+
},
|
| 308 |
+
{
|
| 309 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 310 |
+
"severity": "medium",
|
| 311 |
+
"line": 142,
|
| 312 |
+
"evidence": "now = datetime.now()",
|
| 313 |
+
"suppressed": false,
|
| 314 |
+
"advisory": false,
|
| 315 |
+
"likelihood": "unmeasured",
|
| 316 |
+
"path": "spp_api_v2/services/group_service.py"
|
| 317 |
+
},
|
| 318 |
{
|
| 319 |
"rule": "odoo.sudo-bypasses-record-rules",
|
| 320 |
"severity": "critical",
|
|
|
|
| 325 |
"likelihood": "defect",
|
| 326 |
"path": "spp_api_v2/services/group_service.py"
|
| 327 |
},
|
| 328 |
+
{
|
| 329 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 330 |
+
"severity": "medium",
|
| 331 |
+
"line": 748,
|
| 332 |
+
"evidence": "ended_datetime = datetime.now()",
|
| 333 |
+
"suppressed": false,
|
| 334 |
+
"advisory": false,
|
| 335 |
+
"likelihood": "unmeasured",
|
| 336 |
+
"path": "spp_api_v2/services/group_service.py"
|
| 337 |
+
},
|
| 338 |
+
{
|
| 339 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 340 |
+
"severity": "medium",
|
| 341 |
+
"line": 886,
|
| 342 |
+
"evidence": "now = datetime.now()",
|
| 343 |
+
"suppressed": false,
|
| 344 |
+
"advisory": false,
|
| 345 |
+
"likelihood": "unmeasured",
|
| 346 |
+
"path": "spp_api_v2/services/group_service.py"
|
| 347 |
+
},
|
| 348 |
{
|
| 349 |
"rule": "odoo.sudo-bypasses-record-rules",
|
| 350 |
"severity": "critical",
|
|
|
|
| 355 |
"likelihood": "defect",
|
| 356 |
"path": "spp_api_v2/services/group_service.py"
|
| 357 |
},
|
| 358 |
+
{
|
| 359 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 360 |
+
"severity": "medium",
|
| 361 |
+
"line": 1100,
|
| 362 |
+
"evidence": "now = datetime.now()",
|
| 363 |
+
"suppressed": false,
|
| 364 |
+
"advisory": false,
|
| 365 |
+
"likelihood": "unmeasured",
|
| 366 |
+
"path": "spp_api_v2/services/group_service.py"
|
| 367 |
+
},
|
| 368 |
{
|
| 369 |
"rule": "odoo.sudo-bypasses-record-rules",
|
| 370 |
"severity": "critical",
|
|
|
|
| 845 |
"likelihood": "defect",
|
| 846 |
"path": "spp_attendance/controllers/controllers.py"
|
| 847 |
},
|
| 848 |
+
{
|
| 849 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 850 |
+
"severity": "medium",
|
| 851 |
+
"line": 198,
|
| 852 |
+
"evidence": "current_date = datetime.now().strftime(\"%Y-%m-%d\")",
|
| 853 |
+
"suppressed": false,
|
| 854 |
+
"advisory": false,
|
| 855 |
+
"likelihood": "unmeasured",
|
| 856 |
+
"path": "spp_attendance/controllers/controllers.py"
|
| 857 |
+
},
|
| 858 |
+
{
|
| 859 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 860 |
+
"severity": "medium",
|
| 861 |
+
"line": 351,
|
| 862 |
+
"evidence": "current_date = datetime.now().strftime(\"%Y-%m-%d\")",
|
| 863 |
+
"suppressed": false,
|
| 864 |
+
"advisory": false,
|
| 865 |
+
"likelihood": "unmeasured",
|
| 866 |
+
"path": "spp_attendance/controllers/controllers.py"
|
| 867 |
+
},
|
| 868 |
+
{
|
| 869 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 870 |
+
"severity": "medium",
|
| 871 |
+
"line": 198,
|
| 872 |
+
"evidence": "today = datetime.utcnow().strftime(\"%Y-%m-%d\")",
|
| 873 |
+
"suppressed": false,
|
| 874 |
+
"advisory": false,
|
| 875 |
+
"likelihood": "unmeasured",
|
| 876 |
+
"path": "spp_audit/models/spp_audit_backend.py"
|
| 877 |
+
},
|
| 878 |
+
{
|
| 879 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 880 |
+
"severity": "medium",
|
| 881 |
+
"line": 216,
|
| 882 |
+
"evidence": "today = datetime.utcnow().strftime(\"%Y-%m-%d\")",
|
| 883 |
+
"suppressed": false,
|
| 884 |
+
"advisory": false,
|
| 885 |
+
"likelihood": "unmeasured",
|
| 886 |
+
"path": "spp_audit/models/spp_audit_backend.py"
|
| 887 |
+
},
|
| 888 |
{
|
| 889 |
"rule": "odoo.recursion-check-on-hierarchy",
|
| 890 |
"severity": "high",
|
|
|
|
| 985 |
"likelihood": "unmeasured",
|
| 986 |
"path": "spp_cel_domain/models/cel_expression.py"
|
| 987 |
},
|
| 988 |
+
{
|
| 989 |
+
"rule": "python.eval-and-pickle-execute-their-input",
|
| 990 |
+
"severity": "critical",
|
| 991 |
+
"line": 417,
|
| 992 |
+
"evidence": "and secure than regex-based conversion to Python eval().",
|
| 993 |
+
"suppressed": false,
|
| 994 |
+
"advisory": false,
|
| 995 |
+
"likelihood": "unmeasured",
|
| 996 |
+
"path": "spp_cel_domain/models/cel_service.py"
|
| 997 |
+
},
|
| 998 |
{
|
| 999 |
"rule": "odoo.constrains-must-list-every-field-read",
|
| 1000 |
"severity": "critical",
|
|
|
|
| 1065 |
"likelihood": "preference",
|
| 1066 |
"path": "spp_cel_domain/models/data_provider.py"
|
| 1067 |
},
|
| 1068 |
+
{
|
| 1069 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 1070 |
+
"severity": "medium",
|
| 1071 |
+
"line": 92,
|
| 1072 |
+
"evidence": "return datetime.now()",
|
| 1073 |
+
"suppressed": false,
|
| 1074 |
+
"advisory": false,
|
| 1075 |
+
"likelihood": "unmeasured",
|
| 1076 |
+
"path": "spp_cel_domain/services/cel_functions.py"
|
| 1077 |
+
},
|
| 1078 |
+
{
|
| 1079 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 1080 |
+
"severity": "medium",
|
| 1081 |
+
"line": 757,
|
| 1082 |
+
"evidence": ">>> two_hours_ago = datetime.now() - timedelta(hours=2)",
|
| 1083 |
+
"suppressed": false,
|
| 1084 |
+
"advisory": false,
|
| 1085 |
+
"likelihood": "unmeasured",
|
| 1086 |
+
"path": "spp_cel_domain/services/cel_parser.py"
|
| 1087 |
+
},
|
| 1088 |
+
{
|
| 1089 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 1090 |
+
"severity": "medium",
|
| 1091 |
+
"line": 766,
|
| 1092 |
+
"evidence": "delta = datetime.now() - datetime_value",
|
| 1093 |
+
"suppressed": false,
|
| 1094 |
+
"advisory": false,
|
| 1095 |
+
"likelihood": "unmeasured",
|
| 1096 |
+
"path": "spp_cel_domain/services/cel_parser.py"
|
| 1097 |
+
},
|
| 1098 |
{
|
| 1099 |
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 1100 |
"severity": "critical",
|
|
|
|
| 1455 |
"likelihood": "construct",
|
| 1456 |
"path": "spp_dci_client_sr/models/sr_record.py"
|
| 1457 |
},
|
| 1458 |
+
{
|
| 1459 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 1460 |
+
"severity": "medium",
|
| 1461 |
+
"line": 52,
|
| 1462 |
+
"evidence": "now = datetime.now()",
|
| 1463 |
+
"suppressed": false,
|
| 1464 |
+
"advisory": false,
|
| 1465 |
+
"likelihood": "unmeasured",
|
| 1466 |
+
"path": "spp_dci_server/middleware/rate_limit.py"
|
| 1467 |
+
},
|
| 1468 |
{
|
| 1469 |
"rule": "odoo.prefer-sql-constraint",
|
| 1470 |
"severity": "high",
|
|
|
|
| 1815 |
"likelihood": "unmeasured",
|
| 1816 |
"path": "spp_drims/wizard/report_4w_wizard.py"
|
| 1817 |
},
|
| 1818 |
+
{
|
| 1819 |
+
"rule": "python.path-join-does-not-contain-a-path",
|
| 1820 |
+
"severity": "high",
|
| 1821 |
+
"line": 486,
|
| 1822 |
+
"evidence": "file_path = os.path.join(data_path, file_config[\"filename\"])",
|
| 1823 |
+
"suppressed": false,
|
| 1824 |
+
"advisory": false,
|
| 1825 |
+
"likelihood": "unmeasured",
|
| 1826 |
+
"path": "spp_drims_sl_demo/wizard/drims_demo_generator.py"
|
| 1827 |
+
},
|
| 1828 |
+
{
|
| 1829 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 1830 |
+
"severity": "medium",
|
| 1831 |
+
"line": 110,
|
| 1832 |
+
"evidence": "curr_datetime = f\"{datetime.now().isoformat(timespec='milliseconds')}Z\"",
|
| 1833 |
+
"suppressed": false,
|
| 1834 |
+
"advisory": false,
|
| 1835 |
+
"likelihood": "unmeasured",
|
| 1836 |
+
"path": "spp_encryption/models/encryption_provider.py"
|
| 1837 |
+
},
|
| 1838 |
{
|
| 1839 |
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 1840 |
"severity": "critical",
|
|
|
|
| 1945 |
"likelihood": "unmeasured",
|
| 1946 |
"path": "spp_farmer_registry_demo/models/farmer_demo_generator.py"
|
| 1947 |
},
|
| 1948 |
+
{
|
| 1949 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 1950 |
+
"severity": "medium",
|
| 1951 |
+
"line": 1127,
|
| 1952 |
+
"evidence": "enrollment_dt = datetime.datetime.now()",
|
| 1953 |
+
"suppressed": false,
|
| 1954 |
+
"advisory": false,
|
| 1955 |
+
"likelihood": "unmeasured",
|
| 1956 |
+
"path": "spp_farmer_registry_demo/models/seeded_farm_generator.py"
|
| 1957 |
+
},
|
| 1958 |
{
|
| 1959 |
"rule": "odoo.sudo-bypasses-record-rules",
|
| 1960 |
"severity": "critical",
|
|
|
|
| 2235 |
"likelihood": "preference",
|
| 2236 |
"path": "spp_grm_demo/models/generate_tickets.py"
|
| 2237 |
},
|
| 2238 |
+
{
|
| 2239 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 2240 |
+
"severity": "medium",
|
| 2241 |
+
"line": 922,
|
| 2242 |
+
"evidence": "now = datetime.now()",
|
| 2243 |
+
"suppressed": false,
|
| 2244 |
+
"advisory": false,
|
| 2245 |
+
"likelihood": "unmeasured",
|
| 2246 |
+
"path": "spp_grm_demo/models/generate_tickets.py"
|
| 2247 |
+
},
|
| 2248 |
{
|
| 2249 |
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 2250 |
"severity": "critical",
|
|
|
|
| 2415 |
"likelihood": "unmeasured",
|
| 2416 |
"path": "spp_mis_demo_v2/models/mis_demo_generator.py"
|
| 2417 |
},
|
| 2418 |
+
{
|
| 2419 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 2420 |
+
"severity": "medium",
|
| 2421 |
+
"line": 3388,
|
| 2422 |
+
"evidence": "current_period = datetime.datetime.now().strftime(\"%Y-%m\")",
|
| 2423 |
+
"suppressed": false,
|
| 2424 |
+
"advisory": false,
|
| 2425 |
+
"likelihood": "unmeasured",
|
| 2426 |
+
"path": "spp_mis_demo_v2/models/mis_demo_generator.py"
|
| 2427 |
+
},
|
| 2428 |
+
{
|
| 2429 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 2430 |
+
"severity": "medium",
|
| 2431 |
+
"line": 3390,
|
| 2432 |
+
"evidence": "(datetime.datetime.now() - datetime.timedelta(days=30 * i)).strftime(\"%Y-%m\")",
|
| 2433 |
+
"suppressed": false,
|
| 2434 |
+
"advisory": false,
|
| 2435 |
+
"likelihood": "unmeasured",
|
| 2436 |
+
"path": "spp_mis_demo_v2/models/mis_demo_generator.py"
|
| 2437 |
+
},
|
| 2438 |
+
{
|
| 2439 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 2440 |
+
"severity": "medium",
|
| 2441 |
+
"line": 3740,
|
| 2442 |
+
"evidence": "now = datetime.datetime.now()",
|
| 2443 |
+
"suppressed": false,
|
| 2444 |
+
"advisory": false,
|
| 2445 |
+
"likelihood": "unmeasured",
|
| 2446 |
+
"path": "spp_mis_demo_v2/models/mis_demo_generator.py"
|
| 2447 |
+
},
|
| 2448 |
+
{
|
| 2449 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 2450 |
+
"severity": "medium",
|
| 2451 |
+
"line": 406,
|
| 2452 |
+
"evidence": "enrollment_dt = datetime.datetime.now()",
|
| 2453 |
+
"suppressed": false,
|
| 2454 |
+
"advisory": false,
|
| 2455 |
+
"likelihood": "unmeasured",
|
| 2456 |
+
"path": "spp_mis_demo_v2/models/seeded_volume_generator.py"
|
| 2457 |
+
},
|
| 2458 |
{
|
| 2459 |
"rule": "odoo.stored-compute-needs-complete-depends",
|
| 2460 |
"severity": "critical",
|
|
|
|
| 2645 |
"likelihood": "unmeasured",
|
| 2646 |
"path": "spp_programs/models/accounting/fund_report.py"
|
| 2647 |
},
|
| 2648 |
+
{
|
| 2649 |
+
"rule": "python.decimal-for-money-not-float",
|
| 2650 |
+
"severity": "high",
|
| 2651 |
+
"line": 300,
|
| 2652 |
+
"evidence": "\"base_amount\": float(self.amount or 0.0),",
|
| 2653 |
+
"suppressed": false,
|
| 2654 |
+
"advisory": false,
|
| 2655 |
+
"likelihood": "unmeasured",
|
| 2656 |
+
"path": "spp_programs/models/cel/entitlement_amount_cel.py"
|
| 2657 |
+
},
|
| 2658 |
+
{
|
| 2659 |
+
"rule": "python.decimal-for-money-not-float",
|
| 2660 |
+
"severity": "high",
|
| 2661 |
+
"line": 316,
|
| 2662 |
+
"evidence": "amount = float(result)",
|
| 2663 |
+
"suppressed": false,
|
| 2664 |
+
"advisory": false,
|
| 2665 |
+
"likelihood": "unmeasured",
|
| 2666 |
+
"path": "spp_programs/models/cel/entitlement_amount_cel.py"
|
| 2667 |
+
},
|
| 2668 |
{
|
| 2669 |
"rule": "odoo.sudo-bypasses-record-rules",
|
| 2670 |
"severity": "critical",
|
|
|
|
| 2775 |
"likelihood": "unmeasured",
|
| 2776 |
"path": "spp_programs/models/managers/payment_manager.py"
|
| 2777 |
},
|
| 2778 |
+
{
|
| 2779 |
+
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 2780 |
+
"severity": "medium",
|
| 2781 |
+
"line": 141,
|
| 2782 |
+
"evidence": "new_cycle = cm.new_cycle(\"Cycle 1\", datetime.now(), 1)",
|
| 2783 |
+
"suppressed": false,
|
| 2784 |
+
"advisory": false,
|
| 2785 |
+
"likelihood": "unmeasured",
|
| 2786 |
+
"path": "spp_programs/models/managers/program_manager.py"
|
| 2787 |
+
},
|
| 2788 |
{
|
| 2789 |
"rule": "odoo.onchange-is-not-validation",
|
| 2790 |
"severity": "critical",
|
|
|
|
| 2885 |
"likelihood": "preference",
|
| 2886 |
"path": "spp_registry/models/reg_id.py"
|
| 2887 |
},
|
| 2888 |
+
{
|
| 2889 |
+
"rule": "python.assert-is-not-validation",
|
| 2890 |
+
"severity": "critical",
|
| 2891 |
+
"line": 165,
|
| 2892 |
+
"evidence": "assert side in [\"source\", \"destination\"]",
|
| 2893 |
+
"suppressed": false,
|
| 2894 |
+
"advisory": false,
|
| 2895 |
+
"likelihood": "unmeasured",
|
| 2896 |
+
"path": "spp_registry/models/reg_relationship.py"
|
| 2897 |
+
},
|
| 2898 |
{
|
| 2899 |
"rule": "odoo.constrains-must-list-every-field-read",
|
| 2900 |
"severity": "critical",
|
|
@@ -1,50 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"stack": "python",
|
| 3 |
-
"roots": [
|
| 4 |
-
"llm-router-main",
|
| 5 |
-
"scripts"
|
| 6 |
-
],
|
| 7 |
-
"files": 42,
|
| 8 |
-
"lines": 10225,
|
| 9 |
-
"checkers": 8,
|
| 10 |
-
"triggers": {
|
| 11 |
-
"files": 40,
|
| 12 |
-
"models_declared": 0,
|
| 13 |
-
"models_deleted": 0,
|
| 14 |
-
"declared_and_deleted": 0,
|
| 15 |
-
"never_deleted": 0,
|
| 16 |
-
"multi_company": true
|
| 17 |
-
},
|
| 18 |
-
"hits": [
|
| 19 |
-
{
|
| 20 |
-
"rule": "python.assert-is-not-validation",
|
| 21 |
-
"severity": "critical",
|
| 22 |
-
"line": 129,
|
| 23 |
-
"evidence": "correlation_id=\"req-1\")",
|
| 24 |
-
"suppressed": false,
|
| 25 |
-
"advisory": false,
|
| 26 |
-
"likelihood": "unmeasured",
|
| 27 |
-
"path": "nexa/llm-router-main/router/security/audit.py"
|
| 28 |
-
},
|
| 29 |
-
{
|
| 30 |
-
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 31 |
-
"severity": "medium",
|
| 32 |
-
"line": 745,
|
| 33 |
-
"evidence": "f\"**Generated:** {__import__('datetime').datetime.now().isoformat()}\",",
|
| 34 |
-
"suppressed": false,
|
| 35 |
-
"advisory": false,
|
| 36 |
-
"likelihood": "unmeasured",
|
| 37 |
-
"path": "OpenSPP2-19.0/scripts/compliance/checker.py"
|
| 38 |
-
},
|
| 39 |
-
{
|
| 40 |
-
"rule": "python.naive-and-aware-datetimes-do-not-compare",
|
| 41 |
-
"severity": "medium",
|
| 42 |
-
"line": 813,
|
| 43 |
-
"evidence": "\"generated\": __import__(\"datetime\").datetime.now().isoformat(),",
|
| 44 |
-
"suppressed": false,
|
| 45 |
-
"advisory": false,
|
| 46 |
-
"likelihood": "unmeasured",
|
| 47 |
-
"path": "OpenSPP2-19.0/scripts/compliance/checker.py"
|
| 48 |
-
}
|
| 49 |
-
]
|
| 50 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,2790 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"stack": "typescript",
|
| 3 |
-
"roots": [
|
| 4 |
-
"hono",
|
| 5 |
-
"trpc"
|
| 6 |
-
],
|
| 7 |
-
"files": 781,
|
| 8 |
-
"lines": 73762,
|
| 9 |
-
"checkers": 8,
|
| 10 |
-
"triggers": {
|
| 11 |
-
"files": 0,
|
| 12 |
-
"models_declared": 0,
|
| 13 |
-
"models_deleted": 0,
|
| 14 |
-
"declared_and_deleted": 0,
|
| 15 |
-
"never_deleted": 0,
|
| 16 |
-
"multi_company": false
|
| 17 |
-
},
|
| 18 |
-
"hits": [
|
| 19 |
-
{
|
| 20 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 21 |
-
"severity": "high",
|
| 22 |
-
"line": 4,
|
| 23 |
-
"evidence": "export const buildPage = ({ jsx, Fragment }: { jsx: any; Fragment: any }) => {",
|
| 24 |
-
"suppressed": false,
|
| 25 |
-
"advisory": false,
|
| 26 |
-
"likelihood": "unmeasured",
|
| 27 |
-
"path": "hono/benchmarks/jsx/src/page-react.tsx"
|
| 28 |
-
},
|
| 29 |
-
{
|
| 30 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 31 |
-
"severity": "high",
|
| 32 |
-
"line": 4,
|
| 33 |
-
"evidence": "export const buildPage = ({ jsx, Fragment }: { jsx: any; Fragment: any }) => {",
|
| 34 |
-
"suppressed": false,
|
| 35 |
-
"advisory": false,
|
| 36 |
-
"likelihood": "unmeasured",
|
| 37 |
-
"path": "hono/benchmarks/jsx/src/page.tsx"
|
| 38 |
-
},
|
| 39 |
-
{
|
| 40 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 41 |
-
"severity": "high",
|
| 42 |
-
"line": 46,
|
| 43 |
-
"evidence": "props: any",
|
| 44 |
-
"suppressed": false,
|
| 45 |
-
"advisory": false,
|
| 46 |
-
"likelihood": "unmeasured",
|
| 47 |
-
"path": "hono/src/context.ts"
|
| 48 |
-
},
|
| 49 |
-
{
|
| 50 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 51 |
-
"severity": "high",
|
| 52 |
-
"line": 51,
|
| 53 |
-
"evidence": "exports?: any",
|
| 54 |
-
"suppressed": false,
|
| 55 |
-
"advisory": false,
|
| 56 |
-
"likelihood": "unmeasured",
|
| 57 |
-
"path": "hono/src/context.ts"
|
| 58 |
-
},
|
| 59 |
-
{
|
| 60 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 61 |
-
"severity": "high",
|
| 62 |
-
"line": 333,
|
| 63 |
-
"evidence": "applicationHandler: (request: Request, ...args: any) => Response | Promise<Response>,",
|
| 64 |
-
"suppressed": false,
|
| 65 |
-
"advisory": false,
|
| 66 |
-
"likelihood": "unmeasured",
|
| 67 |
-
"path": "hono/src/hono-base.ts"
|
| 68 |
-
},
|
| 69 |
-
{
|
| 70 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 71 |
-
"severity": "high",
|
| 72 |
-
"line": 21,
|
| 73 |
-
"evidence": "json: any",
|
| 74 |
-
"suppressed": false,
|
| 75 |
-
"advisory": false,
|
| 76 |
-
"likelihood": "unmeasured",
|
| 77 |
-
"path": "hono/src/request.ts"
|
| 78 |
-
},
|
| 79 |
-
{
|
| 80 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 81 |
-
"severity": "high",
|
| 82 |
-
"line": 123,
|
| 83 |
-
"evidence": "#getParamValue(paramKey: any): string | undefined {",
|
| 84 |
-
"suppressed": false,
|
| 85 |
-
"advisory": false,
|
| 86 |
-
"likelihood": "unmeasured",
|
| 87 |
-
"path": "hono/src/request.ts"
|
| 88 |
-
},
|
| 89 |
-
{
|
| 90 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 91 |
-
"severity": "high",
|
| 92 |
-
"line": 2547,
|
| 93 |
-
"evidence": "input: any",
|
| 94 |
-
"suppressed": false,
|
| 95 |
-
"advisory": false,
|
| 96 |
-
"likelihood": "unmeasured",
|
| 97 |
-
"path": "hono/src/types.ts"
|
| 98 |
-
},
|
| 99 |
-
{
|
| 100 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 101 |
-
"severity": "high",
|
| 102 |
-
"line": 2548,
|
| 103 |
-
"evidence": "output: any",
|
| 104 |
-
"suppressed": false,
|
| 105 |
-
"advisory": false,
|
| 106 |
-
"likelihood": "unmeasured",
|
| 107 |
-
"path": "hono/src/types.ts"
|
| 108 |
-
},
|
| 109 |
-
{
|
| 110 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 111 |
-
"severity": "high",
|
| 112 |
-
"line": 2662,
|
| 113 |
-
"evidence": "type MergeMiddlewareResponse<T> = T extends (c: any, next: any) => Promise<infer R>",
|
| 114 |
-
"suppressed": false,
|
| 115 |
-
"advisory": false,
|
| 116 |
-
"likelihood": "unmeasured",
|
| 117 |
-
"path": "hono/src/types.ts"
|
| 118 |
-
},
|
| 119 |
-
{
|
| 120 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 121 |
-
"severity": "high",
|
| 122 |
-
"line": 2668,
|
| 123 |
-
"evidence": ": T extends (c: any, next: any) => infer R",
|
| 124 |
-
"suppressed": false,
|
| 125 |
-
"advisory": false,
|
| 126 |
-
"likelihood": "unmeasured",
|
| 127 |
-
"path": "hono/src/types.ts"
|
| 128 |
-
},
|
| 129 |
-
{
|
| 130 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 131 |
-
"severity": "high",
|
| 132 |
-
"line": 2684,
|
| 133 |
-
"evidence": "json: any",
|
| 134 |
-
"suppressed": false,
|
| 135 |
-
"advisory": false,
|
| 136 |
-
"likelihood": "unmeasured",
|
| 137 |
-
"path": "hono/src/types.ts"
|
| 138 |
-
},
|
| 139 |
-
{
|
| 140 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 141 |
-
"severity": "high",
|
| 142 |
-
"line": 2750,
|
| 143 |
-
"evidence": "export type ExtractHandlerResponse<T> = T extends (c: any, next: any) => Promise<infer R>",
|
| 144 |
-
"suppressed": false,
|
| 145 |
-
"advisory": false,
|
| 146 |
-
"likelihood": "unmeasured",
|
| 147 |
-
"path": "hono/src/types.ts"
|
| 148 |
-
},
|
| 149 |
-
{
|
| 150 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 151 |
-
"severity": "high",
|
| 152 |
-
"line": 2756,
|
| 153 |
-
"evidence": ": T extends (c: any, next: any) => infer R",
|
| 154 |
-
"suppressed": false,
|
| 155 |
-
"advisory": false,
|
| 156 |
-
"likelihood": "unmeasured",
|
| 157 |
-
"path": "hono/src/types.ts"
|
| 158 |
-
},
|
| 159 |
-
{
|
| 160 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 161 |
-
"severity": "high",
|
| 162 |
-
"line": 11,
|
| 163 |
-
"evidence": "Custom?: any",
|
| 164 |
-
"suppressed": false,
|
| 165 |
-
"advisory": false,
|
| 166 |
-
"likelihood": "unmeasured",
|
| 167 |
-
"path": "hono/src/adapter/aws-lambda/types.ts"
|
| 168 |
-
},
|
| 169 |
-
{
|
| 170 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 171 |
-
"severity": "high",
|
| 172 |
-
"line": 18,
|
| 173 |
-
"evidence": "props: any",
|
| 174 |
-
"suppressed": false,
|
| 175 |
-
"advisory": false,
|
| 176 |
-
"likelihood": "unmeasured",
|
| 177 |
-
"path": "hono/src/adapter/cloudflare-pages/handler.ts"
|
| 178 |
-
},
|
| 179 |
-
{
|
| 180 |
-
"rule": "typescript.types-are-erased-at-runtime",
|
| 181 |
-
"severity": "critical",
|
| 182 |
-
"line": 49,
|
| 183 |
-
"evidence": "return content as unknown as ReadableStream",
|
| 184 |
-
"suppressed": false,
|
| 185 |
-
"advisory": false,
|
| 186 |
-
"likelihood": "unmeasured",
|
| 187 |
-
"path": "hono/src/adapter/cloudflare-workers/utils.ts"
|
| 188 |
-
},
|
| 189 |
-
{
|
| 190 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 191 |
-
"severity": "high",
|
| 192 |
-
"line": 6,
|
| 193 |
-
"evidence": "): ((req: Request, context: any) => Response | Promise<Response>) => {",
|
| 194 |
-
"suppressed": false,
|
| 195 |
-
"advisory": false,
|
| 196 |
-
"likelihood": "unmeasured",
|
| 197 |
-
"path": "hono/src/adapter/netlify/handler.ts"
|
| 198 |
-
},
|
| 199 |
-
{
|
| 200 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 201 |
-
"severity": "high",
|
| 202 |
-
"line": 7,
|
| 203 |
-
"evidence": "return (req: Request, context: any) => {",
|
| 204 |
-
"suppressed": false,
|
| 205 |
-
"advisory": false,
|
| 206 |
-
"likelihood": "unmeasured",
|
| 207 |
-
"path": "hono/src/adapter/netlify/handler.ts"
|
| 208 |
-
},
|
| 209 |
-
{
|
| 210 |
-
"rule": "typescript.types-are-erased-at-runtime",
|
| 211 |
-
"severity": "critical",
|
| 212 |
-
"line": 15,
|
| 213 |
-
"evidence": "const _fetchRes = (await fetchRes) as unknown as Response & {",
|
| 214 |
-
"suppressed": false,
|
| 215 |
-
"advisory": false,
|
| 216 |
-
"likelihood": "unmeasured",
|
| 217 |
-
"path": "hono/src/client/fetch-result-please.ts"
|
| 218 |
-
},
|
| 219 |
-
{
|
| 220 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 221 |
-
"severity": "high",
|
| 222 |
-
"line": 16,
|
| 223 |
-
"evidence": "_data: any",
|
| 224 |
-
"suppressed": false,
|
| 225 |
-
"advisory": false,
|
| 226 |
-
"likelihood": "unmeasured",
|
| 227 |
-
"path": "hono/src/client/fetch-result-please.ts"
|
| 228 |
-
},
|
| 229 |
-
{
|
| 230 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 231 |
-
"severity": "high",
|
| 232 |
-
"line": 22,
|
| 233 |
-
"evidence": "_bodyInit?: any",
|
| 234 |
-
"suppressed": false,
|
| 235 |
-
"advisory": false,
|
| 236 |
-
"likelihood": "unmeasured",
|
| 237 |
-
"path": "hono/src/client/fetch-result-please.ts"
|
| 238 |
-
},
|
| 239 |
-
{
|
| 240 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 241 |
-
"severity": "high",
|
| 242 |
-
"line": 50,
|
| 243 |
-
"evidence": "public detail?: any",
|
| 244 |
-
"suppressed": false,
|
| 245 |
-
"advisory": false,
|
| 246 |
-
"likelihood": "unmeasured",
|
| 247 |
-
"path": "hono/src/client/fetch-result-please.ts"
|
| 248 |
-
},
|
| 249 |
-
{
|
| 250 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 251 |
-
"severity": "high",
|
| 252 |
-
"line": 54,
|
| 253 |
-
"evidence": "public code?: any",
|
| 254 |
-
"suppressed": false,
|
| 255 |
-
"advisory": false,
|
| 256 |
-
"likelihood": "unmeasured",
|
| 257 |
-
"path": "hono/src/client/fetch-result-please.ts"
|
| 258 |
-
},
|
| 259 |
-
{
|
| 260 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 261 |
-
"severity": "high",
|
| 262 |
-
"line": 58,
|
| 263 |
-
"evidence": "public log?: any",
|
| 264 |
-
"suppressed": false,
|
| 265 |
-
"advisory": false,
|
| 266 |
-
"likelihood": "unmeasured",
|
| 267 |
-
"path": "hono/src/client/fetch-result-please.ts"
|
| 268 |
-
},
|
| 269 |
-
{
|
| 270 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 271 |
-
"severity": "high",
|
| 272 |
-
"line": 62,
|
| 273 |
-
"evidence": "public statusCode?: any",
|
| 274 |
-
"suppressed": false,
|
| 275 |
-
"advisory": false,
|
| 276 |
-
"likelihood": "unmeasured",
|
| 277 |
-
"path": "hono/src/client/fetch-result-please.ts"
|
| 278 |
-
},
|
| 279 |
-
{
|
| 280 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 281 |
-
"severity": "high",
|
| 282 |
-
"line": 66,
|
| 283 |
-
"evidence": "options: { detail?: any; code?: any; statusCode?: number; log?: any } = {}",
|
| 284 |
-
"suppressed": false,
|
| 285 |
-
"advisory": false,
|
| 286 |
-
"likelihood": "unmeasured",
|
| 287 |
-
"path": "hono/src/client/fetch-result-please.ts"
|
| 288 |
-
},
|
| 289 |
-
{
|
| 290 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 291 |
-
"severity": "high",
|
| 292 |
-
"line": 242,
|
| 293 |
-
"evidence": "options: any | undefined",
|
| 294 |
-
"suppressed": false,
|
| 295 |
-
"advisory": false,
|
| 296 |
-
"likelihood": "unmeasured",
|
| 297 |
-
"path": "hono/src/client/types.ts"
|
| 298 |
-
},
|
| 299 |
-
{
|
| 300 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 301 |
-
"severity": "high",
|
| 302 |
-
"line": 268,
|
| 303 |
-
"evidence": "options: any | undefined",
|
| 304 |
-
"suppressed": false,
|
| 305 |
-
"advisory": false,
|
| 306 |
-
"likelihood": "unmeasured",
|
| 307 |
-
"path": "hono/src/client/types.ts"
|
| 308 |
-
},
|
| 309 |
-
{
|
| 310 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 311 |
-
"severity": "high",
|
| 312 |
-
"line": 275,
|
| 313 |
-
"evidence": "args: any,",
|
| 314 |
-
"suppressed": false,
|
| 315 |
-
"advisory": false,
|
| 316 |
-
"likelihood": "unmeasured",
|
| 317 |
-
"path": "hono/src/client/types.ts"
|
| 318 |
-
},
|
| 319 |
-
{
|
| 320 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 321 |
-
"severity": "high",
|
| 322 |
-
"line": 325,
|
| 323 |
-
"evidence": "args: any[]",
|
| 324 |
-
"suppressed": false,
|
| 325 |
-
"advisory": false,
|
| 326 |
-
"likelihood": "unmeasured",
|
| 327 |
-
"path": "hono/src/client/types.ts"
|
| 328 |
-
},
|
| 329 |
-
{
|
| 330 |
-
"rule": "typescript.types-are-erased-at-runtime",
|
| 331 |
-
"severity": "critical",
|
| 332 |
-
"line": 195,
|
| 333 |
-
"evidence": "children ? (children as unknown as CssClassName)[STYLE_STRING] : ''",
|
| 334 |
-
"suppressed": false,
|
| 335 |
-
"advisory": false,
|
| 336 |
-
"likelihood": "unmeasured",
|
| 337 |
-
"path": "hono/src/helper/css/index.ts"
|
| 338 |
-
},
|
| 339 |
-
{
|
| 340 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 341 |
-
"severity": "high",
|
| 342 |
-
"line": 357,
|
| 343 |
-
"evidence": "createHandlers: CreateHandlersInterface<E, P> = (...handlers: any) => {",
|
| 344 |
-
"suppressed": false,
|
| 345 |
-
"advisory": false,
|
| 346 |
-
"likelihood": "unmeasured",
|
| 347 |
-
"path": "hono/src/helper/factory/index.ts"
|
| 348 |
-
},
|
| 349 |
-
{
|
| 350 |
-
"rule": "typescript.types-are-erased-at-runtime",
|
| 351 |
-
"severity": "critical",
|
| 352 |
-
"line": 240,
|
| 353 |
-
"evidence": "forGetInfoURLRequest = maybeRequest as unknown as AddedSSGDataRequest",
|
| 354 |
-
"suppressed": false,
|
| 355 |
-
"advisory": false,
|
| 356 |
-
"likelihood": "unmeasured",
|
| 357 |
-
"path": "hono/src/helper/ssg/ssg.ts"
|
| 358 |
-
},
|
| 359 |
-
{
|
| 360 |
-
"rule": "typescript.types-are-erased-at-runtime",
|
| 361 |
-
"severity": "critical",
|
| 362 |
-
"line": 150,
|
| 363 |
-
"evidence": "} else if ((child as unknown as HtmlEscaped).isEscaped) {",
|
| 364 |
-
"suppressed": false,
|
| 365 |
-
"advisory": false,
|
| 366 |
-
"likelihood": "unmeasured",
|
| 367 |
-
"path": "hono/src/jsx/base.ts"
|
| 368 |
-
},
|
| 369 |
-
{
|
| 370 |
-
"rule": "typescript.types-are-erased-at-runtime",
|
| 371 |
-
"severity": "critical",
|
| 372 |
-
"line": 152,
|
| 373 |
-
"evidence": "const callbacks = (child as unknown as HtmlEscapedString).callbacks",
|
| 374 |
-
"suppressed": false,
|
| 375 |
-
"advisory": false,
|
| 376 |
-
"likelihood": "unmeasured",
|
| 377 |
-
"path": "hono/src/jsx/base.ts"
|
| 378 |
-
},
|
| 379 |
-
{
|
| 380 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 381 |
-
"severity": "high",
|
| 382 |
-
"line": 223,
|
| 383 |
-
"evidence": "get ref(): any {",
|
| 384 |
-
"suppressed": false,
|
| 385 |
-
"advisory": false,
|
| 386 |
-
"likelihood": "unmeasured",
|
| 387 |
-
"path": "hono/src/jsx/base.ts"
|
| 388 |
-
},
|
| 389 |
-
{
|
| 390 |
-
"rule": "typescript.types-are-erased-at-runtime",
|
| 391 |
-
"severity": "critical",
|
| 392 |
-
"line": 88,
|
| 393 |
-
"evidence": "(awaitedFallback as unknown as HtmlEscapedString).callbacks",
|
| 394 |
-
"suppressed": false,
|
| 395 |
-
"advisory": false,
|
| 396 |
-
"likelihood": "unmeasured",
|
| 397 |
-
"path": "hono/src/jsx/components.ts"
|
| 398 |
-
},
|
| 399 |
-
{
|
| 400 |
-
"rule": "typescript.types-are-erased-at-runtime",
|
| 401 |
-
"severity": "critical",
|
| 402 |
-
"line": 109,
|
| 403 |
-
"evidence": "resArray = children.map(resolveChildEarly) as unknown as HtmlEscapedString[]",
|
| 404 |
-
"suppressed": false,
|
| 405 |
-
"advisory": false,
|
| 406 |
-
"likelihood": "unmeasured",
|
| 407 |
-
"path": "hono/src/jsx/components.ts"
|
| 408 |
-
},
|
| 409 |
-
{
|
| 410 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 411 |
-
"severity": "high",
|
| 412 |
-
"line": 19,
|
| 413 |
-
"evidence": "type AnyAttributes = { [attributeName: string]: any }",
|
| 414 |
-
"suppressed": false,
|
| 415 |
-
"advisory": false,
|
| 416 |
-
"likelihood": "unmeasured",
|
| 417 |
-
"path": "hono/src/jsx/intrinsic-elements.ts"
|
| 418 |
-
},
|
| 419 |
-
{
|
| 420 |
-
"rule": "typescript.types-are-erased-at-runtime",
|
| 421 |
-
"severity": "critical",
|
| 422 |
-
"line": 39,
|
| 423 |
-
"evidence": "} else if (typeof v === 'number' || (v as unknown as HtmlEscaped).isEscaped) {",
|
| 424 |
-
"suppressed": false,
|
| 425 |
-
"advisory": false,
|
| 426 |
-
"likelihood": "unmeasured",
|
| 427 |
-
"path": "hono/src/jsx/jsx-runtime.ts"
|
| 428 |
-
},
|
| 429 |
-
{
|
| 430 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 431 |
-
"severity": "high",
|
| 432 |
-
"line": 51,
|
| 433 |
-
"evidence": "export const Suspense: FC<PropsWithChildren<{ fallback: any }>> = async ({",
|
| 434 |
-
"suppressed": false,
|
| 435 |
-
"advisory": false,
|
| 436 |
-
"likelihood": "unmeasured",
|
| 437 |
-
"path": "hono/src/jsx/streaming.ts"
|
| 438 |
-
},
|
| 439 |
-
{
|
| 440 |
-
"rule": "typescript.types-are-erased-at-runtime",
|
| 441 |
-
"severity": "critical",
|
| 442 |
-
"line": 64,
|
| 443 |
-
"evidence": "const stackNode = { [DOM_STASH]: [0, []] } as unknown as NodeObject",
|
| 444 |
-
"suppressed": false,
|
| 445 |
-
"advisory": false,
|
| 446 |
-
"likelihood": "unmeasured",
|
| 447 |
-
"path": "hono/src/jsx/streaming.ts"
|
| 448 |
-
},
|
| 449 |
-
{
|
| 450 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 451 |
-
"severity": "high",
|
| 452 |
-
"line": 13,
|
| 453 |
-
"evidence": "> = (({ children, fallback, fallbackRender, onError }: any) => {",
|
| 454 |
-
"suppressed": false,
|
| 455 |
-
"advisory": false,
|
| 456 |
-
"likelihood": "unmeasured",
|
| 457 |
-
"path": "hono/src/jsx/dom/components.ts"
|
| 458 |
-
},
|
| 459 |
-
{
|
| 460 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 461 |
-
"severity": "high",
|
| 462 |
-
"line": 15,
|
| 463 |
-
"evidence": ";(res as any)[DOM_ERROR_HANDLER] = (err: any) => {",
|
| 464 |
-
"suppressed": false,
|
| 465 |
-
"advisory": false,
|
| 466 |
-
"likelihood": "unmeasured",
|
| 467 |
-
"path": "hono/src/jsx/dom/components.ts"
|
| 468 |
-
},
|
| 469 |
-
{
|
| 470 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 471 |
-
"severity": "high",
|
| 472 |
-
"line": 25,
|
| 473 |
-
"evidence": "export const Suspense: FC<PropsWithChildren<{ fallback: any }>> = (({",
|
| 474 |
-
"suppressed": false,
|
| 475 |
-
"advisory": false,
|
| 476 |
-
"likelihood": "unmeasured",
|
| 477 |
-
"path": "hono/src/jsx/dom/components.ts"
|
| 478 |
-
},
|
| 479 |
-
{
|
| 480 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 481 |
-
"severity": "high",
|
| 482 |
-
"line": 28,
|
| 483 |
-
"evidence": "}: any) => {",
|
| 484 |
-
"suppressed": false,
|
| 485 |
-
"advisory": false,
|
| 486 |
-
"likelihood": "unmeasured",
|
| 487 |
-
"path": "hono/src/jsx/dom/components.ts"
|
| 488 |
-
},
|
| 489 |
-
{
|
| 490 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 491 |
-
"severity": "high",
|
| 492 |
-
"line": 30,
|
| 493 |
-
"evidence": ";(res as any)[DOM_ERROR_HANDLER] = (err: any, retry: () => void) => {",
|
| 494 |
-
"suppressed": false,
|
| 495 |
-
"advisory": false,
|
| 496 |
-
"likelihood": "unmeasured",
|
| 497 |
-
"path": "hono/src/jsx/dom/components.ts"
|
| 498 |
-
},
|
| 499 |
-
{
|
| 500 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 501 |
-
"severity": "high",
|
| 502 |
-
"line": 15,
|
| 503 |
-
"evidence": "const props: { children: any } = {",
|
| 504 |
-
"suppressed": false,
|
| 505 |
-
"advisory": false,
|
| 506 |
-
"likelihood": "unmeasured",
|
| 507 |
-
"path": "hono/src/jsx/dom/context.ts"
|
| 508 |
-
},
|
| 509 |
-
{
|
| 510 |
-
"rule": "typescript.types-are-erased-at-runtime",
|
| 511 |
-
"severity": "critical",
|
| 512 |
-
"line": 136,
|
| 513 |
-
"evidence": "(c) => (c as unknown as CssClassName)[STYLE_STRING]",
|
| 514 |
-
"suppressed": false,
|
| 515 |
-
"advisory": false,
|
| 516 |
-
"likelihood": "unmeasured",
|
| 517 |
-
"path": "hono/src/jsx/dom/css.ts"
|
| 518 |
-
},
|
| 519 |
-
{
|
| 520 |
-
"rule": "typescript.types-are-erased-at-runtime",
|
| 521 |
-
"severity": "critical",
|
| 522 |
-
"line": 193,
|
| 523 |
-
"evidence": "return cssClassName as unknown as string",
|
| 524 |
-
"suppressed": false,
|
| 525 |
-
"advisory": false,
|
| 526 |
-
"likelihood": "unmeasured",
|
| 527 |
-
"path": "hono/src/jsx/dom/css.ts"
|
| 528 |
-
},
|
| 529 |
-
{
|
| 530 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 531 |
-
"severity": "high",
|
| 532 |
-
"line": 32,
|
| 533 |
-
"evidence": "export type ErrorHandler = (error: any, retry: () => void) => Child | undefined",
|
| 534 |
-
"suppressed": false,
|
| 535 |
-
"advisory": false,
|
| 536 |
-
"likelihood": "unmeasured",
|
| 537 |
-
"path": "hono/src/jsx/dom/render.ts"
|
| 538 |
-
},
|
| 539 |
-
{
|
| 540 |
-
"rule": "typescript.types-are-erased-at-runtime",
|
| 541 |
-
"severity": "critical",
|
| 542 |
-
"line": 216,
|
| 543 |
-
"evidence": ";(container as unknown as HTMLInputElement).value = normalizeFormValue(value)",
|
| 544 |
-
"suppressed": false,
|
| 545 |
-
"advisory": false,
|
| 546 |
-
"likelihood": "unmeasured",
|
| 547 |
-
"path": "hono/src/jsx/dom/render.ts"
|
| 548 |
-
},
|
| 549 |
-
{
|
| 550 |
-
"rule": "typescript.types-are-erased-at-runtime",
|
| 551 |
-
"severity": "critical",
|
| 552 |
-
"line": 612,
|
| 553 |
-
"evidence": "if ((oldChild as unknown as NodeString).t !== child.t) {",
|
| 554 |
-
"suppressed": false,
|
| 555 |
-
"advisory": false,
|
| 556 |
-
"likelihood": "unmeasured",
|
| 557 |
-
"path": "hono/src/jsx/dom/render.ts"
|
| 558 |
-
},
|
| 559 |
-
{
|
| 560 |
-
"rule": "typescript.types-are-erased-at-runtime",
|
| 561 |
-
"severity": "critical",
|
| 562 |
-
"line": 613,
|
| 563 |
-
"evidence": ";(oldChild as unknown as NodeString).t = child.t // update text content",
|
| 564 |
-
"suppressed": false,
|
| 565 |
-
"advisory": false,
|
| 566 |
-
"likelihood": "unmeasured",
|
| 567 |
-
"path": "hono/src/jsx/dom/render.ts"
|
| 568 |
-
},
|
| 569 |
-
{
|
| 570 |
-
"rule": "typescript.types-are-erased-at-runtime",
|
| 571 |
-
"severity": "critical",
|
| 572 |
-
"line": 614,
|
| 573 |
-
"evidence": ";(oldChild as unknown as NodeString).d = true",
|
| 574 |
-
"suppressed": false,
|
| 575 |
-
"advisory": false,
|
| 576 |
-
"likelihood": "unmeasured",
|
| 577 |
-
"path": "hono/src/jsx/dom/render.ts"
|
| 578 |
-
},
|
| 579 |
-
{
|
| 580 |
-
"rule": "typescript.types-are-erased-at-runtime",
|
| 581 |
-
"severity": "critical",
|
| 582 |
-
"line": 244,
|
| 583 |
-
"evidence": "} as unknown as JSXNode & { e?: HTMLElement; p?: PreserveNodeType }",
|
| 584 |
-
"suppressed": false,
|
| 585 |
-
"advisory": false,
|
| 586 |
-
"likelihood": "unmeasured",
|
| 587 |
-
"path": "hono/src/jsx/dom/intrinsic-element/components.ts"
|
| 588 |
-
},
|
| 589 |
-
{
|
| 590 |
-
"rule": "typescript.types-are-erased-at-runtime",
|
| 591 |
-
"severity": "critical",
|
| 592 |
-
"line": 267,
|
| 593 |
-
"evidence": "} as unknown as JSXNode",
|
| 594 |
-
"suppressed": false,
|
| 595 |
-
"advisory": false,
|
| 596 |
-
"likelihood": "unmeasured",
|
| 597 |
-
"path": "hono/src/jsx/dom/intrinsic-element/components.ts"
|
| 598 |
-
},
|
| 599 |
-
{
|
| 600 |
-
"rule": "typescript.types-are-erased-at-runtime",
|
| 601 |
-
"severity": "critical",
|
| 602 |
-
"line": 279,
|
| 603 |
-
"evidence": "} as unknown as JSXNode",
|
| 604 |
-
"suppressed": false,
|
| 605 |
-
"advisory": false,
|
| 606 |
-
"likelihood": "unmeasured",
|
| 607 |
-
"path": "hono/src/jsx/dom/intrinsic-element/components.ts"
|
| 608 |
-
},
|
| 609 |
-
{
|
| 610 |
-
"rule": "typescript.types-are-erased-at-runtime",
|
| 611 |
-
"severity": "critical",
|
| 612 |
-
"line": 291,
|
| 613 |
-
"evidence": "} as unknown as JSXNode",
|
| 614 |
-
"suppressed": false,
|
| 615 |
-
"advisory": false,
|
| 616 |
-
"likelihood": "unmeasured",
|
| 617 |
-
"path": "hono/src/jsx/dom/intrinsic-element/components.ts"
|
| 618 |
-
},
|
| 619 |
-
{
|
| 620 |
-
"rule": "typescript.types-are-erased-at-runtime",
|
| 621 |
-
"severity": "critical",
|
| 622 |
-
"line": 309,
|
| 623 |
-
"evidence": "} as unknown as JSXNode",
|
| 624 |
-
"suppressed": false,
|
| 625 |
-
"advisory": false,
|
| 626 |
-
"likelihood": "unmeasured",
|
| 627 |
-
"path": "hono/src/jsx/dom/intrinsic-element/components.ts"
|
| 628 |
-
},
|
| 629 |
-
{
|
| 630 |
-
"rule": "typescript.types-are-erased-at-runtime",
|
| 631 |
-
"severity": "critical",
|
| 632 |
-
"line": 365,
|
| 633 |
-
"evidence": "tag: FormContext as unknown as Function,",
|
| 634 |
-
"suppressed": false,
|
| 635 |
-
"advisory": false,
|
| 636 |
-
"likelihood": "unmeasured",
|
| 637 |
-
"path": "hono/src/jsx/dom/intrinsic-element/components.ts"
|
| 638 |
-
},
|
| 639 |
-
{
|
| 640 |
-
"rule": "typescript.types-are-erased-at-runtime",
|
| 641 |
-
"severity": "critical",
|
| 642 |
-
"line": 130,
|
| 643 |
-
"evidence": ") as unknown as HtmlEscapedString",
|
| 644 |
-
"suppressed": false,
|
| 645 |
-
"advisory": false,
|
| 646 |
-
"likelihood": "unmeasured",
|
| 647 |
-
"path": "hono/src/jsx/intrinsic-element/components.ts"
|
| 648 |
-
},
|
| 649 |
-
{
|
| 650 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 651 |
-
"severity": "high",
|
| 652 |
-
"line": 49,
|
| 653 |
-
"evidence": "(props: any) => component(props, c),",
|
| 654 |
-
"suppressed": false,
|
| 655 |
-
"advisory": false,
|
| 656 |
-
"likelihood": "unmeasured",
|
| 657 |
-
"path": "hono/src/middleware/jsx-renderer/index.ts"
|
| 658 |
-
},
|
| 659 |
-
{
|
| 660 |
-
"rule": "typescript.types-are-erased-at-runtime",
|
| 661 |
-
"severity": "critical",
|
| 662 |
-
"line": 281,
|
| 663 |
-
"evidence": "const clone = values[1].slice() as unknown as string[]",
|
| 664 |
-
"suppressed": false,
|
| 665 |
-
"advisory": false,
|
| 666 |
-
"likelihood": "unmeasured",
|
| 667 |
-
"path": "hono/src/middleware/secure-headers/secure-headers.ts"
|
| 668 |
-
},
|
| 669 |
-
{
|
| 670 |
-
"rule": "typescript.types-are-erased-at-runtime",
|
| 671 |
-
"severity": "critical",
|
| 672 |
-
"line": 47,
|
| 673 |
-
"evidence": "}, duration) as unknown as number",
|
| 674 |
-
"suppressed": false,
|
| 675 |
-
"advisory": false,
|
| 676 |
-
"likelihood": "unmeasured",
|
| 677 |
-
"path": "hono/src/middleware/timeout/index.ts"
|
| 678 |
-
},
|
| 679 |
-
{
|
| 680 |
-
"rule": "typescript.types-are-erased-at-runtime",
|
| 681 |
-
"severity": "critical",
|
| 682 |
-
"line": 147,
|
| 683 |
-
"evidence": "all[1][i] = all[1][i] ? [] : (0 as unknown as HandlerData<string>)",
|
| 684 |
-
"suppressed": false,
|
| 685 |
-
"advisory": false,
|
| 686 |
-
"likelihood": "unmeasured",
|
| 687 |
-
"path": "hono/src/router/reg-exp-router/prepared-router.ts"
|
| 688 |
-
},
|
| 689 |
-
{
|
| 690 |
-
"rule": "typescript.types-are-erased-at-runtime",
|
| 691 |
-
"severity": "critical",
|
| 692 |
-
"line": 141,
|
| 693 |
-
"evidence": "request.bodyCache.formData = formDataPromise as unknown as FormData",
|
| 694 |
-
"suppressed": false,
|
| 695 |
-
"advisory": false,
|
| 696 |
-
"likelihood": "unmeasured",
|
| 697 |
-
"path": "hono/src/utils/body.ts"
|
| 698 |
-
},
|
| 699 |
-
{
|
| 700 |
-
"rule": "typescript.types-are-erased-at-runtime",
|
| 701 |
-
"severity": "critical",
|
| 702 |
-
"line": 256,
|
| 703 |
-
"evidence": "nestedForm = nestedForm[key] as unknown as BodyData",
|
| 704 |
-
"suppressed": false,
|
| 705 |
-
"advisory": false,
|
| 706 |
-
"likelihood": "unmeasured",
|
| 707 |
-
"path": "hono/src/utils/body.ts"
|
| 708 |
-
},
|
| 709 |
-
{
|
| 710 |
-
"rule": "typescript.types-are-erased-at-runtime",
|
| 711 |
-
"severity": "critical",
|
| 712 |
-
"line": 151,
|
| 713 |
-
"evidence": "str = (str as unknown as string).toString() // HtmlEscapedString object to string",
|
| 714 |
-
"suppressed": false,
|
| 715 |
-
"advisory": false,
|
| 716 |
-
"likelihood": "unmeasured",
|
| 717 |
-
"path": "hono/src/utils/html.ts"
|
| 718 |
-
},
|
| 719 |
-
{
|
| 720 |
-
"rule": "typescript.types-are-erased-at-runtime",
|
| 721 |
-
"severity": "critical",
|
| 722 |
-
"line": 154,
|
| 723 |
-
"evidence": "str = await (str as unknown as Promise<string>)",
|
| 724 |
-
"suppressed": false,
|
| 725 |
-
"advisory": false,
|
| 726 |
-
"likelihood": "unmeasured",
|
| 727 |
-
"path": "hono/src/utils/html.ts"
|
| 728 |
-
},
|
| 729 |
-
{
|
| 730 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 731 |
-
"severity": "high",
|
| 732 |
-
"line": 28,
|
| 733 |
-
"evidence": "export type ExtractValidationResponse<VF> = VF extends (value: any, c: any) => infer R",
|
| 734 |
-
"suppressed": false,
|
| 735 |
-
"advisory": false,
|
| 736 |
-
"likelihood": "unmeasured",
|
| 737 |
-
"path": "hono/src/validator/validator.ts"
|
| 738 |
-
},
|
| 739 |
-
{
|
| 740 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 741 |
-
"severity": "high",
|
| 742 |
-
"line": 175,
|
| 743 |
-
"evidence": "export type ExtractValidatorOutput<VF> = VF extends (value: any, c: any) => infer R",
|
| 744 |
-
"suppressed": false,
|
| 745 |
-
"advisory": false,
|
| 746 |
-
"likelihood": "unmeasured",
|
| 747 |
-
"path": "hono/src/validator/validator.ts"
|
| 748 |
-
},
|
| 749 |
-
{
|
| 750 |
-
"rule": "typescript.validate-env-at-startup",
|
| 751 |
-
"severity": "high",
|
| 752 |
-
"line": 3,
|
| 753 |
-
"evidence": "const baseUrl = process.env.PLAYWRIGHT_TEST_BASE_URL || 'http://localhost:3000';",
|
| 754 |
-
"suppressed": false,
|
| 755 |
-
"advisory": false,
|
| 756 |
-
"likelihood": "unmeasured",
|
| 757 |
-
"path": "trpc/examples/.experimental/next-app-dir/playwright.config.ts"
|
| 758 |
-
},
|
| 759 |
-
{
|
| 760 |
-
"rule": "typescript.validate-env-at-startup",
|
| 761 |
-
"severity": "high",
|
| 762 |
-
"line": 8,
|
| 763 |
-
"evidence": "headless: !!process.env.CI || !!process.env.PLAYWRIGHT_HEADLESS,",
|
| 764 |
-
"suppressed": false,
|
| 765 |
-
"advisory": false,
|
| 766 |
-
"likelihood": "unmeasured",
|
| 767 |
-
"path": "trpc/examples/.experimental/next-app-dir/playwright.config.ts"
|
| 768 |
-
},
|
| 769 |
-
{
|
| 770 |
-
"rule": "typescript.validate-env-at-startup",
|
| 771 |
-
"severity": "high",
|
| 772 |
-
"line": 17,
|
| 773 |
-
"evidence": "channel: process.env.CI ? 'chrome' : undefined,",
|
| 774 |
-
"suppressed": false,
|
| 775 |
-
"advisory": false,
|
| 776 |
-
"likelihood": "unmeasured",
|
| 777 |
-
"path": "trpc/examples/.experimental/next-app-dir/playwright.config.ts"
|
| 778 |
-
},
|
| 779 |
-
{
|
| 780 |
-
"rule": "typescript.validate-env-at-startup",
|
| 781 |
-
"severity": "high",
|
| 782 |
-
"line": 19,
|
| 783 |
-
"evidence": "retries: process.env.CI ? 3 : 0,",
|
| 784 |
-
"suppressed": false,
|
| 785 |
-
"advisory": false,
|
| 786 |
-
"likelihood": "unmeasured",
|
| 787 |
-
"path": "trpc/examples/.experimental/next-app-dir/playwright.config.ts"
|
| 788 |
-
},
|
| 789 |
-
{
|
| 790 |
-
"rule": "typescript.validate-env-at-startup",
|
| 791 |
-
"severity": "high",
|
| 792 |
-
"line": 18,
|
| 793 |
-
"evidence": "clientId: process.env.GITHUB_ID!,",
|
| 794 |
-
"suppressed": false,
|
| 795 |
-
"advisory": false,
|
| 796 |
-
"likelihood": "unmeasured",
|
| 797 |
-
"path": "trpc/examples/.experimental/next-app-dir/src/auth.ts"
|
| 798 |
-
},
|
| 799 |
-
{
|
| 800 |
-
"rule": "typescript.validate-env-at-startup",
|
| 801 |
-
"severity": "high",
|
| 802 |
-
"line": 20,
|
| 803 |
-
"evidence": "clientSecret: process.env.GITHUB_SECRET!,",
|
| 804 |
-
"suppressed": false,
|
| 805 |
-
"advisory": false,
|
| 806 |
-
"likelihood": "unmeasured",
|
| 807 |
-
"path": "trpc/examples/.experimental/next-app-dir/src/auth.ts"
|
| 808 |
-
},
|
| 809 |
-
{
|
| 810 |
-
"rule": "typescript.validate-env-at-startup",
|
| 811 |
-
"severity": "high",
|
| 812 |
-
"line": 9,
|
| 813 |
-
"evidence": "if (process.env.NODE_ENV === 'development') {",
|
| 814 |
-
"suppressed": false,
|
| 815 |
-
"advisory": false,
|
| 816 |
-
"likelihood": "unmeasured",
|
| 817 |
-
"path": "trpc/examples/.experimental/next-app-dir/src/app/posts/layout.tsx"
|
| 818 |
-
},
|
| 819 |
-
{
|
| 820 |
-
"rule": "typescript.validate-env-at-startup",
|
| 821 |
-
"severity": "high",
|
| 822 |
-
"line": 39,
|
| 823 |
-
"evidence": "export const db = process.env.POSTGRES_URL ? pgDb() : mockDb();",
|
| 824 |
-
"suppressed": false,
|
| 825 |
-
"advisory": false,
|
| 826 |
-
"likelihood": "unmeasured",
|
| 827 |
-
"path": "trpc/examples/.experimental/next-app-dir/src/app/posts/_lib/db.ts"
|
| 828 |
-
},
|
| 829 |
-
{
|
| 830 |
-
"rule": "typescript.validate-env-at-startup",
|
| 831 |
-
"severity": "high",
|
| 832 |
-
"line": 37,
|
| 833 |
-
"evidence": "process.env.NODE_ENV === 'development' ||",
|
| 834 |
-
"suppressed": false,
|
| 835 |
-
"advisory": false,
|
| 836 |
-
"likelihood": "unmeasured",
|
| 837 |
-
"path": "trpc/examples/.experimental/next-app-dir/src/trpc/rq-client.tsx"
|
| 838 |
-
},
|
| 839 |
-
{
|
| 840 |
-
"rule": "typescript.validate-env-at-startup",
|
| 841 |
-
"severity": "high",
|
| 842 |
-
"line": 10,
|
| 843 |
-
"evidence": "if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;",
|
| 844 |
-
"suppressed": false,
|
| 845 |
-
"advisory": false,
|
| 846 |
-
"likelihood": "unmeasured",
|
| 847 |
-
"path": "trpc/examples/.experimental/next-app-dir/src/trpc/shared.ts"
|
| 848 |
-
},
|
| 849 |
-
{
|
| 850 |
-
"rule": "typescript.validate-env-at-startup",
|
| 851 |
-
"severity": "high",
|
| 852 |
-
"line": 5,
|
| 853 |
-
"evidence": "eslint: { ignoreDuringBuilds: !!process.env.CI },",
|
| 854 |
-
"suppressed": false,
|
| 855 |
-
"advisory": false,
|
| 856 |
-
"likelihood": "unmeasured",
|
| 857 |
-
"path": "trpc/examples/.test/diagnostics-big-router/next.config.ts"
|
| 858 |
-
},
|
| 859 |
-
{
|
| 860 |
-
"rule": "typescript.validate-env-at-startup",
|
| 861 |
-
"severity": "high",
|
| 862 |
-
"line": 15,
|
| 863 |
-
"evidence": "if (process.env.VERCEL_URL) {",
|
| 864 |
-
"suppressed": false,
|
| 865 |
-
"advisory": false,
|
| 866 |
-
"likelihood": "unmeasured",
|
| 867 |
-
"path": "trpc/examples/.test/diagnostics-big-router/src/utils/trpc.ts"
|
| 868 |
-
},
|
| 869 |
-
{
|
| 870 |
-
"rule": "typescript.validate-env-at-startup",
|
| 871 |
-
"severity": "high",
|
| 872 |
-
"line": 16,
|
| 873 |
-
"evidence": "return `https://${process.env.VERCEL_URL}`;",
|
| 874 |
-
"suppressed": false,
|
| 875 |
-
"advisory": false,
|
| 876 |
-
"likelihood": "unmeasured",
|
| 877 |
-
"path": "trpc/examples/.test/diagnostics-big-router/src/utils/trpc.ts"
|
| 878 |
-
},
|
| 879 |
-
{
|
| 880 |
-
"rule": "typescript.validate-env-at-startup",
|
| 881 |
-
"severity": "high",
|
| 882 |
-
"line": 20,
|
| 883 |
-
"evidence": "return `http://localhost:${process.env.PORT ?? 3000}`;",
|
| 884 |
-
"suppressed": false,
|
| 885 |
-
"advisory": false,
|
| 886 |
-
"likelihood": "unmeasured",
|
| 887 |
-
"path": "trpc/examples/.test/diagnostics-big-router/src/utils/trpc.ts"
|
| 888 |
-
},
|
| 889 |
-
{
|
| 890 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 891 |
-
"severity": "high",
|
| 892 |
-
"line": 19,
|
| 893 |
-
"evidence": "export function genericRouter<TSchema extends (value: any) => unknown>(",
|
| 894 |
-
"suppressed": false,
|
| 895 |
-
"advisory": false,
|
| 896 |
-
"likelihood": "unmeasured",
|
| 897 |
-
"path": "trpc/examples/.test/internal-types-export/src/server.ts"
|
| 898 |
-
},
|
| 899 |
-
{
|
| 900 |
-
"rule": "typescript.validate-env-at-startup",
|
| 901 |
-
"severity": "high",
|
| 902 |
-
"line": 5,
|
| 903 |
-
"evidence": "eslint: { ignoreDuringBuilds: !!process.env.CI },",
|
| 904 |
-
"suppressed": false,
|
| 905 |
-
"advisory": false,
|
| 906 |
-
"likelihood": "unmeasured",
|
| 907 |
-
"path": "trpc/examples/.test/ssg-infinite-serialization/next.config.ts"
|
| 908 |
-
},
|
| 909 |
-
{
|
| 910 |
-
"rule": "typescript.validate-env-at-startup",
|
| 911 |
-
"severity": "high",
|
| 912 |
-
"line": 3,
|
| 913 |
-
"evidence": "const baseUrl = process.env.PLAYWRIGHT_TEST_BASE_URL || 'http://localhost:3000';",
|
| 914 |
-
"suppressed": false,
|
| 915 |
-
"advisory": false,
|
| 916 |
-
"likelihood": "unmeasured",
|
| 917 |
-
"path": "trpc/examples/.test/ssg-infinite-serialization/playwright.config.ts"
|
| 918 |
-
},
|
| 919 |
-
{
|
| 920 |
-
"rule": "typescript.validate-env-at-startup",
|
| 921 |
-
"severity": "high",
|
| 922 |
-
"line": 8,
|
| 923 |
-
"evidence": "headless: !!process.env.CI || !!process.env.PLAYWRIGHT_HEADLESS,",
|
| 924 |
-
"suppressed": false,
|
| 925 |
-
"advisory": false,
|
| 926 |
-
"likelihood": "unmeasured",
|
| 927 |
-
"path": "trpc/examples/.test/ssg-infinite-serialization/playwright.config.ts"
|
| 928 |
-
},
|
| 929 |
-
{
|
| 930 |
-
"rule": "typescript.validate-env-at-startup",
|
| 931 |
-
"severity": "high",
|
| 932 |
-
"line": 10,
|
| 933 |
-
"evidence": "if (process.env.VERCEL_URL) {",
|
| 934 |
-
"suppressed": false,
|
| 935 |
-
"advisory": false,
|
| 936 |
-
"likelihood": "unmeasured",
|
| 937 |
-
"path": "trpc/examples/.test/ssg-infinite-serialization/src/utils/trpc.ts"
|
| 938 |
-
},
|
| 939 |
-
{
|
| 940 |
-
"rule": "typescript.validate-env-at-startup",
|
| 941 |
-
"severity": "high",
|
| 942 |
-
"line": 11,
|
| 943 |
-
"evidence": "return `https://${process.env.VERCEL_URL}`;",
|
| 944 |
-
"suppressed": false,
|
| 945 |
-
"advisory": false,
|
| 946 |
-
"likelihood": "unmeasured",
|
| 947 |
-
"path": "trpc/examples/.test/ssg-infinite-serialization/src/utils/trpc.ts"
|
| 948 |
-
},
|
| 949 |
-
{
|
| 950 |
-
"rule": "typescript.validate-env-at-startup",
|
| 951 |
-
"severity": "high",
|
| 952 |
-
"line": 13,
|
| 953 |
-
"evidence": "return `http://localhost:${process.env.PORT ?? 3000}`;",
|
| 954 |
-
"suppressed": false,
|
| 955 |
-
"advisory": false,
|
| 956 |
-
"likelihood": "unmeasured",
|
| 957 |
-
"path": "trpc/examples/.test/ssg-infinite-serialization/src/utils/trpc.ts"
|
| 958 |
-
},
|
| 959 |
-
{
|
| 960 |
-
"rule": "typescript.validate-env-at-startup",
|
| 961 |
-
"severity": "high",
|
| 962 |
-
"line": 5,
|
| 963 |
-
"evidence": "eslint: { ignoreDuringBuilds: !!process.env.CI },",
|
| 964 |
-
"suppressed": false,
|
| 965 |
-
"advisory": false,
|
| 966 |
-
"likelihood": "unmeasured",
|
| 967 |
-
"path": "trpc/examples/.test/ssg/next.config.ts"
|
| 968 |
-
},
|
| 969 |
-
{
|
| 970 |
-
"rule": "typescript.validate-env-at-startup",
|
| 971 |
-
"severity": "high",
|
| 972 |
-
"line": 3,
|
| 973 |
-
"evidence": "const baseUrl = process.env.PLAYWRIGHT_TEST_BASE_URL || 'http://localhost:3000';",
|
| 974 |
-
"suppressed": false,
|
| 975 |
-
"advisory": false,
|
| 976 |
-
"likelihood": "unmeasured",
|
| 977 |
-
"path": "trpc/examples/.test/ssg/playwright.config.ts"
|
| 978 |
-
},
|
| 979 |
-
{
|
| 980 |
-
"rule": "typescript.validate-env-at-startup",
|
| 981 |
-
"severity": "high",
|
| 982 |
-
"line": 8,
|
| 983 |
-
"evidence": "headless: !!process.env.CI || !!process.env.PLAYWRIGHT_HEADLESS,",
|
| 984 |
-
"suppressed": false,
|
| 985 |
-
"advisory": false,
|
| 986 |
-
"likelihood": "unmeasured",
|
| 987 |
-
"path": "trpc/examples/.test/ssg/playwright.config.ts"
|
| 988 |
-
},
|
| 989 |
-
{
|
| 990 |
-
"rule": "typescript.validate-env-at-startup",
|
| 991 |
-
"severity": "high",
|
| 992 |
-
"line": 17,
|
| 993 |
-
"evidence": "channel: process.env.CI ? 'chrome' : undefined,",
|
| 994 |
-
"suppressed": false,
|
| 995 |
-
"advisory": false,
|
| 996 |
-
"likelihood": "unmeasured",
|
| 997 |
-
"path": "trpc/examples/.test/ssg/playwright.config.ts"
|
| 998 |
-
},
|
| 999 |
-
{
|
| 1000 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1001 |
-
"severity": "high",
|
| 1002 |
-
"line": 20,
|
| 1003 |
-
"evidence": "retries: process.env.CI ? 3 : 0,",
|
| 1004 |
-
"suppressed": false,
|
| 1005 |
-
"advisory": false,
|
| 1006 |
-
"likelihood": "unmeasured",
|
| 1007 |
-
"path": "trpc/examples/.test/ssg/playwright.config.ts"
|
| 1008 |
-
},
|
| 1009 |
-
{
|
| 1010 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1011 |
-
"severity": "high",
|
| 1012 |
-
"line": 10,
|
| 1013 |
-
"evidence": "if (process.env.VERCEL_URL) {",
|
| 1014 |
-
"suppressed": false,
|
| 1015 |
-
"advisory": false,
|
| 1016 |
-
"likelihood": "unmeasured",
|
| 1017 |
-
"path": "trpc/examples/.test/ssg/src/utils/trpc.ts"
|
| 1018 |
-
},
|
| 1019 |
-
{
|
| 1020 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1021 |
-
"severity": "high",
|
| 1022 |
-
"line": 11,
|
| 1023 |
-
"evidence": "return `https://${process.env.VERCEL_URL}`;",
|
| 1024 |
-
"suppressed": false,
|
| 1025 |
-
"advisory": false,
|
| 1026 |
-
"likelihood": "unmeasured",
|
| 1027 |
-
"path": "trpc/examples/.test/ssg/src/utils/trpc.ts"
|
| 1028 |
-
},
|
| 1029 |
-
{
|
| 1030 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1031 |
-
"severity": "high",
|
| 1032 |
-
"line": 13,
|
| 1033 |
-
"evidence": "return `http://localhost:${process.env.PORT ?? 3000}`;",
|
| 1034 |
-
"suppressed": false,
|
| 1035 |
-
"advisory": false,
|
| 1036 |
-
"likelihood": "unmeasured",
|
| 1037 |
-
"path": "trpc/examples/.test/ssg/src/utils/trpc.ts"
|
| 1038 |
-
},
|
| 1039 |
-
{
|
| 1040 |
-
"rule": "typescript.derive-type-from-schema",
|
| 1041 |
-
"severity": "high",
|
| 1042 |
-
"line": 5,
|
| 1043 |
-
"evidence": "interface Post {",
|
| 1044 |
-
"suppressed": false,
|
| 1045 |
-
"advisory": false,
|
| 1046 |
-
"likelihood": "unmeasured",
|
| 1047 |
-
"path": "trpc/examples/fastify-server/src/server/router/routers/posts.ts"
|
| 1048 |
-
},
|
| 1049 |
-
{
|
| 1050 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1051 |
-
"severity": "high",
|
| 1052 |
-
"line": 3,
|
| 1053 |
-
"evidence": "const baseUrl = process.env.PLAYWRIGHT_TEST_BASE_URL || 'http://localhost:3000';",
|
| 1054 |
-
"suppressed": false,
|
| 1055 |
-
"advisory": false,
|
| 1056 |
-
"likelihood": "unmeasured",
|
| 1057 |
-
"path": "trpc/examples/minimal-content-types/playwright.config.ts"
|
| 1058 |
-
},
|
| 1059 |
-
{
|
| 1060 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1061 |
-
"severity": "high",
|
| 1062 |
-
"line": 8,
|
| 1063 |
-
"evidence": "headless: !!process.env.CI || !!process.env.PLAYWRIGHT_HEADLESS,",
|
| 1064 |
-
"suppressed": false,
|
| 1065 |
-
"advisory": false,
|
| 1066 |
-
"likelihood": "unmeasured",
|
| 1067 |
-
"path": "trpc/examples/minimal-content-types/playwright.config.ts"
|
| 1068 |
-
},
|
| 1069 |
-
{
|
| 1070 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1071 |
-
"severity": "high",
|
| 1072 |
-
"line": 18,
|
| 1073 |
-
"evidence": "retries: process.env.CI ? 3 : 0,",
|
| 1074 |
-
"suppressed": false,
|
| 1075 |
-
"advisory": false,
|
| 1076 |
-
"likelihood": "unmeasured",
|
| 1077 |
-
"path": "trpc/examples/minimal-content-types/playwright.config.ts"
|
| 1078 |
-
},
|
| 1079 |
-
{
|
| 1080 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1081 |
-
"severity": "high",
|
| 1082 |
-
"line": 4,
|
| 1083 |
-
"evidence": "const baseUrl = process.env.PLAYWRIGHT_TEST_BASE_URL || 'http://localhost:3000';",
|
| 1084 |
-
"suppressed": false,
|
| 1085 |
-
"advisory": false,
|
| 1086 |
-
"likelihood": "unmeasured",
|
| 1087 |
-
"path": "trpc/examples/minimal-react/playwright.config.ts"
|
| 1088 |
-
},
|
| 1089 |
-
{
|
| 1090 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1091 |
-
"severity": "high",
|
| 1092 |
-
"line": 9,
|
| 1093 |
-
"evidence": "headless: !!process.env.CI || !!process.env.PLAYWRIGHT_HEADLESS,",
|
| 1094 |
-
"suppressed": false,
|
| 1095 |
-
"advisory": false,
|
| 1096 |
-
"likelihood": "unmeasured",
|
| 1097 |
-
"path": "trpc/examples/minimal-react/playwright.config.ts"
|
| 1098 |
-
},
|
| 1099 |
-
{
|
| 1100 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1101 |
-
"severity": "high",
|
| 1102 |
-
"line": 18,
|
| 1103 |
-
"evidence": "channel: process.env.CI ? 'chrome' : undefined,",
|
| 1104 |
-
"suppressed": false,
|
| 1105 |
-
"advisory": false,
|
| 1106 |
-
"likelihood": "unmeasured",
|
| 1107 |
-
"path": "trpc/examples/minimal-react/playwright.config.ts"
|
| 1108 |
-
},
|
| 1109 |
-
{
|
| 1110 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1111 |
-
"severity": "high",
|
| 1112 |
-
"line": 5,
|
| 1113 |
-
"evidence": "eslint: { ignoreDuringBuilds: !!process.env.CI },",
|
| 1114 |
-
"suppressed": false,
|
| 1115 |
-
"advisory": false,
|
| 1116 |
-
"likelihood": "unmeasured",
|
| 1117 |
-
"path": "trpc/examples/next-big-router/next.config.ts"
|
| 1118 |
-
},
|
| 1119 |
-
{
|
| 1120 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1121 |
-
"severity": "high",
|
| 1122 |
-
"line": 13,
|
| 1123 |
-
"evidence": "if (process.env.VERCEL_URL) {",
|
| 1124 |
-
"suppressed": false,
|
| 1125 |
-
"advisory": false,
|
| 1126 |
-
"likelihood": "unmeasured",
|
| 1127 |
-
"path": "trpc/examples/next-big-router/src/utils/trpc.ts"
|
| 1128 |
-
},
|
| 1129 |
-
{
|
| 1130 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1131 |
-
"severity": "high",
|
| 1132 |
-
"line": 14,
|
| 1133 |
-
"evidence": "return `https://${process.env.VERCEL_URL}`;",
|
| 1134 |
-
"suppressed": false,
|
| 1135 |
-
"advisory": false,
|
| 1136 |
-
"likelihood": "unmeasured",
|
| 1137 |
-
"path": "trpc/examples/next-big-router/src/utils/trpc.ts"
|
| 1138 |
-
},
|
| 1139 |
-
{
|
| 1140 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1141 |
-
"severity": "high",
|
| 1142 |
-
"line": 18,
|
| 1143 |
-
"evidence": "return `http://localhost:${process.env.PORT ?? 3000}`;",
|
| 1144 |
-
"suppressed": false,
|
| 1145 |
-
"advisory": false,
|
| 1146 |
-
"likelihood": "unmeasured",
|
| 1147 |
-
"path": "trpc/examples/next-big-router/src/utils/trpc.ts"
|
| 1148 |
-
},
|
| 1149 |
-
{
|
| 1150 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1151 |
-
"severity": "high",
|
| 1152 |
-
"line": 5,
|
| 1153 |
-
"evidence": "eslint: { ignoreDuringBuilds: !!process.env.CI },",
|
| 1154 |
-
"suppressed": false,
|
| 1155 |
-
"advisory": false,
|
| 1156 |
-
"likelihood": "unmeasured",
|
| 1157 |
-
"path": "trpc/examples/next-edge-runtime/next.config.ts"
|
| 1158 |
-
},
|
| 1159 |
-
{
|
| 1160 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1161 |
-
"severity": "high",
|
| 1162 |
-
"line": 13,
|
| 1163 |
-
"evidence": "if (process.env.VERCEL_URL) {",
|
| 1164 |
-
"suppressed": false,
|
| 1165 |
-
"advisory": false,
|
| 1166 |
-
"likelihood": "unmeasured",
|
| 1167 |
-
"path": "trpc/examples/next-edge-runtime/src/utils/trpc.ts"
|
| 1168 |
-
},
|
| 1169 |
-
{
|
| 1170 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1171 |
-
"severity": "high",
|
| 1172 |
-
"line": 14,
|
| 1173 |
-
"evidence": "return `https://${process.env.VERCEL_URL}`;",
|
| 1174 |
-
"suppressed": false,
|
| 1175 |
-
"advisory": false,
|
| 1176 |
-
"likelihood": "unmeasured",
|
| 1177 |
-
"path": "trpc/examples/next-edge-runtime/src/utils/trpc.ts"
|
| 1178 |
-
},
|
| 1179 |
-
{
|
| 1180 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1181 |
-
"severity": "high",
|
| 1182 |
-
"line": 18,
|
| 1183 |
-
"evidence": "return `http://localhost:${process.env.PORT ?? 3000}`;",
|
| 1184 |
-
"suppressed": false,
|
| 1185 |
-
"advisory": false,
|
| 1186 |
-
"likelihood": "unmeasured",
|
| 1187 |
-
"path": "trpc/examples/next-edge-runtime/src/utils/trpc.ts"
|
| 1188 |
-
},
|
| 1189 |
-
{
|
| 1190 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1191 |
-
"severity": "high",
|
| 1192 |
-
"line": 5,
|
| 1193 |
-
"evidence": "eslint: { ignoreDuringBuilds: !!process.env.CI },",
|
| 1194 |
-
"suppressed": false,
|
| 1195 |
-
"advisory": false,
|
| 1196 |
-
"likelihood": "unmeasured",
|
| 1197 |
-
"path": "trpc/examples/next-formdata/next.config.ts"
|
| 1198 |
-
},
|
| 1199 |
-
{
|
| 1200 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1201 |
-
"severity": "high",
|
| 1202 |
-
"line": 20,
|
| 1203 |
-
"evidence": "if (process.env.VERCEL_URL) {",
|
| 1204 |
-
"suppressed": false,
|
| 1205 |
-
"advisory": false,
|
| 1206 |
-
"likelihood": "unmeasured",
|
| 1207 |
-
"path": "trpc/examples/next-formdata/src/utils/trpc.ts"
|
| 1208 |
-
},
|
| 1209 |
-
{
|
| 1210 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1211 |
-
"severity": "high",
|
| 1212 |
-
"line": 21,
|
| 1213 |
-
"evidence": "return `https://${process.env.VERCEL_URL}`;",
|
| 1214 |
-
"suppressed": false,
|
| 1215 |
-
"advisory": false,
|
| 1216 |
-
"likelihood": "unmeasured",
|
| 1217 |
-
"path": "trpc/examples/next-formdata/src/utils/trpc.ts"
|
| 1218 |
-
},
|
| 1219 |
-
{
|
| 1220 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1221 |
-
"severity": "high",
|
| 1222 |
-
"line": 25,
|
| 1223 |
-
"evidence": "return `http://localhost:${process.env.PORT ?? 3000}`;",
|
| 1224 |
-
"suppressed": false,
|
| 1225 |
-
"advisory": false,
|
| 1226 |
-
"likelihood": "unmeasured",
|
| 1227 |
-
"path": "trpc/examples/next-formdata/src/utils/trpc.ts"
|
| 1228 |
-
},
|
| 1229 |
-
{
|
| 1230 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1231 |
-
"severity": "high",
|
| 1232 |
-
"line": 55,
|
| 1233 |
-
"evidence": "process.env.NODE_ENV === 'development' ||",
|
| 1234 |
-
"suppressed": false,
|
| 1235 |
-
"advisory": false,
|
| 1236 |
-
"likelihood": "unmeasured",
|
| 1237 |
-
"path": "trpc/examples/next-formdata/src/utils/trpc.ts"
|
| 1238 |
-
},
|
| 1239 |
-
{
|
| 1240 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1241 |
-
"severity": "high",
|
| 1242 |
-
"line": 5,
|
| 1243 |
-
"evidence": "eslint: { ignoreDuringBuilds: !!process.env.CI },",
|
| 1244 |
-
"suppressed": false,
|
| 1245 |
-
"advisory": false,
|
| 1246 |
-
"likelihood": "unmeasured",
|
| 1247 |
-
"path": "trpc/examples/next-minimal-starter/next.config.ts"
|
| 1248 |
-
},
|
| 1249 |
-
{
|
| 1250 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1251 |
-
"severity": "high",
|
| 1252 |
-
"line": 15,
|
| 1253 |
-
"evidence": "if (process.env.VERCEL_URL) {",
|
| 1254 |
-
"suppressed": false,
|
| 1255 |
-
"advisory": false,
|
| 1256 |
-
"likelihood": "unmeasured",
|
| 1257 |
-
"path": "trpc/examples/next-minimal-starter/src/utils/trpc.ts"
|
| 1258 |
-
},
|
| 1259 |
-
{
|
| 1260 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1261 |
-
"severity": "high",
|
| 1262 |
-
"line": 16,
|
| 1263 |
-
"evidence": "return `https://${process.env.VERCEL_URL}`;",
|
| 1264 |
-
"suppressed": false,
|
| 1265 |
-
"advisory": false,
|
| 1266 |
-
"likelihood": "unmeasured",
|
| 1267 |
-
"path": "trpc/examples/next-minimal-starter/src/utils/trpc.ts"
|
| 1268 |
-
},
|
| 1269 |
-
{
|
| 1270 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1271 |
-
"severity": "high",
|
| 1272 |
-
"line": 20,
|
| 1273 |
-
"evidence": "return `http://localhost:${process.env.PORT ?? 3000}`;",
|
| 1274 |
-
"suppressed": false,
|
| 1275 |
-
"advisory": false,
|
| 1276 |
-
"likelihood": "unmeasured",
|
| 1277 |
-
"path": "trpc/examples/next-minimal-starter/src/utils/trpc.ts"
|
| 1278 |
-
},
|
| 1279 |
-
{
|
| 1280 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1281 |
-
"severity": "high",
|
| 1282 |
-
"line": 5,
|
| 1283 |
-
"evidence": "headless: !!process.env.CI || !!process.env.PLAYWRIGHT_HEADLESS,",
|
| 1284 |
-
"suppressed": false,
|
| 1285 |
-
"advisory": false,
|
| 1286 |
-
"likelihood": "unmeasured",
|
| 1287 |
-
"path": "trpc/examples/next-prisma-starter/playwright.config.ts"
|
| 1288 |
-
},
|
| 1289 |
-
{
|
| 1290 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1291 |
-
"severity": "high",
|
| 1292 |
-
"line": 14,
|
| 1293 |
-
"evidence": "reporter: process.env.CI ? 'github' : 'list',",
|
| 1294 |
-
"suppressed": false,
|
| 1295 |
-
"advisory": false,
|
| 1296 |
-
"likelihood": "unmeasured",
|
| 1297 |
-
"path": "trpc/examples/next-prisma-starter/playwright.config.ts"
|
| 1298 |
-
},
|
| 1299 |
-
{
|
| 1300 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1301 |
-
"severity": "high",
|
| 1302 |
-
"line": 18,
|
| 1303 |
-
"evidence": "channel: process.env.CI ? 'chrome' : undefined,",
|
| 1304 |
-
"suppressed": false,
|
| 1305 |
-
"advisory": false,
|
| 1306 |
-
"likelihood": "unmeasured",
|
| 1307 |
-
"path": "trpc/examples/next-prisma-starter/playwright.config.ts"
|
| 1308 |
-
},
|
| 1309 |
-
{
|
| 1310 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1311 |
-
"severity": "high",
|
| 1312 |
-
"line": 19,
|
| 1313 |
-
"evidence": "video: process.env.CI ? 'off' : 'on',",
|
| 1314 |
-
"suppressed": false,
|
| 1315 |
-
"advisory": false,
|
| 1316 |
-
"likelihood": "unmeasured",
|
| 1317 |
-
"path": "trpc/examples/next-prisma-starter/playwright.config.ts"
|
| 1318 |
-
},
|
| 1319 |
-
{
|
| 1320 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1321 |
-
"severity": "high",
|
| 1322 |
-
"line": 21,
|
| 1323 |
-
"evidence": "retries: process.env.CI ? 3 : 0,",
|
| 1324 |
-
"suppressed": false,
|
| 1325 |
-
"advisory": false,
|
| 1326 |
-
"likelihood": "unmeasured",
|
| 1327 |
-
"path": "trpc/examples/next-prisma-starter/playwright.config.ts"
|
| 1328 |
-
},
|
| 1329 |
-
{
|
| 1330 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1331 |
-
"severity": "high",
|
| 1332 |
-
"line": 23,
|
| 1333 |
-
"evidence": "command: process.env.CI ? 'npm run start' : 'npm run dev',",
|
| 1334 |
-
"suppressed": false,
|
| 1335 |
-
"advisory": false,
|
| 1336 |
-
"likelihood": "unmeasured",
|
| 1337 |
-
"path": "trpc/examples/next-prisma-starter/playwright.config.ts"
|
| 1338 |
-
},
|
| 1339 |
-
{
|
| 1340 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1341 |
-
"severity": "high",
|
| 1342 |
-
"line": 24,
|
| 1343 |
-
"evidence": "reuseExistingServer: Boolean(process.env.TEST_LOCAL === '1'),",
|
| 1344 |
-
"suppressed": false,
|
| 1345 |
-
"advisory": false,
|
| 1346 |
-
"likelihood": "unmeasured",
|
| 1347 |
-
"path": "trpc/examples/next-prisma-starter/playwright.config.ts"
|
| 1348 |
-
},
|
| 1349 |
-
{
|
| 1350 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1351 |
-
"severity": "high",
|
| 1352 |
-
"line": 16,
|
| 1353 |
-
"evidence": "if (process.env.VERCEL_URL) {",
|
| 1354 |
-
"suppressed": false,
|
| 1355 |
-
"advisory": false,
|
| 1356 |
-
"likelihood": "unmeasured",
|
| 1357 |
-
"path": "trpc/examples/next-prisma-starter/src/utils/trpc.ts"
|
| 1358 |
-
},
|
| 1359 |
-
{
|
| 1360 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1361 |
-
"severity": "high",
|
| 1362 |
-
"line": 17,
|
| 1363 |
-
"evidence": "return `https://${process.env.VERCEL_URL}`;",
|
| 1364 |
-
"suppressed": false,
|
| 1365 |
-
"advisory": false,
|
| 1366 |
-
"likelihood": "unmeasured",
|
| 1367 |
-
"path": "trpc/examples/next-prisma-starter/src/utils/trpc.ts"
|
| 1368 |
-
},
|
| 1369 |
-
{
|
| 1370 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1371 |
-
"severity": "high",
|
| 1372 |
-
"line": 21,
|
| 1373 |
-
"evidence": "if (process.env.RENDER_INTERNAL_HOSTNAME) {",
|
| 1374 |
-
"suppressed": false,
|
| 1375 |
-
"advisory": false,
|
| 1376 |
-
"likelihood": "unmeasured",
|
| 1377 |
-
"path": "trpc/examples/next-prisma-starter/src/utils/trpc.ts"
|
| 1378 |
-
},
|
| 1379 |
-
{
|
| 1380 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1381 |
-
"severity": "high",
|
| 1382 |
-
"line": 22,
|
| 1383 |
-
"evidence": "return `http://${process.env.RENDER_INTERNAL_HOSTNAME}:${process.env.PORT}`;",
|
| 1384 |
-
"suppressed": false,
|
| 1385 |
-
"advisory": false,
|
| 1386 |
-
"likelihood": "unmeasured",
|
| 1387 |
-
"path": "trpc/examples/next-prisma-starter/src/utils/trpc.ts"
|
| 1388 |
-
},
|
| 1389 |
-
{
|
| 1390 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1391 |
-
"severity": "high",
|
| 1392 |
-
"line": 26,
|
| 1393 |
-
"evidence": "return `http://127.0.0.1:${process.env.PORT ?? 3000}`;",
|
| 1394 |
-
"suppressed": false,
|
| 1395 |
-
"advisory": false,
|
| 1396 |
-
"likelihood": "unmeasured",
|
| 1397 |
-
"path": "trpc/examples/next-prisma-starter/src/utils/trpc.ts"
|
| 1398 |
-
},
|
| 1399 |
-
{
|
| 1400 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1401 |
-
"severity": "high",
|
| 1402 |
-
"line": 62,
|
| 1403 |
-
"evidence": "process.env.NODE_ENV === 'development' ||",
|
| 1404 |
-
"suppressed": false,
|
| 1405 |
-
"advisory": false,
|
| 1406 |
-
"likelihood": "unmeasured",
|
| 1407 |
-
"path": "trpc/examples/next-prisma-starter/src/utils/trpc.ts"
|
| 1408 |
-
},
|
| 1409 |
-
{
|
| 1410 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1411 |
-
"severity": "high",
|
| 1412 |
-
"line": 3,
|
| 1413 |
-
"evidence": "const baseUrl = process.env.PLAYWRIGHT_TEST_BASE_URL || 'http://localhost:3000';",
|
| 1414 |
-
"suppressed": false,
|
| 1415 |
-
"advisory": false,
|
| 1416 |
-
"likelihood": "unmeasured",
|
| 1417 |
-
"path": "trpc/examples/next-prisma-todomvc/playwright.config.ts"
|
| 1418 |
-
},
|
| 1419 |
-
{
|
| 1420 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1421 |
-
"severity": "high",
|
| 1422 |
-
"line": 8,
|
| 1423 |
-
"evidence": "headless: !!process.env.CI || !!process.env.PLAYWRIGHT_HEADLESS,",
|
| 1424 |
-
"suppressed": false,
|
| 1425 |
-
"advisory": false,
|
| 1426 |
-
"likelihood": "unmeasured",
|
| 1427 |
-
"path": "trpc/examples/next-prisma-todomvc/playwright.config.ts"
|
| 1428 |
-
},
|
| 1429 |
-
{
|
| 1430 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1431 |
-
"severity": "high",
|
| 1432 |
-
"line": 17,
|
| 1433 |
-
"evidence": "channel: process.env.CI ? 'chrome' : undefined,",
|
| 1434 |
-
"suppressed": false,
|
| 1435 |
-
"advisory": false,
|
| 1436 |
-
"likelihood": "unmeasured",
|
| 1437 |
-
"path": "trpc/examples/next-prisma-todomvc/playwright.config.ts"
|
| 1438 |
-
},
|
| 1439 |
-
{
|
| 1440 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1441 |
-
"severity": "high",
|
| 1442 |
-
"line": 19,
|
| 1443 |
-
"evidence": "retries: process.env.CI ? 3 : 0,",
|
| 1444 |
-
"suppressed": false,
|
| 1445 |
-
"advisory": false,
|
| 1446 |
-
"likelihood": "unmeasured",
|
| 1447 |
-
"path": "trpc/examples/next-prisma-todomvc/playwright.config.ts"
|
| 1448 |
-
},
|
| 1449 |
-
{
|
| 1450 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1451 |
-
"severity": "high",
|
| 1452 |
-
"line": 10,
|
| 1453 |
-
"evidence": "if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;",
|
| 1454 |
-
"suppressed": false,
|
| 1455 |
-
"advisory": false,
|
| 1456 |
-
"likelihood": "unmeasured",
|
| 1457 |
-
"path": "trpc/examples/next-prisma-todomvc/src/server/prisma.ts"
|
| 1458 |
-
},
|
| 1459 |
-
{
|
| 1460 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1461 |
-
"severity": "high",
|
| 1462 |
-
"line": 14,
|
| 1463 |
-
"evidence": "if (process.env.VERCEL_URL) {",
|
| 1464 |
-
"suppressed": false,
|
| 1465 |
-
"advisory": false,
|
| 1466 |
-
"likelihood": "unmeasured",
|
| 1467 |
-
"path": "trpc/examples/next-prisma-todomvc/src/utils/trpc.ts"
|
| 1468 |
-
},
|
| 1469 |
-
{
|
| 1470 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1471 |
-
"severity": "high",
|
| 1472 |
-
"line": 15,
|
| 1473 |
-
"evidence": "return `https://${process.env.VERCEL_URL}`;",
|
| 1474 |
-
"suppressed": false,
|
| 1475 |
-
"advisory": false,
|
| 1476 |
-
"likelihood": "unmeasured",
|
| 1477 |
-
"path": "trpc/examples/next-prisma-todomvc/src/utils/trpc.ts"
|
| 1478 |
-
},
|
| 1479 |
-
{
|
| 1480 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1481 |
-
"severity": "high",
|
| 1482 |
-
"line": 19,
|
| 1483 |
-
"evidence": "if (process.env.RENDER_INTERNAL_HOSTNAME) {",
|
| 1484 |
-
"suppressed": false,
|
| 1485 |
-
"advisory": false,
|
| 1486 |
-
"likelihood": "unmeasured",
|
| 1487 |
-
"path": "trpc/examples/next-prisma-todomvc/src/utils/trpc.ts"
|
| 1488 |
-
},
|
| 1489 |
-
{
|
| 1490 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1491 |
-
"severity": "high",
|
| 1492 |
-
"line": 20,
|
| 1493 |
-
"evidence": "return `http://${process.env.RENDER_INTERNAL_HOSTNAME}:${process.env.PORT}`;",
|
| 1494 |
-
"suppressed": false,
|
| 1495 |
-
"advisory": false,
|
| 1496 |
-
"likelihood": "unmeasured",
|
| 1497 |
-
"path": "trpc/examples/next-prisma-todomvc/src/utils/trpc.ts"
|
| 1498 |
-
},
|
| 1499 |
-
{
|
| 1500 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1501 |
-
"severity": "high",
|
| 1502 |
-
"line": 24,
|
| 1503 |
-
"evidence": "return `http://localhost:${process.env.PORT ?? 3000}`;",
|
| 1504 |
-
"suppressed": false,
|
| 1505 |
-
"advisory": false,
|
| 1506 |
-
"likelihood": "unmeasured",
|
| 1507 |
-
"path": "trpc/examples/next-prisma-todomvc/src/utils/trpc.ts"
|
| 1508 |
-
},
|
| 1509 |
-
{
|
| 1510 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1511 |
-
"severity": "high",
|
| 1512 |
-
"line": 46,
|
| 1513 |
-
"evidence": "process.env.NODE_ENV === 'development' ||",
|
| 1514 |
-
"suppressed": false,
|
| 1515 |
-
"advisory": false,
|
| 1516 |
-
"likelihood": "unmeasured",
|
| 1517 |
-
"path": "trpc/examples/next-prisma-todomvc/src/utils/trpc.ts"
|
| 1518 |
-
},
|
| 1519 |
-
{
|
| 1520 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1521 |
-
"severity": "high",
|
| 1522 |
-
"line": 12,
|
| 1523 |
-
"evidence": "APP_URL: process.env.APP_URL,",
|
| 1524 |
-
"suppressed": false,
|
| 1525 |
-
"advisory": false,
|
| 1526 |
-
"likelihood": "unmeasured",
|
| 1527 |
-
"path": "trpc/examples/next-prisma-websockets-starter/next.config.ts"
|
| 1528 |
-
},
|
| 1529 |
-
{
|
| 1530 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1531 |
-
"severity": "high",
|
| 1532 |
-
"line": 13,
|
| 1533 |
-
"evidence": "WS_URL: process.env.WS_URL,",
|
| 1534 |
-
"suppressed": false,
|
| 1535 |
-
"advisory": false,
|
| 1536 |
-
"likelihood": "unmeasured",
|
| 1537 |
-
"path": "trpc/examples/next-prisma-websockets-starter/next.config.ts"
|
| 1538 |
-
},
|
| 1539 |
-
{
|
| 1540 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1541 |
-
"severity": "high",
|
| 1542 |
-
"line": 16,
|
| 1543 |
-
"evidence": "eslint: { ignoreDuringBuilds: !!process.env.CI },",
|
| 1544 |
-
"suppressed": false,
|
| 1545 |
-
"advisory": false,
|
| 1546 |
-
"likelihood": "unmeasured",
|
| 1547 |
-
"path": "trpc/examples/next-prisma-websockets-starter/next.config.ts"
|
| 1548 |
-
},
|
| 1549 |
-
{
|
| 1550 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1551 |
-
"severity": "high",
|
| 1552 |
-
"line": 3,
|
| 1553 |
-
"evidence": "const baseUrl = process.env.PLAYWRIGHT_TEST_BASE_URL || 'http://localhost:3000';",
|
| 1554 |
-
"suppressed": false,
|
| 1555 |
-
"advisory": false,
|
| 1556 |
-
"likelihood": "unmeasured",
|
| 1557 |
-
"path": "trpc/examples/next-prisma-websockets-starter/playwright.config.ts"
|
| 1558 |
-
},
|
| 1559 |
-
{
|
| 1560 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1561 |
-
"severity": "high",
|
| 1562 |
-
"line": 8,
|
| 1563 |
-
"evidence": "headless: !!process.env.CI || !!process.env.PLAYWRIGHT_HEADLESS,",
|
| 1564 |
-
"suppressed": false,
|
| 1565 |
-
"advisory": false,
|
| 1566 |
-
"likelihood": "unmeasured",
|
| 1567 |
-
"path": "trpc/examples/next-prisma-websockets-starter/playwright.config.ts"
|
| 1568 |
-
},
|
| 1569 |
-
{
|
| 1570 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1571 |
-
"severity": "high",
|
| 1572 |
-
"line": 17,
|
| 1573 |
-
"evidence": "channel: process.env.CI ? 'chrome' : undefined,",
|
| 1574 |
-
"suppressed": false,
|
| 1575 |
-
"advisory": false,
|
| 1576 |
-
"likelihood": "unmeasured",
|
| 1577 |
-
"path": "trpc/examples/next-prisma-websockets-starter/playwright.config.ts"
|
| 1578 |
-
},
|
| 1579 |
-
{
|
| 1580 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1581 |
-
"severity": "high",
|
| 1582 |
-
"line": 19,
|
| 1583 |
-
"evidence": "retries: process.env.CI ? 3 : 0,",
|
| 1584 |
-
"suppressed": false,
|
| 1585 |
-
"advisory": false,
|
| 1586 |
-
"likelihood": "unmeasured",
|
| 1587 |
-
"path": "trpc/examples/next-prisma-websockets-starter/playwright.config.ts"
|
| 1588 |
-
},
|
| 1589 |
-
{
|
| 1590 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1591 |
-
"severity": "high",
|
| 1592 |
-
"line": 297,
|
| 1593 |
-
"evidence": "{process.env.NODE_ENV !== 'production' && (",
|
| 1594 |
-
"suppressed": false,
|
| 1595 |
-
"advisory": false,
|
| 1596 |
-
"likelihood": "unmeasured",
|
| 1597 |
-
"path": "trpc/examples/next-prisma-websockets-starter/src/pages/index.tsx"
|
| 1598 |
-
},
|
| 1599 |
-
{
|
| 1600 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1601 |
-
"severity": "high",
|
| 1602 |
-
"line": 7,
|
| 1603 |
-
"evidence": "process.env.NODE_ENV === 'test' ||",
|
| 1604 |
-
"suppressed": false,
|
| 1605 |
-
"advisory": false,
|
| 1606 |
-
"likelihood": "unmeasured",
|
| 1607 |
-
"path": "trpc/examples/next-prisma-websockets-starter/src/pages/api/auth/[...nextauth].ts"
|
| 1608 |
-
},
|
| 1609 |
-
{
|
| 1610 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1611 |
-
"severity": "high",
|
| 1612 |
-
"line": 8,
|
| 1613 |
-
"evidence": "process.env.RAILWAY_ENVIRONMENT_NAME?.includes('-pr-'); // example: 'trpc-pr-5821'",
|
| 1614 |
-
"suppressed": false,
|
| 1615 |
-
"advisory": false,
|
| 1616 |
-
"likelihood": "unmeasured",
|
| 1617 |
-
"path": "trpc/examples/next-prisma-websockets-starter/src/pages/api/auth/[...nextauth].ts"
|
| 1618 |
-
},
|
| 1619 |
-
{
|
| 1620 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1621 |
-
"severity": "high",
|
| 1622 |
-
"line": 15,
|
| 1623 |
-
"evidence": "process.env.NODE_ENV === 'development'",
|
| 1624 |
-
"suppressed": false,
|
| 1625 |
-
"advisory": false,
|
| 1626 |
-
"likelihood": "unmeasured",
|
| 1627 |
-
"path": "trpc/examples/next-prisma-websockets-starter/src/server/prisma.ts"
|
| 1628 |
-
},
|
| 1629 |
-
{
|
| 1630 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1631 |
-
"severity": "high",
|
| 1632 |
-
"line": 20,
|
| 1633 |
-
"evidence": "if (process.env.NODE_ENV !== 'production') {",
|
| 1634 |
-
"suppressed": false,
|
| 1635 |
-
"advisory": false,
|
| 1636 |
-
"likelihood": "unmeasured",
|
| 1637 |
-
"path": "trpc/examples/next-prisma-websockets-starter/src/server/prisma.ts"
|
| 1638 |
-
},
|
| 1639 |
-
{
|
| 1640 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1641 |
-
"severity": "high",
|
| 1642 |
-
"line": 11,
|
| 1643 |
-
"evidence": "const port = parseInt(process.env.PORT || '3000', 10);",
|
| 1644 |
-
"suppressed": false,
|
| 1645 |
-
"advisory": false,
|
| 1646 |
-
"likelihood": "unmeasured",
|
| 1647 |
-
"path": "trpc/examples/next-prisma-websockets-starter/src/server/prodServer.ts"
|
| 1648 |
-
},
|
| 1649 |
-
{
|
| 1650 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1651 |
-
"severity": "high",
|
| 1652 |
-
"line": 12,
|
| 1653 |
-
"evidence": "const dev = process.env.NODE_ENV !== 'production';",
|
| 1654 |
-
"suppressed": false,
|
| 1655 |
-
"advisory": false,
|
| 1656 |
-
"likelihood": "unmeasured",
|
| 1657 |
-
"path": "trpc/examples/next-prisma-websockets-starter/src/server/prodServer.ts"
|
| 1658 |
-
},
|
| 1659 |
-
{
|
| 1660 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1661 |
-
"severity": "high",
|
| 1662 |
-
"line": 46,
|
| 1663 |
-
"evidence": "dev ? 'development' : process.env.NODE_ENV",
|
| 1664 |
-
"suppressed": false,
|
| 1665 |
-
"advisory": false,
|
| 1666 |
-
"likelihood": "unmeasured",
|
| 1667 |
-
"path": "trpc/examples/next-prisma-websockets-starter/src/server/prodServer.ts"
|
| 1668 |
-
},
|
| 1669 |
-
{
|
| 1670 |
-
"rule": "typescript.derive-type-from-schema",
|
| 1671 |
-
"severity": "high",
|
| 1672 |
-
"line": 23,
|
| 1673 |
-
"evidence": "interface MyEvents {",
|
| 1674 |
-
"suppressed": false,
|
| 1675 |
-
"advisory": false,
|
| 1676 |
-
"likelihood": "unmeasured",
|
| 1677 |
-
"path": "trpc/examples/next-prisma-websockets-starter/src/server/routers/post.ts"
|
| 1678 |
-
},
|
| 1679 |
-
{
|
| 1680 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1681 |
-
"severity": "high",
|
| 1682 |
-
"line": 79,
|
| 1683 |
-
"evidence": "(process.env.NODE_ENV === 'development' &&",
|
| 1684 |
-
"suppressed": false,
|
| 1685 |
-
"advisory": false,
|
| 1686 |
-
"likelihood": "unmeasured",
|
| 1687 |
-
"path": "trpc/examples/next-prisma-websockets-starter/src/utils/trpc.ts"
|
| 1688 |
-
},
|
| 1689 |
-
{
|
| 1690 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1691 |
-
"severity": "high",
|
| 1692 |
-
"line": 3,
|
| 1693 |
-
"evidence": "const DB_URL = process.env.DATABASE_URL ?? process.env.POSTGRES_URL;",
|
| 1694 |
-
"suppressed": false,
|
| 1695 |
-
"advisory": false,
|
| 1696 |
-
"likelihood": "unmeasured",
|
| 1697 |
-
"path": "trpc/examples/next-sse-chat/drizzle.config.ts"
|
| 1698 |
-
},
|
| 1699 |
-
{
|
| 1700 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1701 |
-
"severity": "high",
|
| 1702 |
-
"line": 31,
|
| 1703 |
-
"evidence": "if (process.env.APP_URL) return process.env.APP_URL;",
|
| 1704 |
-
"suppressed": false,
|
| 1705 |
-
"advisory": false,
|
| 1706 |
-
"likelihood": "unmeasured",
|
| 1707 |
-
"path": "trpc/examples/next-sse-chat/src/app/providers.tsx"
|
| 1708 |
-
},
|
| 1709 |
-
{
|
| 1710 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1711 |
-
"severity": "high",
|
| 1712 |
-
"line": 32,
|
| 1713 |
-
"evidence": "return `http://localhost:${process.env.PORT ?? 3000}`;",
|
| 1714 |
-
"suppressed": false,
|
| 1715 |
-
"advisory": false,
|
| 1716 |
-
"likelihood": "unmeasured",
|
| 1717 |
-
"path": "trpc/examples/next-sse-chat/src/app/providers.tsx"
|
| 1718 |
-
},
|
| 1719 |
-
{
|
| 1720 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1721 |
-
"severity": "high",
|
| 1722 |
-
"line": 13,
|
| 1723 |
-
"evidence": "process.env.NODE_ENV === 'test' ||",
|
| 1724 |
-
"suppressed": false,
|
| 1725 |
-
"advisory": false,
|
| 1726 |
-
"likelihood": "unmeasured",
|
| 1727 |
-
"path": "trpc/examples/next-sse-chat/src/server/auth.tsx"
|
| 1728 |
-
},
|
| 1729 |
-
{
|
| 1730 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1731 |
-
"severity": "high",
|
| 1732 |
-
"line": 14,
|
| 1733 |
-
"evidence": "process.env.RAILWAY_ENVIRONMENT_NAME?.includes('-pr-'); // example: 'trpc-pr-5821'",
|
| 1734 |
-
"suppressed": false,
|
| 1735 |
-
"advisory": false,
|
| 1736 |
-
"likelihood": "unmeasured",
|
| 1737 |
-
"path": "trpc/examples/next-sse-chat/src/server/auth.tsx"
|
| 1738 |
-
},
|
| 1739 |
-
{
|
| 1740 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1741 |
-
"severity": "high",
|
| 1742 |
-
"line": 54,
|
| 1743 |
-
"evidence": "if (process.env.NODE_ENV === 'development') {",
|
| 1744 |
-
"suppressed": false,
|
| 1745 |
-
"advisory": false,
|
| 1746 |
-
"likelihood": "unmeasured",
|
| 1747 |
-
"path": "trpc/examples/next-sse-chat/src/server/trpc.ts"
|
| 1748 |
-
},
|
| 1749 |
-
{
|
| 1750 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1751 |
-
"severity": "high",
|
| 1752 |
-
"line": 5,
|
| 1753 |
-
"evidence": "const DB_URL = process.env.DATABASE_URL ?? process.env.POSTGRES_URL;",
|
| 1754 |
-
"suppressed": false,
|
| 1755 |
-
"advisory": false,
|
| 1756 |
-
"likelihood": "unmeasured",
|
| 1757 |
-
"path": "trpc/examples/next-sse-chat/src/server/db/client.ts"
|
| 1758 |
-
},
|
| 1759 |
-
{
|
| 1760 |
-
"rule": "typescript.derive-type-from-schema",
|
| 1761 |
-
"severity": "high",
|
| 1762 |
-
"line": 21,
|
| 1763 |
-
"evidence": "export interface MyEvents {",
|
| 1764 |
-
"suppressed": false,
|
| 1765 |
-
"advisory": false,
|
| 1766 |
-
"likelihood": "unmeasured",
|
| 1767 |
-
"path": "trpc/examples/next-sse-chat/src/server/routers/channel.ts"
|
| 1768 |
-
},
|
| 1769 |
-
{
|
| 1770 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1771 |
-
"severity": "high",
|
| 1772 |
-
"line": 5,
|
| 1773 |
-
"evidence": "APP_URL: process.env.APP_URL ?? 'http://localhost:3000',",
|
| 1774 |
-
"suppressed": false,
|
| 1775 |
-
"advisory": false,
|
| 1776 |
-
"likelihood": "unmeasured",
|
| 1777 |
-
"path": "trpc/examples/next-websockets-encoder/next.config.ts"
|
| 1778 |
-
},
|
| 1779 |
-
{
|
| 1780 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1781 |
-
"severity": "high",
|
| 1782 |
-
"line": 6,
|
| 1783 |
-
"evidence": "WS_URL: process.env.WS_URL ?? 'ws://localhost:3001',",
|
| 1784 |
-
"suppressed": false,
|
| 1785 |
-
"advisory": false,
|
| 1786 |
-
"likelihood": "unmeasured",
|
| 1787 |
-
"path": "trpc/examples/next-websockets-encoder/next.config.ts"
|
| 1788 |
-
},
|
| 1789 |
-
{
|
| 1790 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1791 |
-
"severity": "high",
|
| 1792 |
-
"line": 8,
|
| 1793 |
-
"evidence": "eslint: { ignoreDuringBuilds: !!process.env.CI },",
|
| 1794 |
-
"suppressed": false,
|
| 1795 |
-
"advisory": false,
|
| 1796 |
-
"likelihood": "unmeasured",
|
| 1797 |
-
"path": "trpc/examples/next-websockets-encoder/next.config.ts"
|
| 1798 |
-
},
|
| 1799 |
-
{
|
| 1800 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1801 |
-
"severity": "high",
|
| 1802 |
-
"line": 10,
|
| 1803 |
-
"evidence": "const port = parseInt(process.env.PORT ?? '3000', 10);",
|
| 1804 |
-
"suppressed": false,
|
| 1805 |
-
"advisory": false,
|
| 1806 |
-
"likelihood": "unmeasured",
|
| 1807 |
-
"path": "trpc/examples/next-websockets-encoder/src/server/prodServer.ts"
|
| 1808 |
-
},
|
| 1809 |
-
{
|
| 1810 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1811 |
-
"severity": "high",
|
| 1812 |
-
"line": 11,
|
| 1813 |
-
"evidence": "const dev = process.env.NODE_ENV !== 'production';",
|
| 1814 |
-
"suppressed": false,
|
| 1815 |
-
"advisory": false,
|
| 1816 |
-
"likelihood": "unmeasured",
|
| 1817 |
-
"path": "trpc/examples/next-websockets-encoder/src/server/prodServer.ts"
|
| 1818 |
-
},
|
| 1819 |
-
{
|
| 1820 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1821 |
-
"severity": "high",
|
| 1822 |
-
"line": 48,
|
| 1823 |
-
"evidence": "dev ? 'development' : process.env.NODE_ENV",
|
| 1824 |
-
"suppressed": false,
|
| 1825 |
-
"advisory": false,
|
| 1826 |
-
"likelihood": "unmeasured",
|
| 1827 |
-
"path": "trpc/examples/next-websockets-encoder/src/server/prodServer.ts"
|
| 1828 |
-
},
|
| 1829 |
-
{
|
| 1830 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1831 |
-
"severity": "high",
|
| 1832 |
-
"line": 5,
|
| 1833 |
-
"evidence": "headless: !!process.env.CI || !!process.env.PLAYWRIGHT_HEADLESS,",
|
| 1834 |
-
"suppressed": false,
|
| 1835 |
-
"advisory": false,
|
| 1836 |
-
"likelihood": "unmeasured",
|
| 1837 |
-
"path": "trpc/examples/nuxt/playwright.config.ts"
|
| 1838 |
-
},
|
| 1839 |
-
{
|
| 1840 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1841 |
-
"severity": "high",
|
| 1842 |
-
"line": 19,
|
| 1843 |
-
"evidence": "reuseExistingServer: !process.env.CI,",
|
| 1844 |
-
"suppressed": false,
|
| 1845 |
-
"advisory": false,
|
| 1846 |
-
"likelihood": "unmeasured",
|
| 1847 |
-
"path": "trpc/examples/nuxt/playwright.config.ts"
|
| 1848 |
-
},
|
| 1849 |
-
{
|
| 1850 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1851 |
-
"severity": "high",
|
| 1852 |
-
"line": 24,
|
| 1853 |
-
"evidence": "reuseExistingServer: !process.env.CI,",
|
| 1854 |
-
"suppressed": false,
|
| 1855 |
-
"advisory": false,
|
| 1856 |
-
"likelihood": "unmeasured",
|
| 1857 |
-
"path": "trpc/examples/nuxt/playwright.config.ts"
|
| 1858 |
-
},
|
| 1859 |
-
{
|
| 1860 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1861 |
-
"severity": "high",
|
| 1862 |
-
"line": 34,
|
| 1863 |
-
"evidence": "channel: process.env.CI ? 'chrome' : undefined,",
|
| 1864 |
-
"suppressed": false,
|
| 1865 |
-
"advisory": false,
|
| 1866 |
-
"likelihood": "unmeasured",
|
| 1867 |
-
"path": "trpc/examples/nuxt/playwright.config.ts"
|
| 1868 |
-
},
|
| 1869 |
-
{
|
| 1870 |
-
"rule": "typescript.validate-env-at-startup",
|
| 1871 |
-
"severity": "high",
|
| 1872 |
-
"line": 43,
|
| 1873 |
-
"evidence": "channel: process.env.CI ? 'chrome' : undefined,",
|
| 1874 |
-
"suppressed": false,
|
| 1875 |
-
"advisory": false,
|
| 1876 |
-
"likelihood": "unmeasured",
|
| 1877 |
-
"path": "trpc/examples/nuxt/playwright.config.ts"
|
| 1878 |
-
},
|
| 1879 |
-
{
|
| 1880 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 1881 |
-
"severity": "high",
|
| 1882 |
-
"line": 18,
|
| 1883 |
-
"evidence": "body?: any;",
|
| 1884 |
-
"suppressed": false,
|
| 1885 |
-
"advisory": false,
|
| 1886 |
-
"likelihood": "unmeasured",
|
| 1887 |
-
"path": "trpc/examples/openapi-codegen/src/client/generated/client/client.gen.ts"
|
| 1888 |
-
},
|
| 1889 |
-
{
|
| 1890 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 1891 |
-
"severity": "high",
|
| 1892 |
-
"line": 121,
|
| 1893 |
-
"evidence": "let emptyData: any;",
|
| 1894 |
-
"suppressed": false,
|
| 1895 |
-
"advisory": false,
|
| 1896 |
-
"likelihood": "unmeasured",
|
| 1897 |
-
"path": "trpc/examples/openapi-codegen/src/client/generated/client/client.gen.ts"
|
| 1898 |
-
},
|
| 1899 |
-
{
|
| 1900 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 1901 |
-
"severity": "high",
|
| 1902 |
-
"line": 147,
|
| 1903 |
-
"evidence": "let data: any;",
|
| 1904 |
-
"suppressed": false,
|
| 1905 |
-
"advisory": false,
|
| 1906 |
-
"likelihood": "unmeasured",
|
| 1907 |
-
"path": "trpc/examples/openapi-codegen/src/client/generated/client/client.gen.ts"
|
| 1908 |
-
},
|
| 1909 |
-
{
|
| 1910 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 1911 |
-
"severity": "high",
|
| 1912 |
-
"line": 52,
|
| 1913 |
-
"evidence": "input: any;",
|
| 1914 |
-
"suppressed": false,
|
| 1915 |
-
"advisory": false,
|
| 1916 |
-
"likelihood": "unmeasured",
|
| 1917 |
-
"path": "trpc/packages/client/src/createTRPCClient.ts"
|
| 1918 |
-
},
|
| 1919 |
-
{
|
| 1920 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 1921 |
-
"severity": "high",
|
| 1922 |
-
"line": 53,
|
| 1923 |
-
"evidence": "output: any;",
|
| 1924 |
-
"suppressed": false,
|
| 1925 |
-
"advisory": false,
|
| 1926 |
-
"likelihood": "unmeasured",
|
| 1927 |
-
"path": "trpc/packages/client/src/createTRPCClient.ts"
|
| 1928 |
-
},
|
| 1929 |
-
{
|
| 1930 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 1931 |
-
"severity": "high",
|
| 1932 |
-
"line": 55,
|
| 1933 |
-
"evidence": "errorShape: any;",
|
| 1934 |
-
"suppressed": false,
|
| 1935 |
-
"advisory": false,
|
| 1936 |
-
"likelihood": "unmeasured",
|
| 1937 |
-
"path": "trpc/packages/client/src/createTRPCClient.ts"
|
| 1938 |
-
},
|
| 1939 |
-
{
|
| 1940 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 1941 |
-
"severity": "high",
|
| 1942 |
-
"line": 3,
|
| 1943 |
-
"evidence": "type AnyFn = (...args: any[]) => unknown;",
|
| 1944 |
-
"suppressed": false,
|
| 1945 |
-
"advisory": false,
|
| 1946 |
-
"likelihood": "unmeasured",
|
| 1947 |
-
"path": "trpc/packages/client/src/getFetch.ts"
|
| 1948 |
-
},
|
| 1949 |
-
{
|
| 1950 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 1951 |
-
"severity": "high",
|
| 1952 |
-
"line": 66,
|
| 1953 |
-
"evidence": "listener: (...args: any[]) => void,",
|
| 1954 |
-
"suppressed": false,
|
| 1955 |
-
"advisory": false,
|
| 1956 |
-
"likelihood": "unmeasured",
|
| 1957 |
-
"path": "trpc/packages/client/src/internals/types.ts"
|
| 1958 |
-
},
|
| 1959 |
-
{
|
| 1960 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 1961 |
-
"severity": "high",
|
| 1962 |
-
"line": 17,
|
| 1963 |
-
"evidence": "log: (...args: any[]) => void;",
|
| 1964 |
-
"suppressed": false,
|
| 1965 |
-
"advisory": false,
|
| 1966 |
-
"likelihood": "unmeasured",
|
| 1967 |
-
"path": "trpc/packages/client/src/links/loggerLink.ts"
|
| 1968 |
-
},
|
| 1969 |
-
{
|
| 1970 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 1971 |
-
"severity": "high",
|
| 1972 |
-
"line": 18,
|
| 1973 |
-
"evidence": "error: (...args: any[]) => void;",
|
| 1974 |
-
"suppressed": false,
|
| 1975 |
-
"advisory": false,
|
| 1976 |
-
"likelihood": "unmeasured",
|
| 1977 |
-
"path": "trpc/packages/client/src/links/loggerLink.ts"
|
| 1978 |
-
},
|
| 1979 |
-
{
|
| 1980 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 1981 |
-
"severity": "high",
|
| 1982 |
-
"line": 120,
|
| 1983 |
-
"evidence": "const args: any[] = [];",
|
| 1984 |
-
"suppressed": false,
|
| 1985 |
-
"advisory": false,
|
| 1986 |
-
"likelihood": "unmeasured",
|
| 1987 |
-
"path": "trpc/packages/client/src/links/loggerLink.ts"
|
| 1988 |
-
},
|
| 1989 |
-
{
|
| 1990 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 1991 |
-
"severity": "high",
|
| 1992 |
-
"line": 59,
|
| 1993 |
-
"evidence": "let reject: (reason?: any) => void;",
|
| 1994 |
-
"suppressed": false,
|
| 1995 |
-
"advisory": false,
|
| 1996 |
-
"likelihood": "unmeasured",
|
| 1997 |
-
"path": "trpc/packages/client/src/links/wsLink/wsClient/utils.ts"
|
| 1998 |
-
},
|
| 1999 |
-
{
|
| 2000 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2001 |
-
"severity": "high",
|
| 2002 |
-
"line": 76,
|
| 2003 |
-
"evidence": "_action: (...args: any[]) => Promise<any>;",
|
| 2004 |
-
"suppressed": false,
|
| 2005 |
-
"advisory": false,
|
| 2006 |
-
"likelihood": "unmeasured",
|
| 2007 |
-
"path": "trpc/packages/next/src/app-dir/create-action-hook.tsx"
|
| 2008 |
-
},
|
| 2009 |
-
{
|
| 2010 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2011 |
-
"severity": "high",
|
| 2012 |
-
"line": 144,
|
| 2013 |
-
"evidence": "_action: (...args: any[]) => Promise<any>;",
|
| 2014 |
-
"suppressed": false,
|
| 2015 |
-
"advisory": false,
|
| 2016 |
-
"likelihood": "unmeasured",
|
| 2017 |
-
"path": "trpc/packages/next/src/app-dir/create-action-hook.tsx"
|
| 2018 |
-
},
|
| 2019 |
-
{
|
| 2020 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2021 |
-
"severity": "high",
|
| 2022 |
-
"line": 173,
|
| 2023 |
-
"evidence": "(input: any, requestOptions?: TRPCRequestOptions) => {",
|
| 2024 |
-
"suppressed": false,
|
| 2025 |
-
"advisory": false,
|
| 2026 |
-
"likelihood": "unmeasured",
|
| 2027 |
-
"path": "trpc/packages/next/src/app-dir/create-action-hook.tsx"
|
| 2028 |
-
},
|
| 2029 |
-
{
|
| 2030 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2031 |
-
"severity": "high",
|
| 2032 |
-
"line": 219,
|
| 2033 |
-
"evidence": "(...args: any[]) => {",
|
| 2034 |
-
"suppressed": false,
|
| 2035 |
-
"advisory": false,
|
| 2036 |
-
"likelihood": "unmeasured",
|
| 2037 |
-
"path": "trpc/packages/next/src/app-dir/create-action-hook.tsx"
|
| 2038 |
-
},
|
| 2039 |
-
{
|
| 2040 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2041 |
-
"severity": "high",
|
| 2042 |
-
"line": 102,
|
| 2043 |
-
"evidence": "export function generateCacheTag(procedurePath: string, input: any) {",
|
| 2044 |
-
"suppressed": false,
|
| 2045 |
-
"advisory": false,
|
| 2046 |
-
"likelihood": "unmeasured",
|
| 2047 |
-
"path": "trpc/packages/next/src/app-dir/shared.ts"
|
| 2048 |
-
},
|
| 2049 |
-
{
|
| 2050 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2051 |
-
"severity": "high",
|
| 2052 |
-
"line": 120,
|
| 2053 |
-
"evidence": "input?: any;",
|
| 2054 |
-
"suppressed": false,
|
| 2055 |
-
"advisory": false,
|
| 2056 |
-
"likelihood": "unmeasured",
|
| 2057 |
-
"path": "trpc/packages/next/src/app-dir/shared.ts"
|
| 2058 |
-
},
|
| 2059 |
-
{
|
| 2060 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2061 |
-
"severity": "high",
|
| 2062 |
-
"line": 121,
|
| 2063 |
-
"evidence": "output?: any;",
|
| 2064 |
-
"suppressed": false,
|
| 2065 |
-
"advisory": false,
|
| 2066 |
-
"likelihood": "unmeasured",
|
| 2067 |
-
"path": "trpc/packages/next/src/app-dir/shared.ts"
|
| 2068 |
-
},
|
| 2069 |
-
{
|
| 2070 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2071 |
-
"severity": "high",
|
| 2072 |
-
"line": 122,
|
| 2073 |
-
"evidence": "errorShape: any;",
|
| 2074 |
-
"suppressed": false,
|
| 2075 |
-
"advisory": false,
|
| 2076 |
-
"likelihood": "unmeasured",
|
| 2077 |
-
"path": "trpc/packages/next/src/app-dir/shared.ts"
|
| 2078 |
-
},
|
| 2079 |
-
{
|
| 2080 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2081 |
-
"severity": "high",
|
| 2082 |
-
"line": 12,
|
| 2083 |
-
"evidence": "input: any;",
|
| 2084 |
-
"suppressed": false,
|
| 2085 |
-
"advisory": false,
|
| 2086 |
-
"likelihood": "unmeasured",
|
| 2087 |
-
"path": "trpc/packages/next/src/app-dir/types.ts"
|
| 2088 |
-
},
|
| 2089 |
-
{
|
| 2090 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2091 |
-
"severity": "high",
|
| 2092 |
-
"line": 13,
|
| 2093 |
-
"evidence": "output: any;",
|
| 2094 |
-
"suppressed": false,
|
| 2095 |
-
"advisory": false,
|
| 2096 |
-
"likelihood": "unmeasured",
|
| 2097 |
-
"path": "trpc/packages/next/src/app-dir/types.ts"
|
| 2098 |
-
},
|
| 2099 |
-
{
|
| 2100 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2101 |
-
"severity": "high",
|
| 2102 |
-
"line": 15,
|
| 2103 |
-
"evidence": "errorShape: any;",
|
| 2104 |
-
"suppressed": false,
|
| 2105 |
-
"advisory": false,
|
| 2106 |
-
"likelihood": "unmeasured",
|
| 2107 |
-
"path": "trpc/packages/next/src/app-dir/types.ts"
|
| 2108 |
-
},
|
| 2109 |
-
{
|
| 2110 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2111 |
-
"severity": "high",
|
| 2112 |
-
"line": 57,
|
| 2113 |
-
"evidence": "input: any;",
|
| 2114 |
-
"suppressed": false,
|
| 2115 |
-
"advisory": false,
|
| 2116 |
-
"likelihood": "unmeasured",
|
| 2117 |
-
"path": "trpc/packages/react-query/src/createTRPCReact.tsx"
|
| 2118 |
-
},
|
| 2119 |
-
{
|
| 2120 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2121 |
-
"severity": "high",
|
| 2122 |
-
"line": 58,
|
| 2123 |
-
"evidence": "output: any;",
|
| 2124 |
-
"suppressed": false,
|
| 2125 |
-
"advisory": false,
|
| 2126 |
-
"likelihood": "unmeasured",
|
| 2127 |
-
"path": "trpc/packages/react-query/src/createTRPCReact.tsx"
|
| 2128 |
-
},
|
| 2129 |
-
{
|
| 2130 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2131 |
-
"severity": "high",
|
| 2132 |
-
"line": 60,
|
| 2133 |
-
"evidence": "errorShape: any;",
|
| 2134 |
-
"suppressed": false,
|
| 2135 |
-
"advisory": false,
|
| 2136 |
-
"likelihood": "unmeasured",
|
| 2137 |
-
"path": "trpc/packages/react-query/src/createTRPCReact.tsx"
|
| 2138 |
-
},
|
| 2139 |
-
{
|
| 2140 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2141 |
-
"severity": "high",
|
| 2142 |
-
"line": 108,
|
| 2143 |
-
"evidence": "cursor?: any;",
|
| 2144 |
-
"suppressed": false,
|
| 2145 |
-
"advisory": false,
|
| 2146 |
-
"likelihood": "unmeasured",
|
| 2147 |
-
"path": "trpc/packages/react-query/src/createTRPCReact.tsx"
|
| 2148 |
-
},
|
| 2149 |
-
{
|
| 2150 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2151 |
-
"severity": "high",
|
| 2152 |
-
"line": 115,
|
| 2153 |
-
"evidence": "type inferCursorType<TInput> = TInput extends { cursor?: any }",
|
| 2154 |
-
"suppressed": false,
|
| 2155 |
-
"advisory": false,
|
| 2156 |
-
"likelihood": "unmeasured",
|
| 2157 |
-
"path": "trpc/packages/react-query/src/createTRPCReact.tsx"
|
| 2158 |
-
},
|
| 2159 |
-
{
|
| 2160 |
-
"rule": "typescript.types-are-erased-at-runtime",
|
| 2161 |
-
"severity": "critical",
|
| 2162 |
-
"line": 113,
|
| 2163 |
-
"evidence": ") as unknown as DecorateProcedure<RootTypes, AnyProcedure>;",
|
| 2164 |
-
"suppressed": false,
|
| 2165 |
-
"advisory": false,
|
| 2166 |
-
"likelihood": "unmeasured",
|
| 2167 |
-
"path": "trpc/packages/react-query/src/rsc.tsx"
|
| 2168 |
-
},
|
| 2169 |
-
{
|
| 2170 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2171 |
-
"severity": "high",
|
| 2172 |
-
"line": 10,
|
| 2173 |
-
"evidence": "pageParam: any;",
|
| 2174 |
-
"suppressed": false,
|
| 2175 |
-
"advisory": false,
|
| 2176 |
-
"likelihood": "unmeasured",
|
| 2177 |
-
"path": "trpc/packages/react-query/src/internals/getClientArgs.ts"
|
| 2178 |
-
},
|
| 2179 |
-
{
|
| 2180 |
-
"rule": "typescript.types-are-erased-at-runtime",
|
| 2181 |
-
"severity": "critical",
|
| 2182 |
-
"line": 43,
|
| 2183 |
-
"evidence": "return splitPath.length ? [splitPath] : ([] as unknown as TRPCQueryKey);",
|
| 2184 |
-
"suppressed": false,
|
| 2185 |
-
"advisory": false,
|
| 2186 |
-
"likelihood": "unmeasured",
|
| 2187 |
-
"path": "trpc/packages/react-query/src/internals/getQueryKey.ts"
|
| 2188 |
-
},
|
| 2189 |
-
{
|
| 2190 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2191 |
-
"severity": "high",
|
| 2192 |
-
"line": 90,
|
| 2193 |
-
"evidence": "cursor?: any;",
|
| 2194 |
-
"suppressed": false,
|
| 2195 |
-
"advisory": false,
|
| 2196 |
-
"likelihood": "unmeasured",
|
| 2197 |
-
"path": "trpc/packages/react-query/src/internals/getQueryKey.ts"
|
| 2198 |
-
},
|
| 2199 |
-
{
|
| 2200 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2201 |
-
"severity": "high",
|
| 2202 |
-
"line": 110,
|
| 2203 |
-
"evidence": "export type ExtractCursorType<TInput> = TInput extends { cursor?: any }",
|
| 2204 |
-
"suppressed": false,
|
| 2205 |
-
"advisory": false,
|
| 2206 |
-
"likelihood": "unmeasured",
|
| 2207 |
-
"path": "trpc/packages/react-query/src/shared/hooks/types.ts"
|
| 2208 |
-
},
|
| 2209 |
-
{
|
| 2210 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2211 |
-
"severity": "high",
|
| 2212 |
-
"line": 251,
|
| 2213 |
-
"evidence": "inferProcedureInput<TProcedure> extends { cursor?: any } | void",
|
| 2214 |
-
"suppressed": false,
|
| 2215 |
-
"advisory": false,
|
| 2216 |
-
"likelihood": "unmeasured",
|
| 2217 |
-
"path": "trpc/packages/react-query/src/shared/proxy/utilsProxy.ts"
|
| 2218 |
-
},
|
| 2219 |
-
{
|
| 2220 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2221 |
-
"severity": "high",
|
| 2222 |
-
"line": 150,
|
| 2223 |
-
"evidence": "export type inferAsyncReturnType<TFunction extends (...args: any[]) => any> =",
|
| 2224 |
-
"suppressed": false,
|
| 2225 |
-
"advisory": false,
|
| 2226 |
-
"likelihood": "unmeasured",
|
| 2227 |
-
"path": "trpc/packages/server/src/@trpc/server/index.ts"
|
| 2228 |
-
},
|
| 2229 |
-
{
|
| 2230 |
-
"rule": "typescript.types-are-erased-at-runtime",
|
| 2231 |
-
"severity": "critical",
|
| 2232 |
-
"line": 73,
|
| 2233 |
-
"evidence": "opts.trpcOptions as unknown as WSSHandlerOptions<TRouter>;",
|
| 2234 |
-
"suppressed": false,
|
| 2235 |
-
"advisory": false,
|
| 2236 |
-
"likelihood": "unmeasured",
|
| 2237 |
-
"path": "trpc/packages/server/src/adapters/fastify/fastifyTRPCPlugin.ts"
|
| 2238 |
-
},
|
| 2239 |
-
{
|
| 2240 |
-
"rule": "typescript.types-are-erased-at-runtime",
|
| 2241 |
-
"severity": "critical",
|
| 2242 |
-
"line": 29,
|
| 2243 |
-
"evidence": "return new TRPCRedirectError(url, redirectType) as unknown as undefined;",
|
| 2244 |
-
"suppressed": false,
|
| 2245 |
-
"advisory": false,
|
| 2246 |
-
"likelihood": "unmeasured",
|
| 2247 |
-
"path": "trpc/packages/server/src/adapters/next-app-dir/redirect.ts"
|
| 2248 |
-
},
|
| 2249 |
-
{
|
| 2250 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2251 |
-
"severity": "high",
|
| 2252 |
-
"line": 79,
|
| 2253 |
-
"evidence": "> = (req: TRequest, res: TResponse, next: (err?: any) => any) => void;",
|
| 2254 |
-
"suppressed": false,
|
| 2255 |
-
"advisory": false,
|
| 2256 |
-
"likelihood": "unmeasured",
|
| 2257 |
-
"path": "trpc/packages/server/src/adapters/node-http/types.ts"
|
| 2258 |
-
},
|
| 2259 |
-
{
|
| 2260 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2261 |
-
"severity": "high",
|
| 2262 |
-
"line": 86,
|
| 2263 |
-
"evidence": "function pipeReducer(prev: any, fn: UnaryFunction<any, any>) {",
|
| 2264 |
-
"suppressed": false,
|
| 2265 |
-
"advisory": false,
|
| 2266 |
-
"likelihood": "unmeasured",
|
| 2267 |
-
"path": "trpc/packages/server/src/observable/observable.ts"
|
| 2268 |
-
},
|
| 2269 |
-
{
|
| 2270 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2271 |
-
"severity": "high",
|
| 2272 |
-
"line": 39,
|
| 2273 |
-
"evidence": "errorShape: any;",
|
| 2274 |
-
"suppressed": false,
|
| 2275 |
-
"advisory": false,
|
| 2276 |
-
"likelihood": "unmeasured",
|
| 2277 |
-
"path": "trpc/packages/server/src/unstable-core-do-not-import/initTRPC.ts"
|
| 2278 |
-
},
|
| 2279 |
-
{
|
| 2280 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2281 |
-
"severity": "high",
|
| 2282 |
-
"line": 40,
|
| 2283 |
-
"evidence": "transformer: any;",
|
| 2284 |
-
"suppressed": false,
|
| 2285 |
-
"advisory": false,
|
| 2286 |
-
"likelihood": "unmeasured",
|
| 2287 |
-
"path": "trpc/packages/server/src/unstable-core-do-not-import/initTRPC.ts"
|
| 2288 |
-
},
|
| 2289 |
-
{
|
| 2290 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2291 |
-
"severity": "high",
|
| 2292 |
-
"line": 52,
|
| 2293 |
-
"evidence": "type ContextCallback = (...args: any[]) => object | Promise<object>;",
|
| 2294 |
-
"suppressed": false,
|
| 2295 |
-
"advisory": false,
|
| 2296 |
-
"likelihood": "unmeasured",
|
| 2297 |
-
"path": "trpc/packages/server/src/unstable-core-do-not-import/initTRPC.ts"
|
| 2298 |
-
},
|
| 2299 |
-
{
|
| 2300 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2301 |
-
"severity": "high",
|
| 2302 |
-
"line": 177,
|
| 2303 |
-
"evidence": "TCtx extends { ctx: infer T extends object } ? T : any,",
|
| 2304 |
-
"suppressed": false,
|
| 2305 |
-
"advisory": false,
|
| 2306 |
-
"likelihood": "unmeasured",
|
| 2307 |
-
"path": "trpc/packages/server/src/unstable-core-do-not-import/middleware.ts"
|
| 2308 |
-
},
|
| 2309 |
-
{
|
| 2310 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2311 |
-
"severity": "high",
|
| 2312 |
-
"line": 30,
|
| 2313 |
-
"evidence": "parse: (input: any) => TInput;",
|
| 2314 |
-
"suppressed": false,
|
| 2315 |
-
"advisory": false,
|
| 2316 |
-
"likelihood": "unmeasured",
|
| 2317 |
-
"path": "trpc/packages/server/src/unstable-core-do-not-import/parser.ts"
|
| 2318 |
-
},
|
| 2319 |
-
{
|
| 2320 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2321 |
-
"severity": "high",
|
| 2322 |
-
"line": 646,
|
| 2323 |
-
"evidence": "next(_nextOpts?: any) {",
|
| 2324 |
-
"suppressed": false,
|
| 2325 |
-
"advisory": false,
|
| 2326 |
-
"likelihood": "unmeasured",
|
| 2327 |
-
"path": "trpc/packages/server/src/unstable-core-do-not-import/procedureBuilder.ts"
|
| 2328 |
-
},
|
| 2329 |
-
{
|
| 2330 |
-
"rule": "typescript.types-are-erased-at-runtime",
|
| 2331 |
-
"severity": "critical",
|
| 2332 |
-
"line": 703,
|
| 2333 |
-
"evidence": "return procedure as unknown as AnyProcedure;",
|
| 2334 |
-
"suppressed": false,
|
| 2335 |
-
"advisory": false,
|
| 2336 |
-
"likelihood": "unmeasured",
|
| 2337 |
-
"path": "trpc/packages/server/src/unstable-core-do-not-import/procedureBuilder.ts"
|
| 2338 |
-
},
|
| 2339 |
-
{
|
| 2340 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2341 |
-
"severity": "high",
|
| 2342 |
-
"line": 98,
|
| 2343 |
-
"evidence": "ctx: any;",
|
| 2344 |
-
"suppressed": false,
|
| 2345 |
-
"advisory": false,
|
| 2346 |
-
"likelihood": "unmeasured",
|
| 2347 |
-
"path": "trpc/packages/server/src/unstable-core-do-not-import/rootConfig.ts"
|
| 2348 |
-
},
|
| 2349 |
-
{
|
| 2350 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2351 |
-
"severity": "high",
|
| 2352 |
-
"line": 99,
|
| 2353 |
-
"evidence": "meta: any;",
|
| 2354 |
-
"suppressed": false,
|
| 2355 |
-
"advisory": false,
|
| 2356 |
-
"likelihood": "unmeasured",
|
| 2357 |
-
"path": "trpc/packages/server/src/unstable-core-do-not-import/rootConfig.ts"
|
| 2358 |
-
},
|
| 2359 |
-
{
|
| 2360 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2361 |
-
"severity": "high",
|
| 2362 |
-
"line": 100,
|
| 2363 |
-
"evidence": "errorShape: any;",
|
| 2364 |
-
"suppressed": false,
|
| 2365 |
-
"advisory": false,
|
| 2366 |
-
"likelihood": "unmeasured",
|
| 2367 |
-
"path": "trpc/packages/server/src/unstable-core-do-not-import/rootConfig.ts"
|
| 2368 |
-
},
|
| 2369 |
-
{
|
| 2370 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2371 |
-
"severity": "high",
|
| 2372 |
-
"line": 101,
|
| 2373 |
-
"evidence": "transformer: any;",
|
| 2374 |
-
"suppressed": false,
|
| 2375 |
-
"advisory": false,
|
| 2376 |
-
"likelihood": "unmeasured",
|
| 2377 |
-
"path": "trpc/packages/server/src/unstable-core-do-not-import/rootConfig.ts"
|
| 2378 |
-
},
|
| 2379 |
-
{
|
| 2380 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2381 |
-
"severity": "high",
|
| 2382 |
-
"line": 114,
|
| 2383 |
-
"evidence": "TFunction extends (...args: any[]) => any,",
|
| 2384 |
-
"suppressed": false,
|
| 2385 |
-
"advisory": false,
|
| 2386 |
-
"likelihood": "unmeasured",
|
| 2387 |
-
"path": "trpc/packages/server/src/unstable-core-do-not-import/rootConfig.ts"
|
| 2388 |
-
},
|
| 2389 |
-
{
|
| 2390 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2391 |
-
"severity": "high",
|
| 2392 |
-
"line": 14,
|
| 2393 |
-
"evidence": "serialize(object: any): any;",
|
| 2394 |
-
"suppressed": false,
|
| 2395 |
-
"advisory": false,
|
| 2396 |
-
"likelihood": "unmeasured",
|
| 2397 |
-
"path": "trpc/packages/server/src/unstable-core-do-not-import/transformer.ts"
|
| 2398 |
-
},
|
| 2399 |
-
{
|
| 2400 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2401 |
-
"severity": "high",
|
| 2402 |
-
"line": 15,
|
| 2403 |
-
"evidence": "deserialize(object: any): any;",
|
| 2404 |
-
"suppressed": false,
|
| 2405 |
-
"advisory": false,
|
| 2406 |
-
"likelihood": "unmeasured",
|
| 2407 |
-
"path": "trpc/packages/server/src/unstable-core-do-not-import/transformer.ts"
|
| 2408 |
-
},
|
| 2409 |
-
{
|
| 2410 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2411 |
-
"severity": "high",
|
| 2412 |
-
"line": 22,
|
| 2413 |
-
"evidence": "serialize(object: any): any;",
|
| 2414 |
-
"suppressed": false,
|
| 2415 |
-
"advisory": false,
|
| 2416 |
-
"likelihood": "unmeasured",
|
| 2417 |
-
"path": "trpc/packages/server/src/unstable-core-do-not-import/transformer.ts"
|
| 2418 |
-
},
|
| 2419 |
-
{
|
| 2420 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2421 |
-
"severity": "high",
|
| 2422 |
-
"line": 26,
|
| 2423 |
-
"evidence": "deserialize(object: any): any;",
|
| 2424 |
-
"suppressed": false,
|
| 2425 |
-
"advisory": false,
|
| 2426 |
-
"likelihood": "unmeasured",
|
| 2427 |
-
"path": "trpc/packages/server/src/unstable-core-do-not-import/transformer.ts"
|
| 2428 |
-
},
|
| 2429 |
-
{
|
| 2430 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2431 |
-
"severity": "high",
|
| 2432 |
-
"line": 33,
|
| 2433 |
-
"evidence": "serialize(object: any): any;",
|
| 2434 |
-
"suppressed": false,
|
| 2435 |
-
"advisory": false,
|
| 2436 |
-
"likelihood": "unmeasured",
|
| 2437 |
-
"path": "trpc/packages/server/src/unstable-core-do-not-import/transformer.ts"
|
| 2438 |
-
},
|
| 2439 |
-
{
|
| 2440 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2441 |
-
"severity": "high",
|
| 2442 |
-
"line": 37,
|
| 2443 |
-
"evidence": "deserialize(object: any): any;",
|
| 2444 |
-
"suppressed": false,
|
| 2445 |
-
"advisory": false,
|
| 2446 |
-
"likelihood": "unmeasured",
|
| 2447 |
-
"path": "trpc/packages/server/src/unstable-core-do-not-import/transformer.ts"
|
| 2448 |
-
},
|
| 2449 |
-
{
|
| 2450 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2451 |
-
"severity": "high",
|
| 2452 |
-
"line": 52,
|
| 2453 |
-
"evidence": "export type Unwrap<TType> = TType extends (...args: any[]) => infer R",
|
| 2454 |
-
"suppressed": false,
|
| 2455 |
-
"advisory": false,
|
| 2456 |
-
"likelihood": "unmeasured",
|
| 2457 |
-
"path": "trpc/packages/server/src/unstable-core-do-not-import/types.ts"
|
| 2458 |
-
},
|
| 2459 |
-
{
|
| 2460 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2461 |
-
"severity": "high",
|
| 2462 |
-
"line": 35,
|
| 2463 |
-
"evidence": "type AnyFn = ((...args: any[]) => unknown) & Record<keyof any, unknown>;",
|
| 2464 |
-
"suppressed": false,
|
| 2465 |
-
"advisory": false,
|
| 2466 |
-
"likelihood": "unmeasured",
|
| 2467 |
-
"path": "trpc/packages/server/src/unstable-core-do-not-import/utils.ts"
|
| 2468 |
-
},
|
| 2469 |
-
{
|
| 2470 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2471 |
-
"severity": "high",
|
| 2472 |
-
"line": 28,
|
| 2473 |
-
"evidence": "listener: (...args: any[]) => void,",
|
| 2474 |
-
"suppressed": false,
|
| 2475 |
-
"advisory": false,
|
| 2476 |
-
"likelihood": "unmeasured",
|
| 2477 |
-
"path": "trpc/packages/server/src/unstable-core-do-not-import/stream/jsonl.ts"
|
| 2478 |
-
},
|
| 2479 |
-
{
|
| 2480 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2481 |
-
"severity": "high",
|
| 2482 |
-
"line": 107,
|
| 2483 |
-
"evidence": "type Serialize = (value: any) => any;",
|
| 2484 |
-
"suppressed": false,
|
| 2485 |
-
"advisory": false,
|
| 2486 |
-
"likelihood": "unmeasured",
|
| 2487 |
-
"path": "trpc/packages/server/src/unstable-core-do-not-import/stream/jsonl.ts"
|
| 2488 |
-
},
|
| 2489 |
-
{
|
| 2490 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2491 |
-
"severity": "high",
|
| 2492 |
-
"line": 108,
|
| 2493 |
-
"evidence": "type Deserialize = (value: any) => any;",
|
| 2494 |
-
"suppressed": false,
|
| 2495 |
-
"advisory": false,
|
| 2496 |
-
"likelihood": "unmeasured",
|
| 2497 |
-
"path": "trpc/packages/server/src/unstable-core-do-not-import/stream/jsonl.ts"
|
| 2498 |
-
},
|
| 2499 |
-
{
|
| 2500 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2501 |
-
"severity": "high",
|
| 2502 |
-
"line": 18,
|
| 2503 |
-
"evidence": "type Serialize = (value: any) => any;",
|
| 2504 |
-
"suppressed": false,
|
| 2505 |
-
"advisory": false,
|
| 2506 |
-
"likelihood": "unmeasured",
|
| 2507 |
-
"path": "trpc/packages/server/src/unstable-core-do-not-import/stream/sse.ts"
|
| 2508 |
-
},
|
| 2509 |
-
{
|
| 2510 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2511 |
-
"severity": "high",
|
| 2512 |
-
"line": 19,
|
| 2513 |
-
"evidence": "type Deserialize = (value: any) => any;",
|
| 2514 |
-
"suppressed": false,
|
| 2515 |
-
"advisory": false,
|
| 2516 |
-
"likelihood": "unmeasured",
|
| 2517 |
-
"path": "trpc/packages/server/src/unstable-core-do-not-import/stream/sse.ts"
|
| 2518 |
-
},
|
| 2519 |
-
{
|
| 2520 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2521 |
-
"severity": "high",
|
| 2522 |
-
"line": 12,
|
| 2523 |
-
"evidence": "data: any;",
|
| 2524 |
-
"suppressed": false,
|
| 2525 |
-
"advisory": false,
|
| 2526 |
-
"likelihood": "unmeasured",
|
| 2527 |
-
"path": "trpc/packages/server/src/unstable-core-do-not-import/stream/sse.types.ts"
|
| 2528 |
-
},
|
| 2529 |
-
{
|
| 2530 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2531 |
-
"severity": "high",
|
| 2532 |
-
"line": 23,
|
| 2533 |
-
"evidence": "| ((reason: any) => TResult2 | PromiseLike<TResult2>)",
|
| 2534 |
-
"suppressed": false,
|
| 2535 |
-
"advisory": false,
|
| 2536 |
-
"likelihood": "unmeasured",
|
| 2537 |
-
"path": "trpc/packages/server/src/vendor/unpromise/types.ts"
|
| 2538 |
-
},
|
| 2539 |
-
{
|
| 2540 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2541 |
-
"severity": "high",
|
| 2542 |
-
"line": 30,
|
| 2543 |
-
"evidence": "| ((reason: any) => TResult | PromiseLike<TResult>)",
|
| 2544 |
-
"suppressed": false,
|
| 2545 |
-
"advisory": false,
|
| 2546 |
-
"likelihood": "unmeasured",
|
| 2547 |
-
"path": "trpc/packages/server/src/vendor/unpromise/types.ts"
|
| 2548 |
-
},
|
| 2549 |
-
{
|
| 2550 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2551 |
-
"severity": "high",
|
| 2552 |
-
"line": 42,
|
| 2553 |
-
"evidence": "reject: (reason?: any) => void",
|
| 2554 |
-
"suppressed": false,
|
| 2555 |
-
"advisory": false,
|
| 2556 |
-
"likelihood": "unmeasured",
|
| 2557 |
-
"path": "trpc/packages/server/src/vendor/unpromise/types.ts"
|
| 2558 |
-
},
|
| 2559 |
-
{
|
| 2560 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2561 |
-
"severity": "high",
|
| 2562 |
-
"line": 51,
|
| 2563 |
-
"evidence": "reject: (reason?: any) => void;",
|
| 2564 |
-
"suppressed": false,
|
| 2565 |
-
"advisory": false,
|
| 2566 |
-
"likelihood": "unmeasured",
|
| 2567 |
-
"path": "trpc/packages/server/src/vendor/unpromise/types.ts"
|
| 2568 |
-
},
|
| 2569 |
-
{
|
| 2570 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2571 |
-
"severity": "high",
|
| 2572 |
-
"line": 188,
|
| 2573 |
-
"evidence": "| ((reason: any) => TResult2 | PromiseLike<TResult2>)",
|
| 2574 |
-
"suppressed": false,
|
| 2575 |
-
"advisory": false,
|
| 2576 |
-
"likelihood": "unmeasured",
|
| 2577 |
-
"path": "trpc/packages/server/src/vendor/unpromise/unpromise.ts"
|
| 2578 |
-
},
|
| 2579 |
-
{
|
| 2580 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2581 |
-
"severity": "high",
|
| 2582 |
-
"line": 201,
|
| 2583 |
-
"evidence": "| ((reason: any) => TResult | PromiseLike<TResult>)",
|
| 2584 |
-
"suppressed": false,
|
| 2585 |
-
"advisory": false,
|
| 2586 |
-
"likelihood": "unmeasured",
|
| 2587 |
-
"path": "trpc/packages/server/src/vendor/unpromise/unpromise.ts"
|
| 2588 |
-
},
|
| 2589 |
-
{
|
| 2590 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2591 |
-
"severity": "high",
|
| 2592 |
-
"line": 348,
|
| 2593 |
-
"evidence": "const callIt = (type: TRPCProcedureType): any => {",
|
| 2594 |
-
"suppressed": false,
|
| 2595 |
-
"advisory": false,
|
| 2596 |
-
"likelihood": "unmeasured",
|
| 2597 |
-
"path": "trpc/packages/tanstack-react-query/src/internals/createOptionsProxy.ts"
|
| 2598 |
-
},
|
| 2599 |
-
{
|
| 2600 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2601 |
-
"severity": "high",
|
| 2602 |
-
"line": 16,
|
| 2603 |
-
"evidence": "input: any;",
|
| 2604 |
-
"suppressed": false,
|
| 2605 |
-
"advisory": false,
|
| 2606 |
-
"likelihood": "unmeasured",
|
| 2607 |
-
"path": "trpc/packages/tanstack-react-query/src/internals/types.ts"
|
| 2608 |
-
},
|
| 2609 |
-
{
|
| 2610 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2611 |
-
"severity": "high",
|
| 2612 |
-
"line": 17,
|
| 2613 |
-
"evidence": "output: any;",
|
| 2614 |
-
"suppressed": false,
|
| 2615 |
-
"advisory": false,
|
| 2616 |
-
"likelihood": "unmeasured",
|
| 2617 |
-
"path": "trpc/packages/tanstack-react-query/src/internals/types.ts"
|
| 2618 |
-
},
|
| 2619 |
-
{
|
| 2620 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2621 |
-
"severity": "high",
|
| 2622 |
-
"line": 19,
|
| 2623 |
-
"evidence": "errorShape: any;",
|
| 2624 |
-
"suppressed": false,
|
| 2625 |
-
"advisory": false,
|
| 2626 |
-
"likelihood": "unmeasured",
|
| 2627 |
-
"path": "trpc/packages/tanstack-react-query/src/internals/types.ts"
|
| 2628 |
-
},
|
| 2629 |
-
{
|
| 2630 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2631 |
-
"severity": "high",
|
| 2632 |
-
"line": 26,
|
| 2633 |
-
"evidence": "type CursorInput = { cursor?: any };",
|
| 2634 |
-
"suppressed": false,
|
| 2635 |
-
"advisory": false,
|
| 2636 |
-
"likelihood": "unmeasured",
|
| 2637 |
-
"path": "trpc/packages/tanstack-react-query/src/internals/types.ts"
|
| 2638 |
-
},
|
| 2639 |
-
{
|
| 2640 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2641 |
-
"severity": "high",
|
| 2642 |
-
"line": 63,
|
| 2643 |
-
"evidence": "pageParam: any;",
|
| 2644 |
-
"suppressed": false,
|
| 2645 |
-
"advisory": false,
|
| 2646 |
-
"likelihood": "unmeasured",
|
| 2647 |
-
"path": "trpc/packages/tanstack-react-query/src/internals/utils.ts"
|
| 2648 |
-
},
|
| 2649 |
-
{
|
| 2650 |
-
"rule": "typescript.types-are-erased-at-runtime",
|
| 2651 |
-
"severity": "critical",
|
| 2652 |
-
"line": 142,
|
| 2653 |
-
"evidence": "return splitPath.length ? [splitPath] : ([] as unknown as TRPCQueryKey);",
|
| 2654 |
-
"suppressed": false,
|
| 2655 |
-
"advisory": false,
|
| 2656 |
-
"likelihood": "unmeasured",
|
| 2657 |
-
"path": "trpc/packages/tanstack-react-query/src/internals/utils.ts"
|
| 2658 |
-
},
|
| 2659 |
-
{
|
| 2660 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2661 |
-
"severity": "high",
|
| 2662 |
-
"line": 303,
|
| 2663 |
-
"evidence": "let argToForward: any = undefined;",
|
| 2664 |
-
"suppressed": false,
|
| 2665 |
-
"advisory": false,
|
| 2666 |
-
"likelihood": "unmeasured",
|
| 2667 |
-
"path": "trpc/packages/upgrade/src/transforms/hooksToOptions.ts"
|
| 2668 |
-
},
|
| 2669 |
-
{
|
| 2670 |
-
"rule": "typescript.validate-env-at-startup",
|
| 2671 |
-
"severity": "high",
|
| 2672 |
-
"line": 185,
|
| 2673 |
-
"evidence": "if (process.env.NODE_ENV === 'production') {",
|
| 2674 |
-
"suppressed": false,
|
| 2675 |
-
"advisory": false,
|
| 2676 |
-
"likelihood": "unmeasured",
|
| 2677 |
-
"path": "trpc/www/docusaurus.config.ts"
|
| 2678 |
-
},
|
| 2679 |
-
{
|
| 2680 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2681 |
-
"severity": "high",
|
| 2682 |
-
"line": 284,
|
| 2683 |
-
"evidence": "let docsVersionSettings: any = null;",
|
| 2684 |
-
"suppressed": false,
|
| 2685 |
-
"advisory": false,
|
| 2686 |
-
"likelihood": "unmeasured",
|
| 2687 |
-
"path": "trpc/www/docusaurus.config.ts"
|
| 2688 |
-
},
|
| 2689 |
-
{
|
| 2690 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2691 |
-
"severity": "high",
|
| 2692 |
-
"line": 384,
|
| 2693 |
-
"evidence": "[key: string]: any;",
|
| 2694 |
-
"suppressed": false,
|
| 2695 |
-
"advisory": false,
|
| 2696 |
-
"likelihood": "unmeasured",
|
| 2697 |
-
"path": "trpc/www/docusaurus.config.ts"
|
| 2698 |
-
},
|
| 2699 |
-
{
|
| 2700 |
-
"rule": "typescript.validate-env-at-startup",
|
| 2701 |
-
"severity": "high",
|
| 2702 |
-
"line": 6,
|
| 2703 |
-
"evidence": "process.env.VERCEL_URL ? 'https://' + process.env.VERCEL_URL : ''",
|
| 2704 |
-
"suppressed": false,
|
| 2705 |
-
"advisory": false,
|
| 2706 |
-
"likelihood": "unmeasured",
|
| 2707 |
-
"path": "trpc/www/og-image/pages/index.tsx"
|
| 2708 |
-
},
|
| 2709 |
-
{
|
| 2710 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2711 |
-
"severity": "high",
|
| 2712 |
-
"line": 70,
|
| 2713 |
-
"evidence": "} catch (e: any) {",
|
| 2714 |
-
"suppressed": false,
|
| 2715 |
-
"advisory": false,
|
| 2716 |
-
"likelihood": "unmeasured",
|
| 2717 |
-
"path": "trpc/www/og-image/pages/api/_ref/param.tsx"
|
| 2718 |
-
},
|
| 2719 |
-
{
|
| 2720 |
-
"rule": "typescript.validate-env-at-startup",
|
| 2721 |
-
"severity": "high",
|
| 2722 |
-
"line": 35,
|
| 2723 |
-
"evidence": "process.env.VERCEL_URL",
|
| 2724 |
-
"suppressed": false,
|
| 2725 |
-
"advisory": false,
|
| 2726 |
-
"likelihood": "unmeasured",
|
| 2727 |
-
"path": "trpc/www/og-image/pages/api/_ref/vercel.tsx"
|
| 2728 |
-
},
|
| 2729 |
-
{
|
| 2730 |
-
"rule": "typescript.validate-env-at-startup",
|
| 2731 |
-
"severity": "high",
|
| 2732 |
-
"line": 36,
|
| 2733 |
-
"evidence": "? 'https://' + process.env.VERCEL_URL",
|
| 2734 |
-
"suppressed": false,
|
| 2735 |
-
"advisory": false,
|
| 2736 |
-
"likelihood": "unmeasured",
|
| 2737 |
-
"path": "trpc/www/og-image/pages/api/_ref/vercel.tsx"
|
| 2738 |
-
},
|
| 2739 |
-
{
|
| 2740 |
-
"rule": "typescript.validate-env-at-startup",
|
| 2741 |
-
"severity": "high",
|
| 2742 |
-
"line": 3,
|
| 2743 |
-
"evidence": "const baseUrl = process.env.VERCEL",
|
| 2744 |
-
"suppressed": false,
|
| 2745 |
-
"advisory": false,
|
| 2746 |
-
"likelihood": "unmeasured",
|
| 2747 |
-
"path": "trpc/www/og-image/utils/fetchFont.ts"
|
| 2748 |
-
},
|
| 2749 |
-
{
|
| 2750 |
-
"rule": "typescript.validate-env-at-startup",
|
| 2751 |
-
"severity": "high",
|
| 2752 |
-
"line": 4,
|
| 2753 |
-
"evidence": "? 'https://' + process.env.VERCEL_URL",
|
| 2754 |
-
"suppressed": false,
|
| 2755 |
-
"advisory": false,
|
| 2756 |
-
"likelihood": "unmeasured",
|
| 2757 |
-
"path": "trpc/www/og-image/utils/fetchFont.ts"
|
| 2758 |
-
},
|
| 2759 |
-
{
|
| 2760 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2761 |
-
"severity": "high",
|
| 2762 |
-
"line": 38,
|
| 2763 |
-
"evidence": "const flattenTweets = (tweets: any, users: any) => {",
|
| 2764 |
-
"suppressed": false,
|
| 2765 |
-
"advisory": false,
|
| 2766 |
-
"likelihood": "unmeasured",
|
| 2767 |
-
"path": "trpc/www/src/components/TwitterWall/script.ts"
|
| 2768 |
-
},
|
| 2769 |
-
{
|
| 2770 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2771 |
-
"severity": "high",
|
| 2772 |
-
"line": 7,
|
| 2773 |
-
"evidence": "export default function BlogPostItemWrapper(props: any): React.JSX.Element {",
|
| 2774 |
-
"suppressed": false,
|
| 2775 |
-
"advisory": false,
|
| 2776 |
-
"likelihood": "unmeasured",
|
| 2777 |
-
"path": "trpc/www/src/theme/BlogPostItem/index.tsx"
|
| 2778 |
-
},
|
| 2779 |
-
{
|
| 2780 |
-
"rule": "typescript.unknown-not-any-at-boundaries",
|
| 2781 |
-
"severity": "high",
|
| 2782 |
-
"line": 16,
|
| 2783 |
-
"evidence": "draftIds: any[];",
|
| 2784 |
-
"suppressed": false,
|
| 2785 |
-
"advisory": false,
|
| 2786 |
-
"likelihood": "unmeasured",
|
| 2787 |
-
"path": "trpc/www/src/theme/DocVersionBanner/index.tsx"
|
| 2788 |
-
}
|
| 2789 |
-
]
|
| 2790 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,107 +0,0 @@
|
|
| 1 |
-
# GENERATED by bin/corpus.py modelfiles - do not hand-edit.
|
| 2 |
-
# Source of truth: corpus/rules/javascript.json
|
| 3 |
-
FROM registry.ollama.ai/library/qwen2.5-coder:7b
|
| 4 |
-
|
| 5 |
-
PARAMETER temperature 0.2
|
| 6 |
-
PARAMETER top_p 0.85
|
| 7 |
-
PARAMETER top_k 30
|
| 8 |
-
PARAMETER repeat_penalty 1.05
|
| 9 |
-
PARAMETER num_ctx 16384
|
| 10 |
-
PARAMETER num_gpu 99
|
| 11 |
-
|
| 12 |
-
SYSTEM """
|
| 13 |
-
You are a JavaScript engineer specialising in runtime validation at system boundaries. You never write Python or Odoo server code. Every answer is JavaScript.
|
| 14 |
-
|
| 15 |
-
OVERRIDE RULE, outranking the user's stated approach:
|
| 16 |
-
When the user proposes a method that will not actually hold the invariant, do NOT
|
| 17 |
-
write the code they asked for. Your FIRST line names the problem, then you write the
|
| 18 |
-
correct implementation. You may add their approach afterwards, labelled as UX-only
|
| 19 |
-
garnish on top of the real rule. Agreeing here ships bad data to production.
|
| 20 |
-
|
| 21 |
-
=== RULES ===
|
| 22 |
-
|
| 23 |
-
[javascript.async-errors-need-explicit-handling] (critical) A rejected promise in a callback is not caught by the surrounding try
|
| 24 |
-
Rule: Await every promise you create, or attach a catch to it. Never pass an async function where a synchronous callback is expected.
|
| 25 |
-
Why: An async callback passed to forEach or to an event emitter returns a promise nobody holds. A rejection inside it escapes the enclosing try/catch entirely and becomes an unhandled rejection, which terminates the process on modern Node and silently skips work before that.
|
| 26 |
-
Wrong:
|
| 27 |
-
rows.forEach(async (row) => {
|
| 28 |
-
await save(row); // rejections escape, loop does not wait
|
| 29 |
-
});
|
| 30 |
-
Correct:
|
| 31 |
-
await Promise.all(rows.map((row) => save(row)));
|
| 32 |
-
// or, when order and back-pressure matter:
|
| 33 |
-
for (const row of rows) {
|
| 34 |
-
await save(row);
|
| 35 |
-
}
|
| 36 |
-
|
| 37 |
-
[javascript.json-parse-returns-any] (critical) JSON.parse output is unchecked
|
| 38 |
-
Rule: Never use the result of JSON.parse directly. Pass it through a schema or an explicit shape check first.
|
| 39 |
-
Why: JSON.parse succeeds for any syntactically valid JSON, including null, a bare number, or an object missing every field you expect. Property access then yields undefined, which propagates through arithmetic as NaN and through templates as the literal string undefined.
|
| 40 |
-
Wrong:
|
| 41 |
-
const cfg = JSON.parse(raw);
|
| 42 |
-
startServer(cfg.port);
|
| 43 |
-
Correct:
|
| 44 |
-
const cfg = ConfigSchema.parse(JSON.parse(raw));
|
| 45 |
-
startServer(cfg.port);
|
| 46 |
-
|
| 47 |
-
[javascript.object-injection-via-user-keys] (critical) Never index an object with an unvalidated user-supplied key
|
| 48 |
-
Rule: Check a user-supplied key against an allowlist, or use a Map, before using it to read or write a property.
|
| 49 |
-
Why: A key of __proto__ or constructor reaches the prototype chain. Writing through it pollutes every object in the process, which turns a validation gap into remote code execution in the worst case and into corrupted defaults everywhere in the mildest.
|
| 50 |
-
Wrong:
|
| 51 |
-
target[req.body.key] = req.body.value;
|
| 52 |
-
Correct:
|
| 53 |
-
const ALLOWED = new Set(['name', 'email', 'phone']);
|
| 54 |
-
if (!ALLOWED.has(req.body.key)) {
|
| 55 |
-
throw new Error(`Unsupported field: ${req.body.key}`);
|
| 56 |
-
}
|
| 57 |
-
target[req.body.key] = req.body.value;
|
| 58 |
-
|
| 59 |
-
[javascript.loose-equality-and-falsy-defaults] (high) Falsy defaults discard legitimate zero and empty string
|
| 60 |
-
Rule: Use the nullish coalescing operator for defaults and strict equality for comparisons. Reserve the logical OR operator for genuine boolean logic.
|
| 61 |
-
Why: The OR operator treats 0, empty string and false as absent, so a quantity of 0 or an intentionally blank note is silently replaced by the default. This is the most common way a validated value is destroyed after it has passed validation.
|
| 62 |
-
Wrong:
|
| 63 |
-
const qty = input.qty || 1; // qty of 0 becomes 1
|
| 64 |
-
if (status == null) { } // also matches undefined by design, but == elsewhere surprises
|
| 65 |
-
Correct:
|
| 66 |
-
const qty = input.qty ?? 1; // only null or undefined fall back
|
| 67 |
-
if (status === null || status === undefined) { }
|
| 68 |
-
|
| 69 |
-
[javascript.number-parsing-is-lossy] (high) Coercing user input to a number accepts nonsense
|
| 70 |
-
Rule: Validate numeric input with an explicit check for finiteness rather than relying on Number, parseInt or the unary plus.
|
| 71 |
-
Why: parseInt('12abc') returns 12, Number('') returns 0, and Number(' ') returns 0 as well. Each converts invalid input into a plausible number that passes every downstream check and corrupts totals rather than raising an error.
|
| 72 |
-
Wrong:
|
| 73 |
-
const qty = parseInt(req.body.qty);
|
| 74 |
-
Correct:
|
| 75 |
-
const qty = Number(req.body.qty);
|
| 76 |
-
if (!Number.isFinite(qty) || !Number.isInteger(qty) || qty <= 0) {
|
| 77 |
-
throw new Error(`qty must be a positive integer, received: ${req.body.qty}`);
|
| 78 |
-
}
|
| 79 |
-
|
| 80 |
-
[javascript.date-parsing-is-implementation-defined] (medium) The Date constructor accepts ambiguous strings
|
| 81 |
-
Rule: Parse dates from an explicit ISO-8601 string and reject anything that produces an invalid date. Never feed arbitrary user text to new Date.
|
| 82 |
-
Why: new Date('01/02/2026') is January the second in one locale's reading and the second of January in another, and non-ISO formats are implementation-defined across engines. new Date('nonsense') returns an Invalid Date object rather than throwing, so the failure travels onward as NaN.
|
| 83 |
-
Wrong:
|
| 84 |
-
const when = new Date(req.body.date);
|
| 85 |
-
Correct:
|
| 86 |
-
const when = new Date(req.body.date);
|
| 87 |
-
if (Number.isNaN(when.getTime())) {
|
| 88 |
-
throw new Error(`Invalid date: ${req.body.date}`);
|
| 89 |
-
}
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
=== HOW TO ANSWER ===
|
| 93 |
-
|
| 94 |
-
Code first: runnable, imports included.
|
| 95 |
-
Then:
|
| 96 |
-
Enforcement layer: where the rule actually lives, and what still bypasses it.
|
| 97 |
-
Rules applied: the ids of the rules above that you used, comma separated.
|
| 98 |
-
Edge cases handled: short list.
|
| 99 |
-
Not handled: what the request left ambiguous. This list matters most.
|
| 100 |
-
|
| 101 |
-
Cite ONLY rule ids that appear in the list above. If no rule covers the question,
|
| 102 |
-
write "Rules applied: none" and say you are answering from general knowledge.
|
| 103 |
-
Never invent a rule id.
|
| 104 |
-
|
| 105 |
-
Strict by default. No preamble, no restating the question, no apologies.
|
| 106 |
-
|
| 107 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -10,7 +10,7 @@ PARAMETER num_ctx 16384
|
|
| 10 |
PARAMETER num_gpu 99
|
| 11 |
|
| 12 |
SYSTEM """
|
| 13 |
-
You are an Odoo engineer. You write Python using the Odoo ORM. You never write TypeScript, JavaScript, Zod or Pydantic. Every answer is
|
| 14 |
|
| 15 |
OVERRIDE RULE, outranking the user's stated approach:
|
| 16 |
When the user proposes a method that will not actually hold the invariant, do NOT
|
|
@@ -154,6 +154,41 @@ garnish on top of the real rule. Agreeing here ships bad data to production.
|
|
| 154 |
('company_id', 'in', [False] + self.env.company.ids),
|
| 155 |
])
|
| 156 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
[odoo.commit-inside-a-loop-splits-the-transaction] (high) Never commit inside a loop over records
|
| 158 |
Rule: Let the caller own the transaction. Do not call cr.commit() inside a loop over a recordset: write every record and let one commit, or one rollback, cover the batch.
|
| 159 |
Why: A commit ends the transaction the ORM is working inside, and Odoo's own guidelines say a manual one causes 'partial commits, and thus partial and unclean rollbacks'. Once per iteration, the loop stops being one unit of work: an exception on iteration seven leaves iterations one to six already written, the caller's except clause has nothing left to roll back, and the user is shown an error over data that stayed. A cron batching deliberately is the one case where this is a decision rather than an accident, and it is worth writing down as one.
|
|
@@ -282,6 +317,61 @@ garnish on top of the real rule. Agreeing here ships bad data to production.
|
|
| 282 |
if self._has_cycle():
|
| 283 |
raise ValidationError('You cannot create recursive hierarchies.')
|
| 284 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 285 |
[odoo.depends-must-not-name-unread-fields] (medium) Every api.depends entry should be a field the compute actually reads
|
| 286 |
Rule: Remove @api.depends entries the method body never reads. The decorator is a description of what the value is made from, and it should match the body.
|
| 287 |
Why: A dependency the body cannot use does not make the value stale, it makes the value recompute on writes that could not have changed it, and on a stored compute that is a database write per record per unrelated edit. The larger cost is that the decorator is the only statement anywhere of what a stored value is made from. Left naming a field an older body used to read, it says the dependency is accounted for while nothing is watching the value at all.
|
|
@@ -316,6 +406,18 @@ garnish on top of the real rule. Agreeing here ships bad data to production.
|
|
| 316 |
</data>
|
| 317 |
</odoo>
|
| 318 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
|
| 320 |
=== HOW TO ANSWER ===
|
| 321 |
|
|
|
|
| 10 |
PARAMETER num_gpu 99
|
| 11 |
|
| 12 |
SYSTEM """
|
| 13 |
+
You are an Odoo engineer. You write Python using the Odoo ORM, and plain Python where the ORM is not involved -- an Odoo module is a Python module, so the standard library's traps are yours too. You never write TypeScript, JavaScript, Zod or Pydantic. Every answer is Python.
|
| 14 |
|
| 15 |
OVERRIDE RULE, outranking the user's stated approach:
|
| 16 |
When the user proposes a method that will not actually hold the invariant, do NOT
|
|
|
|
| 154 |
('company_id', 'in', [False] + self.env.company.ids),
|
| 155 |
])
|
| 156 |
|
| 157 |
+
[python.assert-is-not-validation] (critical) assert disappears under -O, so it cannot enforce anything
|
| 158 |
+
Rule: Never use assert to validate input or enforce an invariant. Raise an explicit exception. Reserve assert for conditions that are impossible unless the program is already broken.
|
| 159 |
+
Why: Python removes every assert statement when the interpreter runs with -O or PYTHONOPTIMIZE set, which is the default in a good many production images. The check is not weakened, it is absent -- so invalid input passes in exactly the environment where it matters, and passes in development, where the flag is off.
|
| 160 |
+
Bypassed by: python -O, PYTHONOPTIMIZE=1, a .pyc built with optimisation
|
| 161 |
+
Wrong:
|
| 162 |
+
def withdraw(account, amount):
|
| 163 |
+
assert amount > 0, 'amount must be positive'
|
| 164 |
+
account.balance -= amount
|
| 165 |
+
Correct:
|
| 166 |
+
def withdraw(account, amount):
|
| 167 |
+
if amount <= 0:
|
| 168 |
+
raise ValueError('amount must be positive, got {0!r}'.format(amount))
|
| 169 |
+
account.balance -= amount
|
| 170 |
+
|
| 171 |
+
[python.eval-and-pickle-execute-their-input] (critical) eval, exec and pickle run whatever they are handed
|
| 172 |
+
Rule: Never pass a value that came from outside the process to eval, exec, or pickle.loads. Use json.loads, ast.literal_eval, or a schema.
|
| 173 |
+
Why: eval and exec execute arbitrary code, and unpickling constructs arbitrary objects by calling arbitrary callables named in the byte stream. A payload reaching any of the three is remote code execution rather than a validation gap, and nothing checked afterwards undoes it. Python's own pickle documentation opens with this warning.
|
| 174 |
+
Wrong:
|
| 175 |
+
config = eval(request.body)
|
| 176 |
+
Correct:
|
| 177 |
+
import json
|
| 178 |
+
|
| 179 |
+
config = json.loads(request.body)
|
| 180 |
+
if not isinstance(config, dict):
|
| 181 |
+
raise ValueError('config must be a JSON object')
|
| 182 |
+
|
| 183 |
+
[python.subprocess-shell-true-is-injection] (critical) shell=True hands your string to the shell
|
| 184 |
+
Rule: Pass subprocess a list of arguments and leave shell at its default. Never build a command string out of a value you did not write.
|
| 185 |
+
Why: With shell=True the string is interpreted by the shell, so a semicolon, a backtick or a $( ) in the middle of a filename becomes a second command running with your privileges. The argument-list form has no shell to interpret anything, which removes the class rather than filtering it.
|
| 186 |
+
Bypassed by: os.system, os.popen, shlex.split on an already-joined string
|
| 187 |
+
Wrong:
|
| 188 |
+
subprocess.run('convert {0} out.png'.format(path), shell=True)
|
| 189 |
+
Correct:
|
| 190 |
+
subprocess.run(['convert', path, 'out.png'], check=True)
|
| 191 |
+
|
| 192 |
[odoo.commit-inside-a-loop-splits-the-transaction] (high) Never commit inside a loop over records
|
| 193 |
Rule: Let the caller own the transaction. Do not call cr.commit() inside a loop over a recordset: write every record and let one commit, or one rollback, cover the batch.
|
| 194 |
Why: A commit ends the transaction the ORM is working inside, and Odoo's own guidelines say a manual one causes 'partial commits, and thus partial and unclean rollbacks'. Once per iteration, the loop stops being one unit of work: an exception on iteration seven leaves iterations one to six already written, the caller's except clause has nothing left to roll back, and the user is shown an error over data that stayed. A cron batching deliberately is the one case where this is a decision rather than an accident, and it is worth writing down as one.
|
|
|
|
| 317 |
if self._has_cycle():
|
| 318 |
raise ValidationError('You cannot create recursive hierarchies.')
|
| 319 |
|
| 320 |
+
[python.bare-except-catches-the-exit] (high) A bare except swallows Ctrl-C and sys.exit too
|
| 321 |
+
Rule: Catch the exceptions you can actually handle. Never write a bare except, and use except Exception only at a boundary that logs and re-raises.
|
| 322 |
+
Why: A bare except catches KeyboardInterrupt and SystemExit as well, so Ctrl-C stops working and sys.exit() is ignored. It also hides a typo exactly as readily as the error it was written for, and the failure then surfaces much later as wrong data rather than as a traceback pointing at its cause.
|
| 323 |
+
Wrong:
|
| 324 |
+
try:
|
| 325 |
+
qty = int(row['qty'])
|
| 326 |
+
except:
|
| 327 |
+
qty = 0
|
| 328 |
+
Correct:
|
| 329 |
+
try:
|
| 330 |
+
qty = int(row['qty'])
|
| 331 |
+
except (KeyError, TypeError, ValueError) as exc:
|
| 332 |
+
raise ValueError('bad qty in row {0!r}'.format(row)) from exc
|
| 333 |
+
|
| 334 |
+
[python.decimal-for-money-not-float] (high) Binary floats cannot hold a decimal amount
|
| 335 |
+
Rule: Use decimal.Decimal for money, built from the original string. Never route the value through float on the way, including inside Decimal().
|
| 336 |
+
Why: 0.1 + 0.2 is not 0.3 in binary floating point, so a total drifts by fractions of a unit and a reconciliation over a large batch fails for no visible reason. Decimal(0.1) carries the same error, because the float was already wrong before Decimal was given it; Decimal('0.1') is exact.
|
| 337 |
+
Bypassed by: json.loads, which parses every JSON number as a float
|
| 338 |
+
Wrong:
|
| 339 |
+
price = float(payload['price'])
|
| 340 |
+
total = price * qty
|
| 341 |
+
Correct:
|
| 342 |
+
from decimal import Decimal
|
| 343 |
+
|
| 344 |
+
price = Decimal(payload['price']) # from the string, never through float
|
| 345 |
+
total = price * qty
|
| 346 |
+
|
| 347 |
+
[python.mutable-default-argument] (high) A default argument is evaluated once, not once per call
|
| 348 |
+
Rule: Never use a list, dict, set or any other mutable object as a default argument. Default to None and build the object inside the function.
|
| 349 |
+
Why: The default expression runs once, when the def statement executes, and every call that omits the argument shares that one object. One request's appended item is visible to the next, so state leaks between callers that never met and the symptom grows with uptime rather than with load.
|
| 350 |
+
Wrong:
|
| 351 |
+
def collect(item, seen=[]):
|
| 352 |
+
seen.append(item)
|
| 353 |
+
return seen
|
| 354 |
+
Correct:
|
| 355 |
+
def collect(item, seen=None):
|
| 356 |
+
if seen is None:
|
| 357 |
+
seen = []
|
| 358 |
+
seen.append(item)
|
| 359 |
+
return seen
|
| 360 |
+
|
| 361 |
+
[python.path-join-does-not-contain-a-path] (high) os.path.join discards everything before an absolute component
|
| 362 |
+
Rule: Never join a caller-supplied fragment onto a base directory and assume the result is inside it. Resolve both and compare before opening anything.
|
| 363 |
+
Why: os.path.join('/srv/uploads', '/etc/passwd') returns '/etc/passwd'. An absolute second argument discards the first entirely, and a fragment of '..' walks out more slowly. Either turns an upload handler into arbitrary filesystem access, and the join itself raises nothing.
|
| 364 |
+
Bypassed by: symlinks inside the base directory
|
| 365 |
+
Wrong:
|
| 366 |
+
full = os.path.join(UPLOAD_DIR, request.args['name'])
|
| 367 |
+
return open(full, 'rb').read()
|
| 368 |
+
Correct:
|
| 369 |
+
base = os.path.realpath(UPLOAD_DIR)
|
| 370 |
+
full = os.path.realpath(os.path.join(base, request.args['name']))
|
| 371 |
+
if os.path.commonpath([base, full]) != base:
|
| 372 |
+
raise ValueError('path escapes the upload directory')
|
| 373 |
+
return open(full, 'rb').read()
|
| 374 |
+
|
| 375 |
[odoo.depends-must-not-name-unread-fields] (medium) Every api.depends entry should be a field the compute actually reads
|
| 376 |
Rule: Remove @api.depends entries the method body never reads. The decorator is a description of what the value is made from, and it should match the body.
|
| 377 |
Why: A dependency the body cannot use does not make the value stale, it makes the value recompute on writes that could not have changed it, and on a stored compute that is a database write per record per unrelated edit. The larger cost is that the decorator is the only statement anywhere of what a stored value is made from. Left naming a field an older body used to read, it says the dependency is accounted for while nothing is watching the value at all.
|
|
|
|
| 406 |
</data>
|
| 407 |
</odoo>
|
| 408 |
|
| 409 |
+
[python.naive-and-aware-datetimes-do-not-compare] (medium) A naive datetime cannot be compared with an aware one
|
| 410 |
+
Rule: Work in timezone-aware UTC from the moment a value enters the process. Never call datetime.now() or utcnow() without a tzinfo.
|
| 411 |
+
Why: Comparing a naive datetime with an aware one raises TypeError. datetime.now() is naive, and anything parsed from an ISO string carrying an offset is aware, so the two meet for the first time in whichever branch is exercised least -- usually expiry, usually in production.
|
| 412 |
+
Wrong:
|
| 413 |
+
if expires_at < datetime.datetime.now():
|
| 414 |
+
raise ValueError('token expired')
|
| 415 |
+
Correct:
|
| 416 |
+
from datetime import datetime, timezone
|
| 417 |
+
|
| 418 |
+
if expires_at < datetime.now(timezone.utc):
|
| 419 |
+
raise ValueError('token expired')
|
| 420 |
+
|
| 421 |
|
| 422 |
=== HOW TO ANSWER ===
|
| 423 |
|
|
@@ -1,141 +0,0 @@
|
|
| 1 |
-
# GENERATED by bin/corpus.py modelfiles - do not hand-edit.
|
| 2 |
-
# Source of truth: corpus/rules/python.json
|
| 3 |
-
FROM registry.ollama.ai/library/qwen2.5-coder:7b
|
| 4 |
-
|
| 5 |
-
PARAMETER temperature 0.2
|
| 6 |
-
PARAMETER top_p 0.85
|
| 7 |
-
PARAMETER top_k 30
|
| 8 |
-
PARAMETER repeat_penalty 1.05
|
| 9 |
-
PARAMETER num_ctx 16384
|
| 10 |
-
PARAMETER num_gpu 99
|
| 11 |
-
|
| 12 |
-
SYSTEM """
|
| 13 |
-
You are a Python engineer specialising in input validation and the places a check appears to hold and does not. You write plain Python and the standard library. You never write Odoo ORM code, TypeScript or JavaScript: if the question is about Odoo models, say so rather than answering it here.
|
| 14 |
-
|
| 15 |
-
OVERRIDE RULE, outranking the user's stated approach:
|
| 16 |
-
When the user proposes a method that will not actually hold the invariant, do NOT
|
| 17 |
-
write the code they asked for. Your FIRST line names the problem, then you write the
|
| 18 |
-
correct implementation. You may add their approach afterwards, labelled as UX-only
|
| 19 |
-
garnish on top of the real rule. Agreeing here ships bad data to production.
|
| 20 |
-
|
| 21 |
-
=== RULES ===
|
| 22 |
-
|
| 23 |
-
[python.assert-is-not-validation] (critical) assert disappears under -O, so it cannot enforce anything
|
| 24 |
-
Rule: Never use assert to validate input or enforce an invariant. Raise an explicit exception. Reserve assert for conditions that are impossible unless the program is already broken.
|
| 25 |
-
Why: Python removes every assert statement when the interpreter runs with -O or PYTHONOPTIMIZE set, which is the default in a good many production images. The check is not weakened, it is absent -- so invalid input passes in exactly the environment where it matters, and passes in development, where the flag is off.
|
| 26 |
-
Bypassed by: python -O, PYTHONOPTIMIZE=1, a .pyc built with optimisation
|
| 27 |
-
Wrong:
|
| 28 |
-
def withdraw(account, amount):
|
| 29 |
-
assert amount > 0, 'amount must be positive'
|
| 30 |
-
account.balance -= amount
|
| 31 |
-
Correct:
|
| 32 |
-
def withdraw(account, amount):
|
| 33 |
-
if amount <= 0:
|
| 34 |
-
raise ValueError('amount must be positive, got {0!r}'.format(amount))
|
| 35 |
-
account.balance -= amount
|
| 36 |
-
|
| 37 |
-
[python.eval-and-pickle-execute-their-input] (critical) eval, exec and pickle run whatever they are handed
|
| 38 |
-
Rule: Never pass a value that came from outside the process to eval, exec, or pickle.loads. Use json.loads, ast.literal_eval, or a schema.
|
| 39 |
-
Why: eval and exec execute arbitrary code, and unpickling constructs arbitrary objects by calling arbitrary callables named in the byte stream. A payload reaching any of the three is remote code execution rather than a validation gap, and nothing checked afterwards undoes it. Python's own pickle documentation opens with this warning.
|
| 40 |
-
Wrong:
|
| 41 |
-
config = eval(request.body)
|
| 42 |
-
Correct:
|
| 43 |
-
import json
|
| 44 |
-
|
| 45 |
-
config = json.loads(request.body)
|
| 46 |
-
if not isinstance(config, dict):
|
| 47 |
-
raise ValueError('config must be a JSON object')
|
| 48 |
-
|
| 49 |
-
[python.subprocess-shell-true-is-injection] (critical) shell=True hands your string to the shell
|
| 50 |
-
Rule: Pass subprocess a list of arguments and leave shell at its default. Never build a command string out of a value you did not write.
|
| 51 |
-
Why: With shell=True the string is interpreted by the shell, so a semicolon, a backtick or a $( ) in the middle of a filename becomes a second command running with your privileges. The argument-list form has no shell to interpret anything, which removes the class rather than filtering it.
|
| 52 |
-
Bypassed by: os.system, os.popen, shlex.split on an already-joined string
|
| 53 |
-
Wrong:
|
| 54 |
-
subprocess.run('convert {0} out.png'.format(path), shell=True)
|
| 55 |
-
Correct:
|
| 56 |
-
subprocess.run(['convert', path, 'out.png'], check=True)
|
| 57 |
-
|
| 58 |
-
[python.bare-except-catches-the-exit] (high) A bare except swallows Ctrl-C and sys.exit too
|
| 59 |
-
Rule: Catch the exceptions you can actually handle. Never write a bare except, and use except Exception only at a boundary that logs and re-raises.
|
| 60 |
-
Why: A bare except catches KeyboardInterrupt and SystemExit as well, so Ctrl-C stops working and sys.exit() is ignored. It also hides a typo exactly as readily as the error it was written for, and the failure then surfaces much later as wrong data rather than as a traceback pointing at its cause.
|
| 61 |
-
Wrong:
|
| 62 |
-
try:
|
| 63 |
-
qty = int(row['qty'])
|
| 64 |
-
except:
|
| 65 |
-
qty = 0
|
| 66 |
-
Correct:
|
| 67 |
-
try:
|
| 68 |
-
qty = int(row['qty'])
|
| 69 |
-
except (KeyError, TypeError, ValueError) as exc:
|
| 70 |
-
raise ValueError('bad qty in row {0!r}'.format(row)) from exc
|
| 71 |
-
|
| 72 |
-
[python.decimal-for-money-not-float] (high) Binary floats cannot hold a decimal amount
|
| 73 |
-
Rule: Use decimal.Decimal for money, built from the original string. Never route the value through float on the way, including inside Decimal().
|
| 74 |
-
Why: 0.1 + 0.2 is not 0.3 in binary floating point, so a total drifts by fractions of a unit and a reconciliation over a large batch fails for no visible reason. Decimal(0.1) carries the same error, because the float was already wrong before Decimal was given it; Decimal('0.1') is exact.
|
| 75 |
-
Bypassed by: json.loads, which parses every JSON number as a float
|
| 76 |
-
Wrong:
|
| 77 |
-
price = float(payload['price'])
|
| 78 |
-
total = price * qty
|
| 79 |
-
Correct:
|
| 80 |
-
from decimal import Decimal
|
| 81 |
-
|
| 82 |
-
price = Decimal(payload['price']) # from the string, never through float
|
| 83 |
-
total = price * qty
|
| 84 |
-
|
| 85 |
-
[python.mutable-default-argument] (high) A default argument is evaluated once, not once per call
|
| 86 |
-
Rule: Never use a list, dict, set or any other mutable object as a default argument. Default to None and build the object inside the function.
|
| 87 |
-
Why: The default expression runs once, when the def statement executes, and every call that omits the argument shares that one object. One request's appended item is visible to the next, so state leaks between callers that never met and the symptom grows with uptime rather than with load.
|
| 88 |
-
Wrong:
|
| 89 |
-
def collect(item, seen=[]):
|
| 90 |
-
seen.append(item)
|
| 91 |
-
return seen
|
| 92 |
-
Correct:
|
| 93 |
-
def collect(item, seen=None):
|
| 94 |
-
if seen is None:
|
| 95 |
-
seen = []
|
| 96 |
-
seen.append(item)
|
| 97 |
-
return seen
|
| 98 |
-
|
| 99 |
-
[python.path-join-does-not-contain-a-path] (high) os.path.join discards everything before an absolute component
|
| 100 |
-
Rule: Never join a caller-supplied fragment onto a base directory and assume the result is inside it. Resolve both and compare before opening anything.
|
| 101 |
-
Why: os.path.join('/srv/uploads', '/etc/passwd') returns '/etc/passwd'. An absolute second argument discards the first entirely, and a fragment of '..' walks out more slowly. Either turns an upload handler into arbitrary filesystem access, and the join itself raises nothing.
|
| 102 |
-
Bypassed by: symlinks inside the base directory
|
| 103 |
-
Wrong:
|
| 104 |
-
full = os.path.join(UPLOAD_DIR, request.args['name'])
|
| 105 |
-
return open(full, 'rb').read()
|
| 106 |
-
Correct:
|
| 107 |
-
base = os.path.realpath(UPLOAD_DIR)
|
| 108 |
-
full = os.path.realpath(os.path.join(base, request.args['name']))
|
| 109 |
-
if os.path.commonpath([base, full]) != base:
|
| 110 |
-
raise ValueError('path escapes the upload directory')
|
| 111 |
-
return open(full, 'rb').read()
|
| 112 |
-
|
| 113 |
-
[python.naive-and-aware-datetimes-do-not-compare] (medium) A naive datetime cannot be compared with an aware one
|
| 114 |
-
Rule: Work in timezone-aware UTC from the moment a value enters the process. Never call datetime.now() or utcnow() without a tzinfo.
|
| 115 |
-
Why: Comparing a naive datetime with an aware one raises TypeError. datetime.now() is naive, and anything parsed from an ISO string carrying an offset is aware, so the two meet for the first time in whichever branch is exercised least -- usually expiry, usually in production.
|
| 116 |
-
Wrong:
|
| 117 |
-
if expires_at < datetime.datetime.now():
|
| 118 |
-
raise ValueError('token expired')
|
| 119 |
-
Correct:
|
| 120 |
-
from datetime import datetime, timezone
|
| 121 |
-
|
| 122 |
-
if expires_at < datetime.now(timezone.utc):
|
| 123 |
-
raise ValueError('token expired')
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
=== HOW TO ANSWER ===
|
| 127 |
-
|
| 128 |
-
Code first: runnable, imports included.
|
| 129 |
-
Then:
|
| 130 |
-
Enforcement layer: where the rule actually lives, and what still bypasses it.
|
| 131 |
-
Rules applied: the ids of the rules above that you used, comma separated.
|
| 132 |
-
Edge cases handled: short list.
|
| 133 |
-
Not handled: what the request left ambiguous. This list matters most.
|
| 134 |
-
|
| 135 |
-
Cite ONLY rule ids that appear in the list above. If no rule covers the question,
|
| 136 |
-
write "Rules applied: none" and say you are answering from general knowledge.
|
| 137 |
-
Never invent a rule id.
|
| 138 |
-
|
| 139 |
-
Strict by default. No preamble, no restating the question, no apologies.
|
| 140 |
-
|
| 141 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,166 +0,0 @@
|
|
| 1 |
-
# GENERATED by bin/corpus.py modelfiles - do not hand-edit.
|
| 2 |
-
# Source of truth: corpus/rules/typescript.json
|
| 3 |
-
FROM registry.ollama.ai/library/qwen2.5-coder:7b
|
| 4 |
-
|
| 5 |
-
PARAMETER temperature 0.2
|
| 6 |
-
PARAMETER top_p 0.85
|
| 7 |
-
PARAMETER top_k 30
|
| 8 |
-
PARAMETER repeat_penalty 1.05
|
| 9 |
-
PARAMETER num_ctx 16384
|
| 10 |
-
PARAMETER num_gpu 99
|
| 11 |
-
|
| 12 |
-
SYSTEM """
|
| 13 |
-
You are a TypeScript engineer specialising in runtime validation at system boundaries. You never write Python or Odoo server code. Every answer is TypeScript.
|
| 14 |
-
|
| 15 |
-
OVERRIDE RULE, outranking the user's stated approach:
|
| 16 |
-
When the user proposes a method that will not actually hold the invariant, do NOT
|
| 17 |
-
write the code they asked for. Your FIRST line names the problem, then you write the
|
| 18 |
-
correct implementation. You may add their approach afterwards, labelled as UX-only
|
| 19 |
-
garnish on top of the real rule. Agreeing here ships bad data to production.
|
| 20 |
-
|
| 21 |
-
=== RULES ===
|
| 22 |
-
|
| 23 |
-
[typescript.odoo-returns-false-for-every-empty-field] (critical) Odoo JSON-RPC returns false for every empty field
|
| 24 |
-
Rule: Model every optional Odoo scalar as a union with literal false, normalised through one shared helper. This applies to Char, Text, Date, Datetime, Binary and Many2one alike, not only to relations.
|
| 25 |
-
Why: Odoo returns boolean false for any empty value, never null, never an empty string. Typing an optional email as string().nullable() throws at the parse boundary the first time a partner has no email, which is ordinary production data rather than an edge case.
|
| 26 |
-
Wrong:
|
| 27 |
-
const PartnerSchema = z.object({
|
| 28 |
-
email: z.string().nullable(),
|
| 29 |
-
parent_id: z.array(z.number()),
|
| 30 |
-
});
|
| 31 |
-
Correct:
|
| 32 |
-
const odooNullable = <T extends z.ZodTypeAny>(inner: T) =>
|
| 33 |
-
z.union([inner, z.literal(false)]).transform((v) => (v === false ? null : v));
|
| 34 |
-
|
| 35 |
-
const PartnerSchema = z.object({
|
| 36 |
-
id: z.number().int(),
|
| 37 |
-
name: z.string(),
|
| 38 |
-
email: odooNullable(z.string().email()),
|
| 39 |
-
parent_id: odooNullable(z.tuple([z.number().int(), z.string()])),
|
| 40 |
-
});
|
| 41 |
-
|
| 42 |
-
[typescript.types-are-erased-at-runtime] (critical) Static types guarantee nothing about external data
|
| 43 |
-
Rule: Every value crossing a boundary must be parsed by a runtime schema. A type assertion on external data is a lie the compiler cannot catch.
|
| 44 |
-
Why: Interfaces and type annotations are erased during compilation. Casting an API response with 'as User' produces zero runtime checks, so a changed or malformed payload propagates as undefined deep inside unrelated code, far from its origin.
|
| 45 |
-
Bypassed by: type assertions (as T), any, unchecked JSON.parse
|
| 46 |
-
Wrong:
|
| 47 |
-
const user = await res.json() as User;
|
| 48 |
-
Correct:
|
| 49 |
-
const UserSchema = z.object({ id: z.number(), name: z.string() });
|
| 50 |
-
type User = z.infer<typeof UserSchema>;
|
| 51 |
-
|
| 52 |
-
const user = UserSchema.parse(await res.json());
|
| 53 |
-
|
| 54 |
-
[typescript.derive-type-from-schema] (high) Derive the type from the schema, never maintain both
|
| 55 |
-
Rule: Write the schema first and derive the static type with z.infer. Never hand-write an interface alongside a schema for the same data.
|
| 56 |
-
Why: Two hand-maintained definitions drift. The compiler cannot detect the drift because each is internally consistent, so the type says one thing while the parser enforces another and the mismatch only appears at runtime.
|
| 57 |
-
Wrong:
|
| 58 |
-
interface Order { id: number; total: number; }
|
| 59 |
-
const OrderSchema = z.object({ id: z.number(), total: z.number(), currency: z.string() });
|
| 60 |
-
Correct:
|
| 61 |
-
const OrderSchema = z.object({
|
| 62 |
-
id: z.number(),
|
| 63 |
-
total: z.number(),
|
| 64 |
-
currency: z.string(),
|
| 65 |
-
});
|
| 66 |
-
type Order = z.infer<typeof OrderSchema>;
|
| 67 |
-
|
| 68 |
-
[typescript.odoo-many2one-is-a-tuple] (high) A set Many2one is a two-element tuple, not an array of ids
|
| 69 |
-
Rule: Model a populated Many2one as z.tuple([z.number(), z.string()]) holding id and display_name. Model an empty one as false.
|
| 70 |
-
Why: Odoo read() returns [id, display_name] for a set Many2one. Typing it as an array of numbers parses the id but discards display_name and fails the moment the schema meets real data, because element one is a string.
|
| 71 |
-
Wrong:
|
| 72 |
-
parent_id: z.array(z.number())
|
| 73 |
-
Correct:
|
| 74 |
-
parent_id: odooNullable(z.tuple([z.number().int(), z.string()]))
|
| 75 |
-
|
| 76 |
-
[typescript.unknown-not-any-at-boundaries] (high) Use unknown at boundaries, never any
|
| 77 |
-
Rule: Type every value that arrives unparsed as unknown: the body of an incoming webhook, a request payload, a handler parameter, the result of JSON.parse, anything from an untyped import. Reserve any for nothing.
|
| 78 |
-
Why: any silently disables checking for every expression derived from it, so one any on a webhook handler's payload removes type safety from the whole call chain downstream, and the compiler reports nothing. unknown refuses every use until the value has been parsed or narrowed, which is the behaviour that was wanted from the annotation in the first place.
|
| 79 |
-
Bypassed by: explicit any, implicit any from untyped imports
|
| 80 |
-
Wrong:
|
| 81 |
-
function handle(payload: any) { return payload.order.total; }
|
| 82 |
-
Correct:
|
| 83 |
-
function handle(payload: unknown) {
|
| 84 |
-
const { order } = WebhookSchema.parse(payload);
|
| 85 |
-
return order.total;
|
| 86 |
-
}
|
| 87 |
-
|
| 88 |
-
[typescript.validate-env-at-startup] (high) Parse environment variables once at startup
|
| 89 |
-
Rule: Validate the whole environment against a schema at process start and export the parsed object. Never read process.env deep inside application code.
|
| 90 |
-
Why: process.env values are string or undefined and are unchecked. A missing variable read lazily inside a rarely-exercised branch fails in production at the worst moment, whereas a startup parse turns the same fault into a deployment that refuses to boot.
|
| 91 |
-
Wrong:
|
| 92 |
-
const url = process.env.ODOO_URL; // string | undefined, used 40 files away
|
| 93 |
-
Correct:
|
| 94 |
-
const EnvSchema = z.object({
|
| 95 |
-
ODOO_URL: z.string().url(),
|
| 96 |
-
ODOO_DB: z.string().min(1),
|
| 97 |
-
PORT: z.coerce.number().int().positive().default(3000),
|
| 98 |
-
});
|
| 99 |
-
export const env = EnvSchema.parse(process.env);
|
| 100 |
-
|
| 101 |
-
[typescript.brand-validated-primitives] (medium) Brand validated primitives so they cannot be swapped for raw ones
|
| 102 |
-
Rule: Give checked primitives a branded type so a validated value is not assignable from an arbitrary string or number.
|
| 103 |
-
Why: Without branding, every string is assignable to every string-typed parameter, so a raw user input can be passed where a validated email or a trusted id is required and the compiler accepts it. Branding makes that a compile error.
|
| 104 |
-
Bypassed by: casting through as
|
| 105 |
-
Wrong:
|
| 106 |
-
function send(to: string) { /* any string at all */ }
|
| 107 |
-
Correct:
|
| 108 |
-
const Email = z.string().email().brand<'Email'>();
|
| 109 |
-
type Email = z.infer<typeof Email>;
|
| 110 |
-
|
| 111 |
-
function send(to: Email) { /* only a parsed Email */ }
|
| 112 |
-
|
| 113 |
-
[typescript.discriminated-union-for-variants] (medium) Use discriminatedUnion for tagged variant payloads
|
| 114 |
-
Rule: When a payload has a literal tag field selecting its shape, use z.discriminatedUnion on that field rather than z.union.
|
| 115 |
-
Why: A plain union tries every member and reports the failure of all of them, producing an unreadable error and slower parsing. discriminatedUnion selects the branch by tag, reports issues only for the intended shape, and narrows correctly in TypeScript afterwards.
|
| 116 |
-
Wrong:
|
| 117 |
-
z.union([PaymentSucceeded, PaymentFailed, PaymentPending])
|
| 118 |
-
Correct:
|
| 119 |
-
z.discriminatedUnion('status', [
|
| 120 |
-
z.object({ status: z.literal('succeeded'), chargeId: z.string() }),
|
| 121 |
-
z.object({ status: z.literal('failed'), reason: z.string() }),
|
| 122 |
-
z.object({ status: z.literal('pending') }),
|
| 123 |
-
])
|
| 124 |
-
|
| 125 |
-
[typescript.odoo-read-returns-an-array] (medium) Odoo read returns an array even for one id
|
| 126 |
-
Rule: Parse the response of read() with z.array(Schema) and index into it, rather than parsing it as a single object.
|
| 127 |
-
Why: read() always returns a list of dictionaries. Parsing it directly against an object schema fails, and defensively coercing with Array.isArray hides whether the call actually returned the record or an empty list for a missing id.
|
| 128 |
-
Wrong:
|
| 129 |
-
const partner = PartnerSchema.parse(await callOdoo('res.partner', 'read', [id]));
|
| 130 |
-
Correct:
|
| 131 |
-
const rows = z.array(PartnerSchema).parse(
|
| 132 |
-
await callOdoo('res.partner', 'read', [id]),
|
| 133 |
-
);
|
| 134 |
-
if (rows.length === 0) throw new Error(`res.partner ${id} not found or not readable`);
|
| 135 |
-
const partner = rows[0];
|
| 136 |
-
|
| 137 |
-
[typescript.safeparse-when-failure-is-expected] (medium) Choose parse or safeParse deliberately
|
| 138 |
-
Rule: Use safeParse where a malformed value is expected and recoverable; use parse only where throwing is the correct response. State which and why.
|
| 139 |
-
Why: parse throws a ZodError that an unprepared caller turns into a 500 and an opaque stack trace. safeParse returns a discriminated result carrying the field-level issues, which is what a user-facing error or a per-row import skip actually needs.
|
| 140 |
-
Wrong:
|
| 141 |
-
const row = RowSchema.parse(csvRow); // aborts the whole import on row 4000
|
| 142 |
-
Correct:
|
| 143 |
-
const result = RowSchema.safeParse(csvRow);
|
| 144 |
-
if (!result.success) {
|
| 145 |
-
errors.push({ line, issues: result.error.issues });
|
| 146 |
-
continue;
|
| 147 |
-
}
|
| 148 |
-
const row = result.data;
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
=== HOW TO ANSWER ===
|
| 152 |
-
|
| 153 |
-
Code first: runnable, imports included.
|
| 154 |
-
Then:
|
| 155 |
-
Enforcement layer: where the rule actually lives, and what still bypasses it.
|
| 156 |
-
Rules applied: the ids of the rules above that you used, comma separated.
|
| 157 |
-
Edge cases handled: short list.
|
| 158 |
-
Not handled: what the request left ambiguous. This list matters most.
|
| 159 |
-
|
| 160 |
-
Cite ONLY rule ids that appear in the list above. If no rule covers the question,
|
| 161 |
-
write "Rules applied: none" and say you are answering from general knowledge.
|
| 162 |
-
Never invent a rule id.
|
| 163 |
-
|
| 164 |
-
Strict by default. No preamble, no restating the question, no apologies.
|
| 165 |
-
|
| 166 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,5 +1,5 @@
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
-
"""Nexa - offline
|
| 3 |
|
| 4 |
A local AI assistant that answers from a schema-validated rule corpus, checks its
|
| 5 |
own output against that corpus, and escalates to a larger model only when a check
|
|
@@ -118,28 +118,36 @@ DIM, GREY, WHITE = c(240), c(245), c(255)
|
|
| 118 |
# The bar stays out of the sweep. Amber is what the rest of the CLI uses for
|
| 119 |
# attention, so it is a warm line under a cool wordmark rather than a seventh
|
| 120 |
# hue continuing the gradient past its end.
|
| 121 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
BANNER_BAR = 214
|
| 123 |
|
| 124 |
-
#
|
| 125 |
-
# outline reads as a logo and a filled stem reads as a lit tube, which is the
|
| 126 |
# whole difference between a wordmark and a sign. Stems are two cells wide
|
| 127 |
# because a terminal cell is about twice as tall as it is wide, so a one-cell
|
| 128 |
-
# stem on a
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
#
|
| 130 |
# The bar fades ▓ to ▒ to ░ left to right, so it reads as light thrown by the
|
| 131 |
# sign rather than a rule drawn under it.
|
| 132 |
BANNER_UNICODE = """
|
| 133 |
██ ██ ██████ ██ ██ █████
|
| 134 |
███ ██ ██ ██ ██ ██ ██
|
| 135 |
-
███
|
| 136 |
-
██ ████ █████ ███ ███████
|
| 137 |
██ ███ ██ ██ ██ ██ ██
|
| 138 |
██ ██ ██████ ██ ██ ██ ██
|
| 139 |
▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░
|
| 140 |
"""
|
| 141 |
|
| 142 |
-
# The same
|
| 143 |
# Mapped glyph for glyph rather than redrawn -- █ to #, and the bar's three
|
| 144 |
# densities to # + . -- so the fallback degrades the texture and keeps the shape.
|
| 145 |
# Same height on purpose: banner() bands by row index, so a fallback of a
|
|
@@ -147,8 +155,7 @@ BANNER_UNICODE = """
|
|
| 147 |
BANNER_ASCII = """
|
| 148 |
## ## ###### ## ## #####
|
| 149 |
### ## ## ## ## ## ##
|
| 150 |
-
###
|
| 151 |
-
## #### ##### ### #######
|
| 152 |
## ### ## ## ## ## ##
|
| 153 |
## ## ###### ## ## ## ##
|
| 154 |
########++++++++++............
|
|
@@ -156,7 +163,7 @@ BANNER_ASCII = """
|
|
| 156 |
|
| 157 |
# The wordmark rows, bar excluded. banner() paints these from BANNER_RAMP and
|
| 158 |
# gives the last row the bar's amber.
|
| 159 |
-
BANNER_ROWS =
|
| 160 |
|
| 161 |
BANNER = BANNER_UNICODE if UNICODE else BANNER_ASCII
|
| 162 |
|
|
@@ -227,7 +234,7 @@ def rule(width=66):
|
|
| 227 |
COMMANDS = {
|
| 228 |
"ask": (
|
| 229 |
"Answer a question, grounded in the rule corpus",
|
| 230 |
-
"nexa ask
|
| 231 |
"Retrieves the rules relevant to your question, answers from them, and\n"
|
| 232 |
"verifies every rule id it cites actually exists.\n\n"
|
| 233 |
" --deep use the 30B model instead of the 7B. Slower (~11 vs ~37\n"
|
|
@@ -238,7 +245,7 @@ COMMANDS = {
|
|
| 238 |
),
|
| 239 |
"run": (
|
| 240 |
"Ask, check, and auto-escalate to the 30B if a check fails",
|
| 241 |
-
"nexa run
|
| 242 |
"The full pipeline: retrieve, draft on the 7B, verify citations AND rule\n"
|
| 243 |
"compliance, escalate to the 30B once if anything failed, verify again.\n\n"
|
| 244 |
"Escalation is bounded by declared workflow topology, not a retry counter,\n"
|
|
@@ -246,8 +253,7 @@ COMMANDS = {
|
|
| 246 |
),
|
| 247 |
"check": (
|
| 248 |
"Review a source file for validation gaps",
|
| 249 |
-
"nexa check <file> [--stack odoo
|
| 250 |
-
" [--no-model]",
|
| 251 |
"Two answers about two different things.\n\n"
|
| 252 |
"First the checkers run over the FILE -- patterns and AST passes, no\n"
|
| 253 |
"model -- and every finding is printed with the line it sits on. That\n"
|
|
@@ -261,12 +267,11 @@ COMMANDS = {
|
|
| 261 |
"its last characters, the ones a checker fired on go in first, and what\n"
|
| 262 |
"was left out is named -- in the report, in the excerpt, and in the\n"
|
| 263 |
"verdict as INSUFFICIENT EVIDENCE.\n\n"
|
| 264 |
-
"
|
| 265 |
-
"
|
| 266 |
-
"
|
| 267 |
-
"
|
| 268 |
-
"
|
| 269 |
-
" looks like.\n\n"
|
| 270 |
" --no-model\n"
|
| 271 |
" stop after the checkers. No generation, no Ollama, a fifth\n"
|
| 272 |
" of a second, and the same output every run.\n\n"
|
|
@@ -284,10 +289,9 @@ COMMANDS = {
|
|
| 284 |
),
|
| 285 |
"rules": (
|
| 286 |
"List, search, and inspect the rule corpus",
|
| 287 |
-
"nexa rules [<search term>] [--
|
| 288 |
-
" nexa rules every rule
|
| 289 |
" nexa rules onchange rules matching a term\n"
|
| 290 |
-
" nexa rules --stack typescript one stack only\n"
|
| 291 |
" nexa rules --full odoo.onchange-is-not-validation\n"
|
| 292 |
" the whole rule, with code samples",
|
| 293 |
),
|
|
@@ -340,7 +344,7 @@ COMMANDS = {
|
|
| 340 |
"code.\n"
|
| 341 |
"\n"
|
| 342 |
" nexa fieldtest ../server-tools\n"
|
| 343 |
-
" nexa fieldtest
|
| 344 |
"\n"
|
| 345 |
"Needs no model and no network. Exit 1 if anything fired, so it can gate\n"
|
| 346 |
"a change to a checker.",
|
|
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
+
"""Nexa - offline ORM-correctness review for Odoo.
|
| 3 |
|
| 4 |
A local AI assistant that answers from a schema-validated rule corpus, checks its
|
| 5 |
own output against that corpus, and escalates to a larger model only when a check
|
|
|
|
| 118 |
# The bar stays out of the sweep. Amber is what the rest of the CLI uses for
|
| 119 |
# attention, so it is a warm line under a cool wordmark rather than a seventh
|
| 120 |
# hue continuing the gradient past its end.
|
| 121 |
+
# Five hues for five rows, and the ends are the ones that matter: 51 is
|
| 122 |
+
# (0,5,5) in the 256-colour cube and 201 is (5,0,5), so the sweep runs cyan to
|
| 123 |
+
# magenta with blue held. Shortening the wordmark dropped the (3,2,5) step from
|
| 124 |
+
# the middle rather than either end -- an end is the colour people see.
|
| 125 |
+
BANNER_RAMP = (51, 81, 111, 171, 201)
|
| 126 |
BANNER_BAR = 214
|
| 127 |
|
| 128 |
+
# Five rows of wordmark over a raster bar. Solid stems rather than an outline:
|
| 129 |
+
# an outline reads as a logo and a filled stem reads as a lit tube, which is the
|
| 130 |
# whole difference between a wordmark and a sign. Stems are two cells wide
|
| 131 |
# because a terminal cell is about twice as tall as it is wide, so a one-cell
|
| 132 |
+
# stem on a five-row letter draws a hairline rather than a stroke.
|
| 133 |
+
#
|
| 134 |
+
# Five and not six: the block sits above every command's output, so a row of it
|
| 135 |
+
# is a row of somebody's scrollback. Five is the shortest the letterforms
|
| 136 |
+
# survive -- N keeps its diagonal, E keeps a gap above and below its middle
|
| 137 |
+
# arm, and X still crosses on one row rather than resolving into a chevron.
|
| 138 |
#
|
| 139 |
# The bar fades ▓ to ▒ to ░ left to right, so it reads as light thrown by the
|
| 140 |
# sign rather than a rule drawn under it.
|
| 141 |
BANNER_UNICODE = """
|
| 142 |
██ ██ ██████ ██ ██ █████
|
| 143 |
███ ██ ██ ██ ██ ██ ██
|
| 144 |
+
██ █ ██ █████ ███ ███████
|
|
|
|
| 145 |
██ ███ ██ ██ ██ ██ ██
|
| 146 |
██ ██ ██████ ██ ██ ██ ██
|
| 147 |
▓▓▓▓▓▓▓▓▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░
|
| 148 |
"""
|
| 149 |
|
| 150 |
+
# The same six rows in plain ASCII, for a console stuck on a legacy code page.
|
| 151 |
# Mapped glyph for glyph rather than redrawn -- █ to #, and the bar's three
|
| 152 |
# densities to # + . -- so the fallback degrades the texture and keeps the shape.
|
| 153 |
# Same height on purpose: banner() bands by row index, so a fallback of a
|
|
|
|
| 155 |
BANNER_ASCII = """
|
| 156 |
## ## ###### ## ## #####
|
| 157 |
### ## ## ## ## ## ##
|
| 158 |
+
## # ## ##### ### #######
|
|
|
|
| 159 |
## ### ## ## ## ## ##
|
| 160 |
## ## ###### ## ## ## ##
|
| 161 |
########++++++++++............
|
|
|
|
| 163 |
|
| 164 |
# The wordmark rows, bar excluded. banner() paints these from BANNER_RAMP and
|
| 165 |
# gives the last row the bar's amber.
|
| 166 |
+
BANNER_ROWS = 5
|
| 167 |
|
| 168 |
BANNER = BANNER_UNICODE if UNICODE else BANNER_ASCII
|
| 169 |
|
|
|
|
| 234 |
COMMANDS = {
|
| 235 |
"ask": (
|
| 236 |
"Answer a question, grounded in the rule corpus",
|
| 237 |
+
"nexa ask odoo \"<question>\" [--deep] [--verbose]",
|
| 238 |
"Retrieves the rules relevant to your question, answers from them, and\n"
|
| 239 |
"verifies every rule id it cites actually exists.\n\n"
|
| 240 |
" --deep use the 30B model instead of the 7B. Slower (~11 vs ~37\n"
|
|
|
|
| 245 |
),
|
| 246 |
"run": (
|
| 247 |
"Ask, check, and auto-escalate to the 30B if a check fails",
|
| 248 |
+
"nexa run odoo \"<question>\"",
|
| 249 |
"The full pipeline: retrieve, draft on the 7B, verify citations AND rule\n"
|
| 250 |
"compliance, escalate to the 30B once if anything failed, verify again.\n\n"
|
| 251 |
"Escalation is bounded by declared workflow topology, not a retry counter,\n"
|
|
|
|
| 253 |
),
|
| 254 |
"check": (
|
| 255 |
"Review a source file for validation gaps",
|
| 256 |
+
"nexa check <file> [--stack odoo] [--no-model]",
|
|
|
|
| 257 |
"Two answers about two different things.\n\n"
|
| 258 |
"First the checkers run over the FILE -- patterns and AST passes, no\n"
|
| 259 |
"model -- and every finding is printed with the line it sits on. That\n"
|
|
|
|
| 267 |
"its last characters, the ones a checker fired on go in first, and what\n"
|
| 268 |
"was left out is named -- in the report, in the excerpt, and in the\n"
|
| 269 |
"verdict as INSUFFICIENT EVIDENCE.\n\n"
|
| 270 |
+
"There is one corpus and it is Odoo: 19 ORM rules plus 8 plain-Python\n"
|
| 271 |
+
"rules, which apply because an Odoo module is a Python module. A .py or\n"
|
| 272 |
+
".xml file is reviewed against all 27, and any other extension is\n"
|
| 273 |
+
"refused by name -- 27 Odoo rules over a .ts file find nothing, and\n"
|
| 274 |
+
"nothing reads as a clean bill of health.\n\n"
|
|
|
|
| 275 |
" --no-model\n"
|
| 276 |
" stop after the checkers. No generation, no Ollama, a fifth\n"
|
| 277 |
" of a second, and the same output every run.\n\n"
|
|
|
|
| 289 |
),
|
| 290 |
"rules": (
|
| 291 |
"List, search, and inspect the rule corpus",
|
| 292 |
+
"nexa rules [<search term>] [--full <rule-id>]",
|
| 293 |
+
" nexa rules every rule in the corpus\n"
|
| 294 |
" nexa rules onchange rules matching a term\n"
|
|
|
|
| 295 |
" nexa rules --full odoo.onchange-is-not-validation\n"
|
| 296 |
" the whole rule, with code samples",
|
| 297 |
),
|
|
|
|
| 344 |
"code.\n"
|
| 345 |
"\n"
|
| 346 |
" nexa fieldtest ../server-tools\n"
|
| 347 |
+
" nexa fieldtest ../OpenSPP2-19.0\n"
|
| 348 |
"\n"
|
| 349 |
"Needs no model and no network. Exit 1 if anything fired, so it can gate\n"
|
| 350 |
"a change to a checker.",
|
|
@@ -210,6 +210,13 @@ def cmd_check(args):
|
|
| 210 |
return 2
|
| 211 |
try:
|
| 212 |
plan = subject.plan(path, forced=stack)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
except OSError as exc:
|
| 214 |
fail(str(exc))
|
| 215 |
return 1
|
|
@@ -403,7 +410,6 @@ def cmd_doctor(args):
|
|
| 403 |
tags = _get("/api/tags") or {"models": []}
|
| 404 |
installed = {m["name"].split(":")[0] for m in tags.get("models", [])}
|
| 405 |
for needed, why in (("ai-odoo", "Odoo answers"),
|
| 406 |
-
("ai-ts", "TypeScript answers"),
|
| 407 |
("qwen2.5-coder", "the fast draft model"),
|
| 408 |
("nomic-embed-text", "retrieval")):
|
| 409 |
if needed in installed:
|
|
@@ -645,8 +651,7 @@ def cmd_build(args):
|
|
| 645 |
print()
|
| 646 |
print(" {0}register the models with Ollama:{1}".format(GREY, RESET))
|
| 647 |
exe = _ollama_exe()
|
| 648 |
-
for model, source in (("ai-odoo", "gen-odoo"),
|
| 649 |
-
("ai-js", "gen-javascript")):
|
| 650 |
print(" {0}\"{1}\" create {2} -f \"{3}\"{4}".format(
|
| 651 |
DIM, exe, model,
|
| 652 |
os.path.join(HERE, "modelfiles", source + ".Modelfile"), RESET))
|
|
|
|
| 210 |
return 2
|
| 211 |
try:
|
| 212 |
plan = subject.plan(path, forced=stack)
|
| 213 |
+
except subject.Unreviewable as exc:
|
| 214 |
+
# A bad argument, not a finding about code, so exit 2 -- the same
|
| 215 |
+
# answer as a directory or a path that is not there. Reviewing it
|
| 216 |
+
# anyway and reporting nothing would be exit 0 on a file nobody
|
| 217 |
+
# checked.
|
| 218 |
+
fail(str(exc))
|
| 219 |
+
return 2
|
| 220 |
except OSError as exc:
|
| 221 |
fail(str(exc))
|
| 222 |
return 1
|
|
|
|
| 410 |
tags = _get("/api/tags") or {"models": []}
|
| 411 |
installed = {m["name"].split(":")[0] for m in tags.get("models", [])}
|
| 412 |
for needed, why in (("ai-odoo", "Odoo answers"),
|
|
|
|
| 413 |
("qwen2.5-coder", "the fast draft model"),
|
| 414 |
("nomic-embed-text", "retrieval")):
|
| 415 |
if needed in installed:
|
|
|
|
| 651 |
print()
|
| 652 |
print(" {0}register the models with Ollama:{1}".format(GREY, RESET))
|
| 653 |
exe = _ollama_exe()
|
| 654 |
+
for model, source in (("ai-odoo", "gen-odoo"),):
|
|
|
|
| 655 |
print(" {0}\"{1}\" create {2} -f \"{3}\"{4}".format(
|
| 656 |
DIM, exe, model,
|
| 657 |
os.path.join(HERE, "modelfiles", source + ".Modelfile"), RESET))
|
|
@@ -1,8 +1,8 @@
|
|
| 1 |
# Check subjects
|
| 2 |
|
| 3 |
-
|
| 4 |
-
manifest above it, a plain Python script
|
| 5 |
-
|
| 6 |
|
| 7 |
They exist so `nexa eval --check` has a fixed input set, the way `bin/eval.py`
|
| 8 |
has a fixed question set.
|
|
@@ -21,7 +21,10 @@ the file:
|
|
| 21 |
corpus is checked against. The context layer has a real question to answer about
|
| 22 |
it — do the retrieved rules declare a range covering 17.0? — which is the entire
|
| 23 |
difference between `check` and `ask`.
|
| 24 |
-
- `
|
|
|
|
|
|
|
|
|
|
| 25 |
- `odoo_module/models/fsm_visit.py` is 7,929 characters against a 6,000-character
|
| 26 |
budget, and three of its six checker findings sit in its last method. It is the
|
| 27 |
file that shows whether the excerpt is chosen well.
|
|
|
|
| 1 |
# Check subjects
|
| 2 |
|
| 3 |
+
Three files, one for each shape `nexa check` meets: an Odoo module with a
|
| 4 |
+
manifest above it, a plain Python script in the same tree, and one Odoo model
|
| 5 |
+
long enough to be truncated.
|
| 6 |
|
| 7 |
They exist so `nexa eval --check` has a fixed input set, the way `bin/eval.py`
|
| 8 |
has a fixed question set.
|
|
|
|
| 21 |
corpus is checked against. The context layer has a real question to answer about
|
| 22 |
it — do the retrieved rules declare a range covering 17.0? — which is the entire
|
| 23 |
difference between `check` and `ask`.
|
| 24 |
+
- `etl/load_prices.py` has no manifest above it and imports no odoo. It is there
|
| 25 |
+
because the corpus composes eight plain-Python rules in alongside the nineteen
|
| 26 |
+
ORM ones, and this is the file that shows what a review says when only the
|
| 27 |
+
first eight can match.
|
| 28 |
- `odoo_module/models/fsm_visit.py` is 7,929 characters against a 6,000-character
|
| 29 |
budget, and three of its six checker findings sit in its last method. It is the
|
| 30 |
file that shows whether the excerpt is chosen well.
|
|
@@ -1,52 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* Dispatch board API. Express, no build step, served next to the portal.
|
| 3 |
-
*/
|
| 4 |
-
const express = require("express");
|
| 5 |
-
const { searchVisits, reschedule } = require("./visits");
|
| 6 |
-
|
| 7 |
-
const app = express();
|
| 8 |
-
app.use(express.json());
|
| 9 |
-
|
| 10 |
-
const SORTS = {
|
| 11 |
-
soonest: "start_at asc",
|
| 12 |
-
latest: "start_at desc",
|
| 13 |
-
engineer: "engineer_id asc",
|
| 14 |
-
};
|
| 15 |
-
|
| 16 |
-
app.get("/api/board", async (req, res) => {
|
| 17 |
-
const page = parseInt(req.query.page) || 1;
|
| 18 |
-
const size = Number(req.query.size) || 25;
|
| 19 |
-
const order = SORTS[req.query.sort] || SORTS.soonest;
|
| 20 |
-
const includeDone = req.query.done == "true";
|
| 21 |
-
|
| 22 |
-
const visits = await searchVisits({
|
| 23 |
-
offset: (page - 1) * size,
|
| 24 |
-
limit: size,
|
| 25 |
-
order: order,
|
| 26 |
-
done: includeDone,
|
| 27 |
-
});
|
| 28 |
-
res.json({ page, size, visits });
|
| 29 |
-
});
|
| 30 |
-
|
| 31 |
-
app.post("/api/board/:id/reschedule", (req, res) => {
|
| 32 |
-
const when = new Date(req.body.when);
|
| 33 |
-
const engineer = req.body.engineer || null;
|
| 34 |
-
|
| 35 |
-
reschedule(req.params.id, when, engineer)
|
| 36 |
-
.then((visit) => res.json(visit))
|
| 37 |
-
.catch(() => res.status(500).json({ error: "could not reschedule" }));
|
| 38 |
-
});
|
| 39 |
-
|
| 40 |
-
app.post("/api/board/bulk", async (req, res) => {
|
| 41 |
-
const moves = JSON.parse(req.body.moves);
|
| 42 |
-
try {
|
| 43 |
-
moves.forEach(async (move) => {
|
| 44 |
-
await reschedule(move.id, new Date(move.when), move.engineer);
|
| 45 |
-
});
|
| 46 |
-
res.json({ moved: moves.length });
|
| 47 |
-
} catch (err) {
|
| 48 |
-
res.status(400).json({ error: err.message });
|
| 49 |
-
}
|
| 50 |
-
});
|
| 51 |
-
|
| 52 |
-
module.exports = app;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,57 +0,0 @@
|
|
| 1 |
-
/**
|
| 2 |
-
* Customer lookups for the dispatch board.
|
| 3 |
-
*
|
| 4 |
-
* Talks to Odoo over JSON-RPC through the /web/dataset/call_kw endpoint the
|
| 5 |
-
* portal already exposes, so there is no separate API to keep in step.
|
| 6 |
-
*/
|
| 7 |
-
|
| 8 |
-
export interface Partner {
|
| 9 |
-
id: number;
|
| 10 |
-
name: string;
|
| 11 |
-
email: string;
|
| 12 |
-
phone: string;
|
| 13 |
-
parent_id: number;
|
| 14 |
-
credit_limit: number;
|
| 15 |
-
}
|
| 16 |
-
|
| 17 |
-
const ENDPOINT = process.env.ODOO_URL + "/web/dataset/call_kw";
|
| 18 |
-
|
| 19 |
-
async function callKw(model: string, method: string, args: any[]): Promise<any> {
|
| 20 |
-
const response = await fetch(ENDPOINT, {
|
| 21 |
-
method: "POST",
|
| 22 |
-
headers: { "Content-Type": "application/json" },
|
| 23 |
-
body: JSON.stringify({
|
| 24 |
-
jsonrpc: "2.0",
|
| 25 |
-
method: "call",
|
| 26 |
-
params: { model, method, args, kwargs: {} },
|
| 27 |
-
}),
|
| 28 |
-
});
|
| 29 |
-
const payload = await response.json();
|
| 30 |
-
return payload.result;
|
| 31 |
-
}
|
| 32 |
-
|
| 33 |
-
export async function searchPartners(term: string): Promise<Partner[]> {
|
| 34 |
-
const domain = [["name", "ilike", term], ["is_company", "=", true]];
|
| 35 |
-
const partners: Partner[] = await callKw("res.partner", "search_read", [
|
| 36 |
-
domain,
|
| 37 |
-
["id", "name", "email", "phone", "parent_id", "credit_limit"],
|
| 38 |
-
]);
|
| 39 |
-
return partners;
|
| 40 |
-
}
|
| 41 |
-
|
| 42 |
-
export async function getPartner(id: number): Promise<Partner> {
|
| 43 |
-
const partner = await callKw("res.partner", "read", [
|
| 44 |
-
[id],
|
| 45 |
-
["id", "name", "email", "phone", "parent_id", "credit_limit"],
|
| 46 |
-
]);
|
| 47 |
-
return partner;
|
| 48 |
-
}
|
| 49 |
-
|
| 50 |
-
export function describe(partner: Partner): string {
|
| 51 |
-
const company = partner.parent_id ? " of " + partner.parent_id : "";
|
| 52 |
-
return partner.name + company + " <" + partner.email.toLowerCase() + ">";
|
| 53 |
-
}
|
| 54 |
-
|
| 55 |
-
export function headroom(partner: Partner, outstanding: number): number {
|
| 56 |
-
return partner.credit_limit - outstanding;
|
| 57 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1,14 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"compilerOptions": {
|
| 3 |
-
"target": "ES2022",
|
| 4 |
-
"module": "ESNext",
|
| 5 |
-
"moduleResolution": "bundler",
|
| 6 |
-
"lib": ["ES2022", "DOM"],
|
| 7 |
-
// Turned off for the migration off the old client. Ticket FSM-412 tracks
|
| 8 |
-
// turning it back on once the generated RPC types land.
|
| 9 |
-
"strict": false,
|
| 10 |
-
"outDir": "dist",
|
| 11 |
-
"skipLibCheck": true
|
| 12 |
-
},
|
| 13 |
-
"include": ["src"]
|
| 14 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -65,7 +65,7 @@ CASES = [
|
|
| 65 |
(["rules"], OK, "the whole corpus"),
|
| 66 |
(["rules", "onchange"], OK, "a term that matches"),
|
| 67 |
(["rules", "zzzznomatch"], FINDINGS, "ran fine, found nothing -- NOT misuse"),
|
| 68 |
-
(["rules", "--stack", "
|
| 69 |
(["rules", "--full", "odoo.onchange-is-not-validation"], OK, "a real id"),
|
| 70 |
(["rules", "--full", "no.such.rule"], MISUSE, "was 1: an id that does not exist"),
|
| 71 |
(["validate"], OK, "corpus is schema-valid"),
|
|
|
|
| 65 |
(["rules"], OK, "the whole corpus"),
|
| 66 |
(["rules", "onchange"], OK, "a term that matches"),
|
| 67 |
(["rules", "zzzznomatch"], FINDINGS, "ran fine, found nothing -- NOT misuse"),
|
| 68 |
+
(["rules", "--stack", "odoo"], OK, "the one stack, named"),
|
| 69 |
(["rules", "--full", "odoo.onchange-is-not-validation"], OK, "a real id"),
|
| 70 |
(["rules", "--full", "no.such.rule"], MISUSE, "was 1: an id that does not exist"),
|
| 71 |
(["validate"], OK, "corpus is schema-valid"),
|
|
@@ -362,7 +362,7 @@ def test_doctor_names_a_missing_model(monkeypatch, capsys):
|
|
| 362 |
monkeypatch.setattr(nexa_cmds, "_gpu_line", lambda: None)
|
| 363 |
assert nexa_cmds.cmd_doctor([]) == 1
|
| 364 |
out = capsys.readouterr().out
|
| 365 |
-
assert "
|
| 366 |
assert "nomic-embed-text missing" in out
|
| 367 |
# Not installed and not required is a warning, not a problem: run works
|
| 368 |
# without it, it just cannot escalate.
|
|
|
|
| 362 |
monkeypatch.setattr(nexa_cmds, "_gpu_line", lambda: None)
|
| 363 |
assert nexa_cmds.cmd_doctor([]) == 1
|
| 364 |
out = capsys.readouterr().out
|
| 365 |
+
assert "nomic-embed-text missing" in out
|
| 366 |
assert "nomic-embed-text missing" in out
|
| 367 |
# Not installed and not required is a warning, not a problem: run works
|
| 368 |
# without it, it just cannot escalate.
|
|
@@ -23,29 +23,20 @@ def module(tmp_path, version="18.0.1.0.0", manifest=True):
|
|
| 23 |
return target
|
| 24 |
|
| 25 |
|
| 26 |
-
def
|
| 27 |
-
"""
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
-
The two modules disagreed for a while. .mts and .cts were TypeScript to this
|
| 30 |
-
one and fell through to Python in the router, so a .mts file was reviewed
|
| 31 |
-
against the Python corpus while this layer went looking for its tsconfig.
|
| 32 |
-
"""
|
| 33 |
-
for name, expected in (("a.ts", "typescript"), ("a.tsx", "typescript"),
|
| 34 |
-
("a.mts", "typescript"), ("a.cts", "typescript"),
|
| 35 |
-
("a.js", "javascript"), ("a.jsx", "javascript"),
|
| 36 |
-
("a.mjs", "javascript"), ("a.cjs", "javascript")):
|
| 37 |
-
assert context.stack_for(str(tmp_path / name)) == expected
|
| 38 |
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
"""
|
| 47 |
-
assert context.stack_for(str(tmp_path / "a.py")) == "python"
|
| 48 |
-
assert context.stack_for(str(tmp_path / "a.unknown")) == "python"
|
| 49 |
|
| 50 |
|
| 51 |
def test_the_caller_that_read_the_file_can_say_which_stack(tmp_path):
|
|
@@ -90,34 +81,6 @@ def test_series_is_the_first_two_components(tmp_path, version, series):
|
|
| 90 |
assert ctx["odoo_series"] == series
|
| 91 |
|
| 92 |
|
| 93 |
-
def test_tsconfig_strict_is_read(tmp_path):
|
| 94 |
-
(tmp_path / "tsconfig.json").write_text(
|
| 95 |
-
'{"compilerOptions": {"strict": true}}', encoding="utf-8")
|
| 96 |
-
target = tmp_path / "src.ts"
|
| 97 |
-
target.write_text("export const x = 1;", encoding="utf-8")
|
| 98 |
-
assert context.from_file(str(target))["ts_strict"] is True
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
def test_tsconfig_with_comments_is_still_read(tmp_path):
|
| 102 |
-
"""JSON with comments is not JSON, and tsconfig.json is full of them.
|
| 103 |
-
A parser that gave up here would report strict as unknown for most real
|
| 104 |
-
projects, which is the same false 'nothing to check' this layer used to give."""
|
| 105 |
-
(tmp_path / "tsconfig.json").write_text(
|
| 106 |
-
'{\n // the compiler options\n "compilerOptions": {\n'
|
| 107 |
-
' "strict": false /* off for now */\n }\n}', encoding="utf-8")
|
| 108 |
-
target = tmp_path / "src.ts"
|
| 109 |
-
target.write_text("export const x = 1;", encoding="utf-8")
|
| 110 |
-
assert context.from_file(str(target))["ts_strict"] is False
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
def test_missing_tsconfig_is_unknown_not_false(tmp_path):
|
| 114 |
-
"""strict off and strict unstated are different claims: one is a finding
|
| 115 |
-
about the project, the other is a gap in what was established."""
|
| 116 |
-
target = tmp_path / "src.ts"
|
| 117 |
-
target.write_text("export const x = 1;", encoding="utf-8")
|
| 118 |
-
assert context.from_file(str(target))["ts_strict"] is None
|
| 119 |
-
|
| 120 |
-
|
| 121 |
def test_empty_context_declares_that_it_has_no_file():
|
| 122 |
ctx = context.empty("odoo")
|
| 123 |
assert ctx["source"] == "none"
|
|
@@ -135,7 +98,7 @@ def test_unreadable_file_does_not_raise(tmp_path):
|
|
| 135 |
"""from_file runs before anything else in `nexa check`; an exception here
|
| 136 |
would take down the command rather than report a gap."""
|
| 137 |
ctx = context.from_file(str(tmp_path / "does_not_exist.py"))
|
| 138 |
-
assert ctx["stack"] == "
|
| 139 |
assert ctx["odoo_series"] is None
|
| 140 |
|
| 141 |
|
|
|
|
| 23 |
return target
|
| 24 |
|
| 25 |
|
| 26 |
+
def test_the_reviewable_extensions_are_the_ones_the_corpus_has_rules_for(tmp_path):
|
| 27 |
+
""".py holds every rule but one; .xml holds the noupdate rule, whose
|
| 28 |
+
violation is a <record> in a data file. Both answer with the one stack."""
|
| 29 |
+
for name in ("a.py", "views.xml"):
|
| 30 |
+
assert context.stack_for(str(tmp_path / name)) == "odoo"
|
| 31 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
+
def test_a_file_in_another_language_is_not_routed_anywhere(tmp_path):
|
| 34 |
+
"""None, not a stack. Running 27 Odoo rules over a .ts file produces an
|
| 35 |
+
empty finding list, and an empty finding list reads as a clean bill of
|
| 36 |
+
health -- so the router refuses instead, and subject.route raises."""
|
| 37 |
+
for name in ("a.ts", "a.tsx", "a.mts", "a.js", "a.jsx", "a.mjs",
|
| 38 |
+
"a.go", "a.unknown"):
|
| 39 |
+
assert context.stack_for(str(tmp_path / name)) is None
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
|
| 42 |
def test_the_caller_that_read_the_file_can_say_which_stack(tmp_path):
|
|
|
|
| 81 |
assert ctx["odoo_series"] == series
|
| 82 |
|
| 83 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
def test_empty_context_declares_that_it_has_no_file():
|
| 85 |
ctx = context.empty("odoo")
|
| 86 |
assert ctx["source"] == "none"
|
|
|
|
| 98 |
"""from_file runs before anything else in `nexa check`; an exception here
|
| 99 |
would take down the command rather than report a gap."""
|
| 100 |
ctx = context.from_file(str(tmp_path / "does_not_exist.py"))
|
| 101 |
+
assert ctx["stack"] == "odoo"
|
| 102 |
assert ctx["odoo_series"] is None
|
| 103 |
|
| 104 |
|
|
@@ -264,14 +264,14 @@ def test_literal_names_drop_the_metavariables():
|
|
| 264 |
|
| 265 |
def test_the_reverse_direction_is_restricted_to_their_languages(tmp_path,
|
| 266 |
all_rules):
|
| 267 |
-
"""A
|
| 268 |
-
|
|
|
|
|
|
|
| 269 |
rules, _ = crosswalk.read_ruleset(ruleset(tmp_path, SIMPLE))
|
| 270 |
-
assert crosswalk.covered_stacks(rules) == {"odoo"
|
| 271 |
outside = crosswalk.unreachable_here(all_rules, rules)
|
| 272 |
assert outside, "a two-rule ruleset reaches almost nothing"
|
| 273 |
-
assert not [rid for rid in outside
|
| 274 |
-
if rid.startswith(("typescript.", "javascript."))]
|
| 275 |
|
| 276 |
|
| 277 |
def test_a_rule_their_patterns_name_is_not_reported_as_unreachable(tmp_path,
|
|
|
|
| 264 |
|
| 265 |
def test_the_reverse_direction_is_restricted_to_their_languages(tmp_path,
|
| 266 |
all_rules):
|
| 267 |
+
"""A `languages: [python]` ruleset is about the one stack this corpus has.
|
| 268 |
+
A JavaScript-only one would be about none of it, and comparing across that
|
| 269 |
+
line would pad the gap with entries whose answer was known before the tool
|
| 270 |
+
ran."""
|
| 271 |
rules, _ = crosswalk.read_ruleset(ruleset(tmp_path, SIMPLE))
|
| 272 |
+
assert crosswalk.covered_stacks(rules) == {"odoo"}
|
| 273 |
outside = crosswalk.unreachable_here(all_rules, rules)
|
| 274 |
assert outside, "a two-rule ruleset reaches almost nothing"
|
|
|
|
|
|
|
| 275 |
|
| 276 |
|
| 277 |
def test_a_rule_their_patterns_name_is_not_reported_as_unreachable(tmp_path,
|
|
@@ -244,7 +244,7 @@ def cli(args, env=None):
|
|
| 244 |
{"NO_COLOR": "1", "TERM": "dumb"},
|
| 245 |
])
|
| 246 |
def test_documented_colour_opt_outs_emit_no_escapes(env):
|
| 247 |
-
code, out = cli(["rules", "--stack", "
|
| 248 |
assert code == 0
|
| 249 |
assert "\033" not in out, "an ANSI escape survived " + repr(env)
|
| 250 |
|
|
@@ -253,7 +253,7 @@ def test_colour_is_off_when_stdout_is_not_a_terminal():
|
|
| 253 |
"""A pipe is not a terminal, so colour is off even with nothing set.
|
| 254 |
Escape codes landing in a redirected file is the complaint that made
|
| 255 |
NO_COLOR a standard in the first place."""
|
| 256 |
-
code, out = cli(["rules", "--stack", "
|
| 257 |
assert code == 0
|
| 258 |
assert "\033" not in out
|
| 259 |
|
|
|
|
| 244 |
{"NO_COLOR": "1", "TERM": "dumb"},
|
| 245 |
])
|
| 246 |
def test_documented_colour_opt_outs_emit_no_escapes(env):
|
| 247 |
+
code, out = cli(["rules", "--stack", "odoo"], env)
|
| 248 |
assert code == 0
|
| 249 |
assert "\033" not in out, "an ANSI escape survived " + repr(env)
|
| 250 |
|
|
|
|
| 253 |
"""A pipe is not a terminal, so colour is off even with nothing set.
|
| 254 |
Escape codes landing in a redirected file is the complaint that made
|
| 255 |
NO_COLOR a standard in the first place."""
|
| 256 |
+
code, out = cli(["rules", "--stack", "odoo"])
|
| 257 |
assert code == 0
|
| 258 |
assert "\033" not in out
|
| 259 |
|
|
@@ -30,28 +30,19 @@ def test_python_names_include_methods():
|
|
| 30 |
assert evalmod.declared_names(body, "python") == ["Loader", "main", "parse_row"]
|
| 31 |
|
| 32 |
|
| 33 |
-
def
|
| 34 |
-
"""`
|
| 35 |
|
| 36 |
-
Counting them would score any answer that mentions
|
| 37 |
-
this file, which is the one thing this number exists to tell apart.
|
| 38 |
"""
|
| 39 |
-
body = (
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
'}\n')
|
| 46 |
-
assert evalmod.declared_names(body, "javascript") == ["ROUTES", "listVisits"]
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
def test_short_names_are_not_counted():
|
| 50 |
-
"""A three-letter name matches English prose by accident."""
|
| 51 |
-
assert evalmod.declared_names("def run():\n pass\n", "python") == []
|
| 52 |
-
|
| 53 |
|
| 54 |
-
# ---------------------------------------------------------------- gather
|
| 55 |
|
| 56 |
def test_gather_takes_files_as_given():
|
| 57 |
paths, skipped = evalmod.gather(evalmod.DEFAULT_SUBJECTS, 10)
|
|
@@ -63,12 +54,14 @@ def test_gather_walks_a_directory_and_skips_the_noise(tmp_path):
|
|
| 63 |
(tmp_path / "src").mkdir()
|
| 64 |
(tmp_path / "src" / "a.ts").write_text("export const a = 1;\n", encoding="utf-8")
|
| 65 |
(tmp_path / "src" / "b.py").write_text("x = 1\n", encoding="utf-8")
|
|
|
|
| 66 |
(tmp_path / "src" / "notes.md").write_text("# no\n", encoding="utf-8")
|
| 67 |
(tmp_path / "node_modules").mkdir()
|
| 68 |
(tmp_path / "node_modules" / "dep.js").write_text("//\n", encoding="utf-8")
|
| 69 |
|
| 70 |
paths, skipped = evalmod.gather([str(tmp_path)], 10)
|
| 71 |
-
assert [os.path.basename(p) for p in paths] == ["
|
|
|
|
| 72 |
assert skipped == 0
|
| 73 |
|
| 74 |
|
|
@@ -193,7 +186,7 @@ def test_a_failed_run_does_not_describe_the_file(capsys):
|
|
| 193 |
evalmod.summarise_check(failed + rows_for(["PASS"]), 2)
|
| 194 |
out = capsys.readouterr().out
|
| 195 |
assert "7929 chars" in out
|
| 196 |
-
assert "odoo
|
| 197 |
assert "ERRORS (1)" in out
|
| 198 |
|
| 199 |
|
|
@@ -371,18 +364,18 @@ def test_no_escalation_reports_no_rescue_rate(capsys):
|
|
| 371 |
|
| 372 |
|
| 373 |
def test_the_ask_summary_names_rules_that_never_surfaced(capsys):
|
| 374 |
-
"""Scoped to the
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
|
|
|
| 379 |
layers_with_findings=["domain"])]
|
| 380 |
evalmod.summarise(rows, 1, pipeline_mode=False)
|
| 381 |
out = capsys.readouterr().out
|
| 382 |
-
assert "RETRIEVAL (
|
| 383 |
-
assert "never
|
| 384 |
-
assert "
|
| 385 |
-
assert "1x javascript.json-parse-returns-any" in out
|
| 386 |
|
| 387 |
|
| 388 |
def test_the_ask_summary_says_so_when_no_checker_fired(capsys):
|
|
@@ -443,6 +436,6 @@ def test_a_pinned_rule_is_excluded_from_the_ranking(capsys):
|
|
| 443 |
def test_a_k_that_cannot_fail_says_so(capsys):
|
| 444 |
"""At k >= the size of the stack, every rule comes back and the test has
|
| 445 |
measured nothing. Printing a green tick for that is the failure mode."""
|
| 446 |
-
rows = [retrieval_row("
|
| 447 |
evalmod.summarise_retrieval(rows, k=40)
|
| 448 |
assert "nothing can miss" in capsys.readouterr().out
|
|
|
|
| 30 |
assert evalmod.declared_names(body, "python") == ["Loader", "main", "parse_row"]
|
| 31 |
|
| 32 |
|
| 33 |
+
def test_names_skip_locals_and_imported_bindings():
|
| 34 |
+
"""`row` is a parameter and `csv` is somebody else's name.
|
| 35 |
|
| 36 |
+
Counting them would score any answer that mentions CSV parsing as a review
|
| 37 |
+
of this file, which is the one thing this number exists to tell apart.
|
| 38 |
"""
|
| 39 |
+
body = ("import csv\n"
|
| 40 |
+
"\n"
|
| 41 |
+
"def load_prices(handle):\n"
|
| 42 |
+
" for row in csv.reader(handle):\n"
|
| 43 |
+
" yield row\n")
|
| 44 |
+
assert evalmod.declared_names(body, "odoo") == ["load_prices"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
|
|
|
| 46 |
|
| 47 |
def test_gather_takes_files_as_given():
|
| 48 |
paths, skipped = evalmod.gather(evalmod.DEFAULT_SUBJECTS, 10)
|
|
|
|
| 54 |
(tmp_path / "src").mkdir()
|
| 55 |
(tmp_path / "src" / "a.ts").write_text("export const a = 1;\n", encoding="utf-8")
|
| 56 |
(tmp_path / "src" / "b.py").write_text("x = 1\n", encoding="utf-8")
|
| 57 |
+
(tmp_path / "src" / "views.xml").write_text("<odoo/>\n", encoding="utf-8")
|
| 58 |
(tmp_path / "src" / "notes.md").write_text("# no\n", encoding="utf-8")
|
| 59 |
(tmp_path / "node_modules").mkdir()
|
| 60 |
(tmp_path / "node_modules" / "dep.js").write_text("//\n", encoding="utf-8")
|
| 61 |
|
| 62 |
paths, skipped = evalmod.gather([str(tmp_path)], 10)
|
| 63 |
+
assert [os.path.basename(p) for p in paths] == ["b.py", "views.xml"], \
|
| 64 |
+
"the .ts is not reviewable and is not walked into the set"
|
| 65 |
assert skipped == 0
|
| 66 |
|
| 67 |
|
|
|
|
| 186 |
evalmod.summarise_check(failed + rows_for(["PASS"]), 2)
|
| 187 |
out = capsys.readouterr().out
|
| 188 |
assert "7929 chars" in out
|
| 189 |
+
assert "odoo extension" in out
|
| 190 |
assert "ERRORS (1)" in out
|
| 191 |
|
| 192 |
|
|
|
|
| 364 |
|
| 365 |
|
| 366 |
def test_the_ask_summary_names_rules_that_never_surfaced(capsys):
|
| 367 |
+
"""Scoped to the stack that actually ran, and the never-retrieved list is
|
| 368 |
+
what the summary exists to print: a rule nothing surfaced is a rule no
|
| 369 |
+
question in the set could reach."""
|
| 370 |
+
rows = [pipeline_row(stack="odoo",
|
| 371 |
+
retrieved=["odoo.onchange-is-not-validation"],
|
| 372 |
+
rules_fired=["odoo.onchange-is-not-validation"],
|
| 373 |
layers_with_findings=["domain"])]
|
| 374 |
evalmod.summarise(rows, 1, pipeline_mode=False)
|
| 375 |
out = capsys.readouterr().out
|
| 376 |
+
assert "RETRIEVAL (odoo)" in out
|
| 377 |
+
assert "never odoo." in out, "the rules no question in the set reached"
|
| 378 |
+
assert "1x odoo.onchange-is-not-validation" in out
|
|
|
|
| 379 |
|
| 380 |
|
| 381 |
def test_the_ask_summary_says_so_when_no_checker_fired(capsys):
|
|
|
|
| 436 |
def test_a_k_that_cannot_fail_says_so(capsys):
|
| 437 |
"""At k >= the size of the stack, every rule comes back and the test has
|
| 438 |
measured nothing. Printing a green tick for that is the failure mode."""
|
| 439 |
+
rows = [retrieval_row("odoo.onchange-is-not-validation")]
|
| 440 |
evalmod.summarise_retrieval(rows, k=40)
|
| 441 |
assert "nothing can miss" in capsys.readouterr().out
|
|
@@ -137,12 +137,14 @@ def test_vendored_directories_are_skipped(tmp_path):
|
|
| 137 |
assert fieldtest.scan([root], "odoo")["files"] == 1
|
| 138 |
|
| 139 |
|
| 140 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
root = tree(tmp_path, {"a.py": CLEAN, "b.ts": "const x: any = 1;\n"})
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
assert typescript["files"] == 1
|
| 145 |
-
assert typescript["hits"]["typescript.unknown-not-any-at-boundaries"] == 1
|
| 146 |
|
| 147 |
|
| 148 |
def test_rules_that_never_fired_are_listed(tmp_path):
|
|
@@ -320,53 +322,13 @@ def test_the_never_deleted_count_is_a_set_difference(tmp_path):
|
|
| 320 |
|
| 321 |
# -------------------------------- what counts as the code under review (3.44)
|
| 322 |
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
"""
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
def test_odoo_static_is_javascript_and_not_assets(tmp_path):
|
| 331 |
-
"""SKIP_DIRS is not one list. Reviewing Python, static/ holds assets and
|
| 332 |
-
vendored bundles; reviewing JavaScript on an Odoo tree it holds ALL of it,
|
| 333 |
-
because every line of first-party JS an Odoo module ships lives under
|
| 334 |
-
static/src. Skipping it for both made `fieldtest --stack javascript`
|
| 335 |
-
answer "0 files" on any Odoo checkout, which is most of why those seven
|
| 336 |
-
rules had never been measured."""
|
| 337 |
-
root = tree(tmp_path, {"my_mod/static/src/js/widget.js": JS_SOURCE})
|
| 338 |
-
assert list(fieldtest.sources([root], "javascript", False)), "static skipped"
|
| 339 |
-
# And the python side keeps the exemption it has always had.
|
| 340 |
-
root2 = tree(tmp_path / "two", {"my_mod/static/src/thing.py": "x = 1\n"})
|
| 341 |
-
assert list(fieldtest.sources([root2], "python", False)) == []
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
def test_a_vendored_bundle_is_not_reviewed(tmp_path):
|
| 345 |
-
"""Recognised from the file rather than the directory, and by ordinary
|
| 346 |
-
JavaScript convention rather than anything Odoo-shaped: the name says so,
|
| 347 |
-
or one line is longer than any line a person types. OpenSPP's five vendored
|
| 348 |
-
libraries carry single lines of 32,071 to 375,248 characters; its 59
|
| 349 |
-
first-party files top out at 87."""
|
| 350 |
-
root = tree(tmp_path, {
|
| 351 |
-
"static/src/app.js": JS_SOURCE,
|
| 352 |
-
"static/lib/turf.min.js": "var t=1;\n",
|
| 353 |
-
"static/lib/maptiler.umd.js": "var x=" + ("a" * 2000) + ";\n",
|
| 354 |
-
})
|
| 355 |
-
found = [os.path.basename(f) for f in fieldtest.sources([root], "javascript", False)]
|
| 356 |
-
assert found == ["app.js"], found
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
def test_a_test_named_by_its_file_is_still_a_test(tmp_path):
|
| 360 |
-
"""_is_test_path reads directory components, which is where Python and Odoo
|
| 361 |
-
put tests. The JavaScript convention puts them beside the thing they test,
|
| 362 |
-
and 45 of the first 355 TypeScript hits were in files like hono.test.ts."""
|
| 363 |
-
root = tree(tmp_path, {
|
| 364 |
-
"src/app.ts": JS_SOURCE,
|
| 365 |
-
"src/app.test.ts": JS_SOURCE,
|
| 366 |
-
"src/app.spec.ts": JS_SOURCE,
|
| 367 |
-
})
|
| 368 |
-
found = [os.path.basename(f) for f in fieldtest.sources([root], "typescript", False)]
|
| 369 |
-
assert found == ["app.ts"], found
|
| 370 |
|
| 371 |
|
| 372 |
def test_a_filename_that_merely_contains_test_is_not_one(tmp_path):
|
|
@@ -378,15 +340,15 @@ def test_a_filename_that_merely_contains_test_is_not_one(tmp_path):
|
|
| 378 |
|
| 379 |
def test_two_matches_on_one_line_are_one_finding():
|
| 380 |
"""Pattern rules emitted one hit per regex match, and the evidence IS the
|
| 381 |
-
line -- so a line
|
| 382 |
-
word
|
| 383 |
import corpus
|
| 384 |
import layers
|
| 385 |
rules = [r for r in corpus.load_rules()
|
| 386 |
-
if r["id"] == "
|
| 387 |
assert rules, "rule missing from the corpus"
|
| 388 |
-
body = "
|
| 389 |
-
hits = layers.scan_source(body, rules, "
|
| 390 |
assert len(hits) == 1, hits
|
| 391 |
assert hits[0].line == 1
|
| 392 |
|
|
|
|
| 137 |
assert fieldtest.scan([root], "odoo")["files"] == 1
|
| 138 |
|
| 139 |
|
| 140 |
+
def test_only_python_is_read(tmp_path):
|
| 141 |
+
"""One stack, and the files it reviews are Python. A .ts file beside them
|
| 142 |
+
is not counted as reviewed and is not counted in the line total either --
|
| 143 |
+
a per-kloc rate whose denominator includes lines nothing ran over is the
|
| 144 |
+
quiet way that number gets better."""
|
| 145 |
root = tree(tmp_path, {"a.py": CLEAN, "b.ts": "const x: any = 1;\n"})
|
| 146 |
+
result = fieldtest.scan([root], "odoo")
|
| 147 |
+
assert result["files"] == 1
|
|
|
|
|
|
|
| 148 |
|
| 149 |
|
| 150 |
def test_rules_that_never_fired_are_listed(tmp_path):
|
|
|
|
| 322 |
|
| 323 |
# -------------------------------- what counts as the code under review (3.44)
|
| 324 |
|
| 325 |
+
def test_static_is_never_the_code_under_review(tmp_path):
|
| 326 |
+
"""It used to be one list per stack, because static/ holds every line of
|
| 327 |
+
first-party JavaScript an Odoo module ships. With the JS rules gone there
|
| 328 |
+
is nothing in there for the corpus to say anything about, so it is skipped
|
| 329 |
+
for good -- including the stray .py file people leave in it."""
|
| 330 |
+
root = tree(tmp_path, {"my_mod/static/src/thing.py": "x = 1\n"})
|
| 331 |
+
assert list(fieldtest.sources([root], "odoo", False)) == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 332 |
|
| 333 |
|
| 334 |
def test_a_filename_that_merely_contains_test_is_not_one(tmp_path):
|
|
|
|
| 340 |
|
| 341 |
def test_two_matches_on_one_line_are_one_finding():
|
| 342 |
"""Pattern rules emitted one hit per regex match, and the evidence IS the
|
| 343 |
+
line -- so a line with two matches on it reported itself twice, word for
|
| 344 |
+
word. The semantic checkers have always deduplicated."""
|
| 345 |
import corpus
|
| 346 |
import layers
|
| 347 |
rules = [r for r in corpus.load_rules()
|
| 348 |
+
if r["id"] == "python.mutable-default-argument"]
|
| 349 |
assert rules, "rule missing from the corpus"
|
| 350 |
+
body = "def f(a=[], b=[]):\n return a\n"
|
| 351 |
+
hits = layers.scan_source(body, rules, "odoo", errors=[])
|
| 352 |
assert len(hits) == 1, hits
|
| 353 |
assert hits[0].line == 1
|
| 354 |
|
|
@@ -156,23 +156,6 @@ def test_context_still_warns_about_a_rule_that_does_not_apply(odoo_rules):
|
|
| 156 |
assert all(f.rule for f in result.findings), "and each one names its rule"
|
| 157 |
|
| 158 |
|
| 159 |
-
def test_strict_off_is_a_gap_too():
|
| 160 |
-
"""Same shape as the series mismatch: true of the project, not of the
|
| 161 |
-
answer, and true on every run until somebody finishes the migration."""
|
| 162 |
-
result = layers.layer_context([], _ctx(stack="typescript", ts_strict=False))
|
| 163 |
-
assert result.findings == []
|
| 164 |
-
assert result.verdict == layers.UNKNOWN
|
| 165 |
-
assert any("strict off" in why for _, why in result.not_checked)
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
def test_strict_on_is_something_this_layer_checked():
|
| 169 |
-
result = layers.layer_context([], _ctx(stack="typescript", ts_strict=True))
|
| 170 |
-
assert result.verdict == layers.PASS
|
| 171 |
-
assert "tsconfig strict" in result.checked
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
# ------------------------------------------------------------ semantic gating
|
| 175 |
-
|
| 176 |
def test_lifted_fragment_moves_imports_out_of_findings(odoo_rules):
|
| 177 |
"""Two medium findings for imports a method fragment could never carry were
|
| 178 |
enough to turn this correct answer into WARN."""
|
|
|
|
| 156 |
assert all(f.rule for f in result.findings), "and each one names its rule"
|
| 157 |
|
| 158 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
def test_lifted_fragment_moves_imports_out_of_findings(odoo_rules):
|
| 160 |
"""Two medium findings for imports a method fragment could never carry were
|
| 161 |
enough to turn this correct answer into WARN."""
|
|
@@ -33,10 +33,17 @@ def rules_events(retrieved, violated=()):
|
|
| 33 |
def test_corpus_rule_ids_reads_the_whole_corpus():
|
| 34 |
ids = report._corpus_rule_ids()
|
| 35 |
assert ids is not None
|
| 36 |
-
assert len(ids) >=
|
| 37 |
assert "odoo.prefer-sql-constraint" in ids
|
| 38 |
-
assert any(i.startswith("
|
| 39 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
|
| 42 |
def test_corpus_rule_ids_returns_none_when_unreadable(monkeypatch, tmp_path):
|
|
@@ -74,19 +81,22 @@ def test_absence_is_not_reported_as_a_corpus_defect(capsys):
|
|
| 74 |
assert "nexa eval --retrieval" in out, "and the real measurement pointed at"
|
| 75 |
|
| 76 |
|
| 77 |
-
def
|
| 78 |
-
"""Retrieval is scoped to
|
| 79 |
-
|
|
|
|
| 80 |
report.cmd_rules(rules_events(["odoo.prefer-sql-constraint"]))
|
| 81 |
out = capsys.readouterr().out
|
| 82 |
assert "queries recorded, by stack:" in out
|
| 83 |
-
|
| 84 |
-
|
|
|
|
| 85 |
|
| 86 |
|
| 87 |
def test_full_coverage_says_so_rather_than_printing_nothing(capsys, monkeypatch):
|
| 88 |
-
monkeypatch.setattr(report, "_corpus_rule_ids",
|
| 89 |
-
|
|
|
|
| 90 |
assert "every rule has surfaced" in capsys.readouterr().out
|
| 91 |
|
| 92 |
|
|
|
|
| 33 |
def test_corpus_rule_ids_reads_the_whole_corpus():
|
| 34 |
ids = report._corpus_rule_ids()
|
| 35 |
assert ids is not None
|
| 36 |
+
assert len(ids) >= 27
|
| 37 |
assert "odoo.prefer-sql-constraint" in ids
|
| 38 |
+
assert any(i.startswith("python.") for i in ids), "the composed-in half"
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def test_a_rules_stack_comes_off_the_rule_and_not_off_its_id():
|
| 42 |
+
"""python.decimal-for-money-not-float is in the odoo stack. Splitting the
|
| 43 |
+
id on the dot instead invents a stack with eight rules and no queries, and
|
| 44 |
+
then reports all eight as unreachable by arithmetic."""
|
| 45 |
+
ids = report._corpus_rule_ids()
|
| 46 |
+
assert set(ids.values()) == {"odoo"}
|
| 47 |
|
| 48 |
|
| 49 |
def test_corpus_rule_ids_returns_none_when_unreadable(monkeypatch, tmp_path):
|
|
|
|
| 81 |
assert "nexa eval --retrieval" in out, "and the real measurement pointed at"
|
| 82 |
|
| 83 |
|
| 84 |
+
def test_absence_arithmetic_counts_the_stack_the_rules_are_actually_in(capsys):
|
| 85 |
+
"""Retrieval is scoped to a stack, so the headroom is per stack. There is
|
| 86 |
+
one, and every rule is in it -- including the eight whose id says python,
|
| 87 |
+
which must not be counted as a stack of their own with no queries."""
|
| 88 |
report.cmd_rules(rules_events(["odoo.prefer-sql-constraint"]))
|
| 89 |
out = capsys.readouterr().out
|
| 90 |
assert "queries recorded, by stack:" in out
|
| 91 |
+
tail = out.split("queries recorded, by stack:")[1]
|
| 92 |
+
assert "odoo" in tail
|
| 93 |
+
assert "python" not in tail, "python is an id prefix, not a stack"
|
| 94 |
|
| 95 |
|
| 96 |
def test_full_coverage_says_so_rather_than_printing_nothing(capsys, monkeypatch):
|
| 97 |
+
monkeypatch.setattr(report, "_corpus_rule_ids",
|
| 98 |
+
lambda: {"odoo.one": "odoo", "odoo.two": "odoo"})
|
| 99 |
+
report.cmd_rules(rules_events(["odoo.one", "odoo.two"]))
|
| 100 |
assert "every rule has surfaced" in capsys.readouterr().out
|
| 101 |
|
| 102 |
|
|
@@ -31,42 +31,52 @@ def plan_for(tmp_path, name, body, forced=None):
|
|
| 31 |
return subject.plan(str(path), forced)
|
| 32 |
|
| 33 |
|
| 34 |
-
def
|
| 35 |
plan = plan_for(tmp_path, "models/thing.py", ODOO)
|
| 36 |
assert plan["stack"] == "odoo"
|
| 37 |
-
assert
|
|
|
|
| 38 |
|
| 39 |
|
| 40 |
-
def
|
| 41 |
-
"""
|
| 42 |
-
|
|
|
|
| 43 |
plan = plan_for(tmp_path, "load.py", "import csv\n\n\ndef main():\n pass\n")
|
| 44 |
-
assert plan["stack"] == "
|
|
|
|
| 45 |
|
| 46 |
|
| 47 |
-
def
|
| 48 |
-
"""A view mixin need not import odoo itself to be part of a module.
|
|
|
|
|
|
|
| 49 |
(tmp_path / "mod").mkdir()
|
| 50 |
(tmp_path / "mod" / "__manifest__.py").write_text(
|
| 51 |
'{"name": "m", "version": "17.0.1.0.0"}', encoding="utf-8")
|
| 52 |
plan = plan_for(tmp_path, "mod/models/helper.py", "def helper():\n return 1\n")
|
| 53 |
assert plan["stack"] == "odoo"
|
| 54 |
-
assert
|
| 55 |
|
| 56 |
|
| 57 |
-
@pytest.mark.parametrize("name
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
|
|
|
| 63 |
|
| 64 |
|
| 65 |
def test_stack_can_be_forced_and_says_so(tmp_path):
|
| 66 |
plan = plan_for(tmp_path, "load.py", "import csv\n", forced="odoo")
|
| 67 |
assert plan["stack"] == "odoo"
|
| 68 |
assert plan["routed_by"] == "--stack"
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
|
| 71 |
|
| 72 |
def test_a_file_over_budget_loses_whole_symbols_and_names_them(tmp_path):
|
|
@@ -93,9 +103,7 @@ def test_a_file_that_fits_says_nothing(tmp_path):
|
|
| 93 |
SUBJECT_ROUTES = [
|
| 94 |
("odoo_module/models/fsm_job.py", "odoo"),
|
| 95 |
("odoo_module/models/fsm_visit.py", "odoo"),
|
| 96 |
-
("etl/load_prices.py", "
|
| 97 |
-
("web/src/partners.ts", "typescript"),
|
| 98 |
-
("web/src/board.js", "javascript"),
|
| 99 |
]
|
| 100 |
|
| 101 |
|
|
@@ -103,9 +111,9 @@ SUBJECT_ROUTES = [
|
|
| 103 |
def test_every_subject_reaches_the_route_it_was_written_for(relative, stack):
|
| 104 |
"""subjects/ is the input set for `nexa eval --check`.
|
| 105 |
|
| 106 |
-
A subject that stops
|
| 107 |
-
|
| 108 |
-
distribution, with one of
|
| 109 |
"""
|
| 110 |
plan = subject.plan(os.path.join(SUBJECTS, *relative.split("/")))
|
| 111 |
assert plan["stack"] == stack
|
|
@@ -127,12 +135,14 @@ def test_no_subject_is_mistaken_for_a_test_file():
|
|
| 127 |
|
| 128 |
|
| 129 |
def test_the_subjects_carry_the_surroundings_the_context_layer_reads():
|
| 130 |
-
"""
|
|
|
|
| 131 |
odoo = subject.plan(os.path.join(SUBJECTS, "odoo_module", "models", "fsm_job.py"))
|
| 132 |
assert odoo["ctx"]["odoo_series"] == "17.0"
|
| 133 |
|
| 134 |
-
|
| 135 |
-
assert
|
|
|
|
| 136 |
|
| 137 |
|
| 138 |
def test_the_flagged_symbol_survives_the_budget():
|
|
|
|
| 31 |
return subject.plan(str(path), forced)
|
| 32 |
|
| 33 |
|
| 34 |
+
def test_a_py_file_that_imports_odoo_is_reviewed(tmp_path):
|
| 35 |
plan = plan_for(tmp_path, "models/thing.py", ODOO)
|
| 36 |
assert plan["stack"] == "odoo"
|
| 37 |
+
assert plan["routed_by"] == "extension"
|
| 38 |
+
assert not plan["notes"], "an Odoo file needs no caveat"
|
| 39 |
|
| 40 |
|
| 41 |
+
def test_a_py_file_with_no_odoo_in_it_is_still_reviewed_and_says_so(tmp_path):
|
| 42 |
+
"""There is no second corpus to send it to. The 19 ORM rules have nothing
|
| 43 |
+
to match in a plain script, the 8 plain-Python rules do, and the report
|
| 44 |
+
says which of the two it is rather than leaving the reader to guess."""
|
| 45 |
plan = plan_for(tmp_path, "load.py", "import csv\n\n\ndef main():\n pass\n")
|
| 46 |
+
assert plan["stack"] == "odoo"
|
| 47 |
+
assert any("plain-Python" in n for n in plan["notes"])
|
| 48 |
|
| 49 |
|
| 50 |
+
def test_a_manifest_above_the_file_counts_as_odoo_in_the_report(tmp_path):
|
| 51 |
+
"""A view mixin need not import odoo itself to be part of a module. That no
|
| 52 |
+
longer decides which rules run -- one corpus does -- but it does decide
|
| 53 |
+
whether the report calls this an Odoo file."""
|
| 54 |
(tmp_path / "mod").mkdir()
|
| 55 |
(tmp_path / "mod" / "__manifest__.py").write_text(
|
| 56 |
'{"name": "m", "version": "17.0.1.0.0"}', encoding="utf-8")
|
| 57 |
plan = plan_for(tmp_path, "mod/models/helper.py", "def helper():\n return 1\n")
|
| 58 |
assert plan["stack"] == "odoo"
|
| 59 |
+
assert not plan["notes"]
|
| 60 |
|
| 61 |
|
| 62 |
+
@pytest.mark.parametrize("name", ["app.ts", "app.tsx", "app.js", "app.jsx",
|
| 63 |
+
"app.mjs", "app.go"])
|
| 64 |
+
def test_a_file_in_another_language_is_refused_not_reviewed(tmp_path, name):
|
| 65 |
+
"""The rules that used to answer for these are gone, and silence about a
|
| 66 |
+
file nobody checked is the one lie this tool is built not to tell."""
|
| 67 |
+
with pytest.raises(subject.Unreviewable):
|
| 68 |
+
plan_for(tmp_path, name, "export const x = 1;\n")
|
| 69 |
|
| 70 |
|
| 71 |
def test_stack_can_be_forced_and_says_so(tmp_path):
|
| 72 |
plan = plan_for(tmp_path, "load.py", "import csv\n", forced="odoo")
|
| 73 |
assert plan["stack"] == "odoo"
|
| 74 |
assert plan["routed_by"] == "--stack"
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def test_forcing_a_stack_that_does_not_exist_is_refused(tmp_path):
|
| 78 |
+
with pytest.raises(subject.Unreviewable):
|
| 79 |
+
plan_for(tmp_path, "load.py", "import csv\n", forced="typescript")
|
| 80 |
|
| 81 |
|
| 82 |
def test_a_file_over_budget_loses_whole_symbols_and_names_them(tmp_path):
|
|
|
|
| 103 |
SUBJECT_ROUTES = [
|
| 104 |
("odoo_module/models/fsm_job.py", "odoo"),
|
| 105 |
("odoo_module/models/fsm_visit.py", "odoo"),
|
| 106 |
+
("etl/load_prices.py", "odoo"),
|
|
|
|
|
|
|
| 107 |
]
|
| 108 |
|
| 109 |
|
|
|
|
| 111 |
def test_every_subject_reaches_the_route_it_was_written_for(relative, stack):
|
| 112 |
"""subjects/ is the input set for `nexa eval --check`.
|
| 113 |
|
| 114 |
+
A subject that stops being reviewable silently changes what the
|
| 115 |
+
measurement covers -- the eval would still print its files and its
|
| 116 |
+
distribution, with one of them quietly missing from the set.
|
| 117 |
"""
|
| 118 |
plan = subject.plan(os.path.join(SUBJECTS, *relative.split("/")))
|
| 119 |
assert plan["stack"] == stack
|
|
|
|
| 135 |
|
| 136 |
|
| 137 |
def test_the_subjects_carry_the_surroundings_the_context_layer_reads():
|
| 138 |
+
"""The module exists to give that layer something real to say: it pins
|
| 139 |
+
17.0, one series below the 18.0 the corpus was checked against."""
|
| 140 |
odoo = subject.plan(os.path.join(SUBJECTS, "odoo_module", "models", "fsm_job.py"))
|
| 141 |
assert odoo["ctx"]["odoo_series"] == "17.0"
|
| 142 |
|
| 143 |
+
script = subject.plan(os.path.join(SUBJECTS, "etl", "load_prices.py"))
|
| 144 |
+
assert script["ctx"]["odoo_series"] is None
|
| 145 |
+
assert any("plain-Python" in n for n in script["notes"])
|
| 146 |
|
| 147 |
|
| 148 |
def test_the_flagged_symbol_survives_the_budget():
|
|
@@ -27,7 +27,7 @@ import corpus
|
|
| 27 |
def good_rule(**overrides):
|
| 28 |
rule = {
|
| 29 |
"id": "python.a-sample-rule",
|
| 30 |
-
"stack": "
|
| 31 |
"severity": "high",
|
| 32 |
# The second axis. `unmeasured` is the default the build gives a rule
|
| 33 |
# nothing has placed, and it derives advisory false.
|
|
@@ -50,7 +50,7 @@ def corpus_dir(tmp_path, monkeypatch):
|
|
| 50 |
path.mkdir()
|
| 51 |
monkeypatch.setattr(corpus, "RULES_DIR", str(path))
|
| 52 |
|
| 53 |
-
def write(rules, name="
|
| 54 |
with io.open(str(path / name), "w", encoding="utf-8") as handle:
|
| 55 |
handle.write(json.dumps(rules, ensure_ascii=False))
|
| 56 |
|
|
@@ -108,11 +108,22 @@ def test_a_duplicate_id_fails(corpus_dir, capsys):
|
|
| 108 |
assert any("duplicate id" in line for line in failures(capsys))
|
| 109 |
|
| 110 |
|
| 111 |
-
def
|
| 112 |
-
|
|
|
|
|
|
|
|
|
|
| 113 |
assert corpus.cmd_validate() == 1
|
| 114 |
-
assert any("id prefix
|
| 115 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
|
| 117 |
|
| 118 |
def test_ui_only_enforcement_must_say_what_bypasses_it(corpus_dir, capsys):
|
|
|
|
| 27 |
def good_rule(**overrides):
|
| 28 |
rule = {
|
| 29 |
"id": "python.a-sample-rule",
|
| 30 |
+
"stack": "odoo",
|
| 31 |
"severity": "high",
|
| 32 |
# The second axis. `unmeasured` is the default the build gives a rule
|
| 33 |
# nothing has placed, and it derives advisory false.
|
|
|
|
| 50 |
path.mkdir()
|
| 51 |
monkeypatch.setattr(corpus, "RULES_DIR", str(path))
|
| 52 |
|
| 53 |
+
def write(rules, name="odoo.json"):
|
| 54 |
with io.open(str(path / name), "w", encoding="utf-8") as handle:
|
| 55 |
handle.write(json.dumps(rules, ensure_ascii=False))
|
| 56 |
|
|
|
|
| 108 |
assert any("duplicate id" in line for line in failures(capsys))
|
| 109 |
|
| 110 |
|
| 111 |
+
def test_an_id_with_an_unknown_origin_prefix_fails(corpus_dir, capsys):
|
| 112 |
+
"""The prefix names where a rule came from -- odoo. or python. -- and it is
|
| 113 |
+
what every measurement and every citation is keyed on. A rule whose prefix
|
| 114 |
+
is neither is orphaned from its own classification artifact, silently."""
|
| 115 |
+
corpus_dir([good_rule(id="ruff.a-sample-rule")])
|
| 116 |
assert corpus.cmd_validate() == 1
|
| 117 |
+
assert any("id prefix is not one of" in line for line in failures(capsys))
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def test_both_origin_prefixes_are_accepted_under_the_one_stack(corpus_dir):
|
| 121 |
+
"""python.decimal-for-money-not-float carries stack odoo. The prefix is
|
| 122 |
+
provenance, the stack is selection, and they are allowed to differ."""
|
| 123 |
+
corpus_dir([good_rule(id="odoo.a-sample-rule"),
|
| 124 |
+
good_rule(id="python.another-sample-rule",
|
| 125 |
+
title="A second sample rule for the validator")])
|
| 126 |
+
assert corpus.cmd_validate() == 0
|
| 127 |
|
| 128 |
|
| 129 |
def test_ui_only_enforcement_must_say_what_bypasses_it(corpus_dir, capsys):
|