wave4: psalm-0001 flagship patch + linter/CI/config scan survey

psalm-0001: FileFilter.allowsClass runs in_array() on every class the
analyzer visits. For C classes and F filter entries, per-run cost is
O(C*F). Fix: lazy array_fill_keys hash set; O(1) probe per class.
Bench: 336x at C=F=5000. Patch + ticket + bench + intel brief ship.

wave4-linter-ci-survey.md: consolidated report on 40 projects scanned
across linters (eslint, biome, prettier, pylint, ruff, black, rubocop,
shellcheck, stylelint, sqlfluff, phpstan, PHP_CodeSniffer, psalm,
rustfmt, golangci-lint, scalafmt, hadolint, yamllint, markdownlint,
ktlint, detekt), CI runners (act, buildkite-agent, tektoncd/pipeline,
concourse, woodpecker), config management (aws-cdk, cdk8s, kustomize),
and build tools (rollup, parcel, vite, turborepo, nx, lerna, swc,
babel, gulp).

Clean scans (zero HIGH+ findings): hadolint, shellcheck, gulp.

Document includes per-target finding counts and 7 triage follow-ups for
future waves (PHP_CodeSniffer ReDoS, ktlint spacing rule, pylint
MSG_ORDER.index, black pgen2 dfa, golangci-lint migrate, tektoncd
forbidden-env scan, aws-cdk region-info).
This commit is contained in:
russell@unturf.com 2026-04-24 17:21:51 -04:00
parent 788514bcf7
commit bb81a1a3a0
No known key found for this signature in database
9 changed files with 384 additions and 1 deletions

View file

@ -0,0 +1,73 @@
# psalm-0001: FileFilter.allowsClass — O(C×F) linear scan per class analyzed
**Target:** vimeo/psalm
**Severity:** MEDIUM
**CWE:** CWE-407 (Inefficient Algorithmic Complexity)
**MOAD:** MOAD-0001 (A Sedimentary Defect)
**File:** `src/Psalm/Config/FileFilter.php:573-584`
**Language:** PHP
**Status:** open
## Description
`Psalm\Config\FileFilter::allowsClass()` is called during static analysis once per fully-qualified class the analyzer visits. The method iterates an optional list of regex patterns, then falls back to `in_array(strtolower($fq_classlike_name), $this->fq_classlike_names, true)` — an O(F) linear scan of the configured class-name filter list.
For a project with C classes and a filter list of size F (common in large codebases that carefully scope Psalm analysis), per-analysis cost is O(C×F). Filter lists commonly hit hundreds of entries on large monorepos.
## Root Cause
```php
public function allowsClass(string $fq_classlike_name): bool
{
if ($this->fq_classlike_patterns) {
foreach ($this->fq_classlike_patterns as $pattern) {
if (preg_match($pattern, $fq_classlike_name)) {
return true;
}
}
}
return in_array(strtolower($fq_classlike_name), $this->fq_classlike_names, true);
}
```
`in_array` is O(F) and runs on every class the analyzer encounters. C class × F filter = O(C×F).
## Fix
Pre-lowercase `$this->fq_classlike_names` once and store as an associative array (hash set). `isset($this->fq_classlike_names_set[$lowered])` is O(1). Keep the patterns-first path unchanged.
```php
private ?array $fq_classlike_names_set = null; // lazy O(1) lookup
public function allowsClass(string $fq_classlike_name): bool
{
if ($this->fq_classlike_patterns) {
foreach ($this->fq_classlike_patterns as $pattern) {
if (preg_match($pattern, $fq_classlike_name)) {
return true;
}
}
}
if ($this->fq_classlike_names_set === null) {
$this->fq_classlike_names_set = array_fill_keys(
array_map('strtolower', $this->fq_classlike_names),
true,
);
}
return isset($this->fq_classlike_names_set[strtolower($fq_classlike_name)]);
}
```
Total cost drops to O(C+F). The lazy-init ensures the hash is built once per FileFilter instance and reused.
## Severity Note
Runs on every class visited during Psalm analysis. Impact scales with project size × filter-list size. A 10,000-class project with a 1,000-entry filter list goes from 10M scans to 11K hash lookups. Psalm runs are already CPU-bound; this closes an O(N²) that compounds the cost.
## Complexity Gate
- C=F=1000: fixed must complete in <5ms
- k-scaling 5×: time ratio must be <17.5× (O(k) ≈5×, not O(k²) ≈25×)