java-topology/whitepaper/outreach/wave17-php-python-bundler-survey.md
russell@unturf.com e55ba1608e
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.
2026-04-25 15:10:56 -04:00

9.3 KiB
Raw Permalink Blame History

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.containsexisting_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_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:

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