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:
parent
788514bcf7
commit
bb81a1a3a0
9 changed files with 384 additions and 1 deletions
73
docs/tickets/psalm-0001-filefilter-allowsclass-in-array.md
Normal file
73
docs/tickets/psalm-0001-filefilter-allowsclass-in-array.md
Normal 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×)
|
||||
Loading…
Add table
Add a link
Reference in a new issue