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.
9.3 KiB
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:
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_flipmap - 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:
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
- 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.
- pyright callHierarchyProvider follow-up — convert to Map<"uri|start|end", entry>; check downstream consumers.
- Scanner enhancement: Rust FxHashSet/FxHashMap awareness — same as Wave 11 HashSet gap; ruff reinforces.
- 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
unmoaddetection 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