wave17 survey: 6 clean-scan additions (flask, black, mypy, sanic, vite, prettier)

10 PHP fw / Python tooling / JS bundler targets scanned. Honor roll
cumulative: 86 projects. Two borderline candidates documented for
type-aware follow-up (symfony PropertyAccessor::writeCollection O(P*C),
pyright callHierarchyProvider O(C^2)) — both real but need type-aware
helpers, not single-line set hoists.
This commit is contained in:
russell@unturf.com 2026-04-25 15:10:56 -04:00
parent 878876c7af
commit e55ba1608e
No known key found for this signature in database

View file

@ -0,0 +1,116 @@
# Wave 17 — PHP Frameworks, Python Tooling, JS Bundlers
**Survey date:** 2026-04-25
**Tool:** unmoad (9 active MOAD detectors, HIGH+ severity filter)
**Scope:** 10 projects across PHP frameworks (laravel, symfony), Python web frameworks (flask, sanic), Python tooling (ruff, black, mypy, pyright), and JS bundler/formatter (vite, prettier).
---
## Summary
Wave 17 totals 1,370 HIGH+ findings across 10 projects. **Six new clean-scan honor roll entries** (flask, black, mypy, sanic, vite, prettier). Honor roll cumulative: **86 projects** across waves 3-17.
**No flagship CWE-407 patches ship this pass.** Two borderline-real candidates documented (symfony PropertyAccessor::writeCollection O(P × C) collection diff, pyright CallHierarchyProvider O(C²) outgoing-call dedup) but both require careful type-handling work beyond a single set-hoist. Logged for follow-up rather than shipped half-baked.
## Clean-scan honor roll — 6 new entries
| Project | Lang | Role | Notes |
|---------|------|------|-------|
| **flask** | Python | Microframework | 6 findings: `_cv_app: ContextVar[AppContext]` is intentional Flask app-context plumbing (the framework's central design); `cache.set` and `secrets.token_hex()` references are docs samples. **clean** |
| **black** | Python | Code formatter | 13 findings: `pgen.py:54/56` `dfa.index()` for pgen2 grammar build (one-time at startup); `comments.py:143` `remainder.count("\n")` is single-char count not list-element scan; `ipynb_magics.py` mask matching bounded. **clean** |
| **mypy** | Python | Static type checker | 36 findings: `mypyc/test-data/` test fixtures, `mypyc/lib-rt` strncmp on fixed function names, `imaplib.pyi` / `smtplib.pyi` `cram_md5` API names (typeshed reference, not implementation). **clean** |
| **sanic** | Python | Async web framework | 56 findings: most in `examples/`, `guide/` docs, `scripts/release.py`, vendored livereload.js. ContextVar.set patterns are intentional request-id propagation. **clean** |
| **vite** | TS | Frontend build tool | 77 findings: ALL in `playground/__tests__/` test specs (CSS sourcemap tests, HMR SSR tests, asset tests). Core vite clean. |
| **prettier** | JS | Opinionated code formatter | 52 findings: `String.includes(needle)` substring checks (literal print, template-literal print, srcset descriptor, CLI argv flag). Bounded by source-text length per print call. **clean** |
Honor roll now stands at **86 projects** validated zero-real-finding under MOAD scanning.
## Per-target findings
| Project | Lang | Total | M1 | M3 | M4 | M5 | M6 | M7 | M9 | M11 | Triage |
|---------|------|------:|---:|---:|---:|---:|---:|---:|---:|----:|--------|
| symfony | PHP/JS | 520 | 232 | - | 109 | - | 9 | 96 | - | 74 | 58 in vendored mermaid-flowchart. `PropertyAccessor::writeCollection` real but complex — see investigation. `LokaliseProvider`, `MandrillApiTransport` use bounded fixed enums. |
| ruff | Rust | 388 | 166 | 30 | 6 | - | - | 172 | 2 | 12 | `completion.rs:1142` `existing_class_bases.contains``existing_class_bases: Option<FxHashSet<Name>>` (O(1)). `nodes.rs` `InterpolatedStringFlagsInner::TRIPLE_QUOTED.contains` is bitflags (Wave 9 pattern). `shell_injection.rs` `text.contains('*')` is single-char substring. |
| framework (laravel) | PHP | 112 | 36 | - | 1 | - | 7 | 42 | 10 | 16 | `in_array` patterns on small bounded enums (rules, attributes, methods). PHP idiom for fixed validation rule lists. |
| pyright | TS | 110 | 80 | - | 6 | 4 | 11 | 3 | 6 | - | `typeEvaluator.ts:1670/1671` `findIndex` per evaluation (bounded by string concat count). `docStringUtils.ts` paramOffset substring search bounded by docstring length. `callHierarchyProvider` real but borderline — see investigation. |
| **vite** | TS | 77 | 72 | - | 1 | 1 | 3 | - | - | - | All in playground tests. **clean** |
| **sanic** | Python | 56 | 5 | 44 | 2 | - | 2 | 1 | - | 2 | Examples + docs + ContextVar. **clean** |
| **prettier** | JS | 52 | 47 | - | 5 | - | - | - | - | - | Substring checks bounded by source text. **clean** |
| **mypy** | Python | 36 | 19 | 2 | 1 | - | 3 | 3 | - | 8 | Test fixtures + stub files. **clean** |
| **black** | Python | 13 | 8 | 2 | 1 | - | - | - | - | 2 | pgen2 grammar build + char count. **clean** |
| **flask** | Python | 6 | - | 2 | 3 | 1 | - | - | - | - | Intentional Flask context-var design. **clean** |
## Investigations: borderline real, not patch-shipped this pass
### symfony `PropertyAccessor::writeCollection` — O(P × C) collection diff
`src/Symfony/Component/PropertyAccess/PropertyAccessor.php:580-595` does a collection diff for entity property updates:
```php
foreach ($previousValue as $key => $item) {
if (!\in_array($item, $collection, true)) { // O(C) per call
// remove $item
}
}
foreach ($collection as $item) {
if (!$previousValue || !\in_array($item, $previousValue, true)) { // O(P) per call
// add $item
}
}
```
For P previous items × C new items, total cost is **O(P × C)**. This runs on every form submission with a CollectionType field and on every PropertyAccessor write to a collection-valued entity property.
**Why not patch-shipped this pass:** `\in_array(..., true)` with `$strict=true` on objects checks identity (===), but on scalars checks `===`. The collection can contain mixed types (objects, scalars, arrays). A correct fix needs:
- Objects → `SplObjectStorage` (O(1) identity lookup)
- Hashable scalars (strings, ints, bools) → `array_flip` map
- Arrays / non-hashable → fall back to `\in_array` (rare in practice)
Single-line set hoist isn't sufficient. The fix is mechanical but needs careful type-dispatch code. Logged for a follow-up patch with type-aware lookup helper.
Real-world scale: a Symfony entity with a `OneToMany` association of 500 items being updated to 500 different items pays 250k strict-equality comparisons per write. For most Symfony apps with smaller collections (5-50 items), the cost is invisible. Worth fixing for ORM-heavy apps.
### pyright `CallHierarchyProvider._outgoingCalls.find` — O(C²) call dedup
`packages/pyright-internal/src/languageService/callHierarchyProvider.ts:394-396`:
```ts
let outgoingCall = this._outgoingCalls.find(
(outgoing) => outgoing.to.uri === callDest.uri && rangesAreEqual(outgoing.to.range, callDest.range)
);
```
Per discovered call expression, linear scan over already-recorded outgoing calls to dedup. For a function with C call expressions where many resolve to distinct destinations, total cost is O(C²).
**Why not patch-shipped this pass:** the dedup key is `(uri, range)` — a composite where `range` is a `{start, end}` struct. JS Map needs a string key. We can serialize as `${uri}|${start}|${end}`, but it's worth checking whether the existing Map representation downstream relies on object identity. Logged for follow-up after reading callers.
Real-world scale: typical "show outgoing calls from this function" query has 10-50 unique destinations. At 50 the constant is 2500 ops — invisible in the IDE. For glue functions with 200+ call sites, becomes measurable (40k ops). Below patch-grade today.
### ruff `existing_class_bases.contains` — false positive on FxHashSet
Already a `FxHashSet<Name>` — O(1) contains. Same scanner gap as Wave 11 (Rust HashSet declared-type awareness).
### ruff `InterpolatedStringFlagsInner::TRIPLE_QUOTED.contains` — bitflags FP
Same pattern as helix/alacritty/wezterm in Wave 9. `bitflags::bitflags!` macro generates `.contains(other)` that compiles to bitwise AND.
### laravel + symfony `in_array` clusters — fixed enum patterns
Across both PHP frameworks, `\in_array($key, $rules, true)` and similar patterns are checking against fixed enums (validation rule names, allowed mutator methods, allowed transport headers). These are PHP's idiomatic fixed-enum check — 5-50 entries — and fast at this scale.
## Triage backlog
1. **symfony PropertyAccessor::writeCollection follow-up** — write a type-aware collection-membership helper (SplObjectStorage for objects, array_flip for hashable scalars, fallback for the rest). Patch is real but needs careful type dispatch.
2. **pyright callHierarchyProvider follow-up** — convert to Map<"uri|start|end", entry>; check downstream consumers.
3. **Scanner enhancement: Rust FxHashSet/FxHashMap awareness** — same as Wave 11 HashSet gap; ruff reinforces.
4. **Scanner enhancement: bitflags::bitflags! macro suppression** — already on backlog from Wave 9.
## Method
Same as Waves 3-16: shallow clone, `unmoad -s high -f json`, filter test/vendor/UI noise, manual triage of strongest source-only candidates per project. **Six projects added to clean-scan honor roll.** No new UNDF IDs assigned (no patches shipped — two real candidates logged for type-aware follow-up).
## References
- `unmoad` detection engine: `git.unturf.com/engineering/unmoad.com`
- Earlier surveys: `/test-harness-survey/`, `/wave4-linter-ci-survey/`, `/wave5-cicd-iac-survey/`, `/wave6-docgen-webfw-tui-survey/`, `/docs-pipeline-survey/`, `/wave7-mail-dns-storage-vpn-rtos-survey/`, `/wave8-observability-streaming-survey/`, `/wave9-image-pdf-db-editors-survey/`, `/wave10-crypto-text-geo-flutter-survey/`, `/wave11-unix-search-ml-survey/`, `/wave12-sci-static-site-api-gateway-survey/`, `/wave13-vms-devtools-graphql-survey/`, `/wave14-mobile-edge-pl-wm-survey/`, `/wave15-security-data-eng-ide-survey/`, `/wave16-cms-workflow-bio-node-survey/`
- Clean-scan honor roll cumulative: 86 projects across waves 3-17