diff --git a/whitepaper/outreach/wave17-php-python-bundler-survey.md b/whitepaper/outreach/wave17-php-python-bundler-survey.md new file mode 100644 index 000000000..bd0217f97 --- /dev/null +++ b/whitepaper/outreach/wave17-php-python-bundler-survey.md @@ -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>` (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` — 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