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
6
defects/psalm/Makefile
Normal file
6
defects/psalm/Makefile
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
.PHONY: all bench clean
|
||||
all: bench
|
||||
bench:
|
||||
python3 bench/run_all.py
|
||||
clean:
|
||||
rm -rf bench/__pycache__ __pycache__
|
||||
58
defects/psalm/bench/bench-psalm-0001.py
Normal file
58
defects/psalm/bench/bench-psalm-0001.py
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
#!/usr/bin/env python3
|
||||
# bench-psalm-0001.py
|
||||
# FileFilter.allowsClass: in_array(strtolower($cls), $fq_classlike_names, true)
|
||||
# per class visited. For C classes x F filter entries, O(C*F). Fix: lazy
|
||||
# array_fill_keys hash set for O(1) probe.
|
||||
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
def bench_defective(c_classes, f_filter):
|
||||
"""PHP in_array O(F) per class visit."""
|
||||
filter_list = [f"ns\\Cls{i:05d}" for i in range(f_filter)]
|
||||
classes = [f"ns\\Cls{(i * 31) % f_filter:05d}" for i in range(c_classes)]
|
||||
|
||||
t0 = time.perf_counter()
|
||||
hits = 0
|
||||
for cls in classes:
|
||||
lowered = cls.lower()
|
||||
# mimic in_array strict: O(F)
|
||||
if lowered in filter_list:
|
||||
hits += 1
|
||||
return time.perf_counter() - t0
|
||||
|
||||
|
||||
def bench_fixed(c_classes, f_filter):
|
||||
"""array_fill_keys hash set; O(1) lookup."""
|
||||
filter_list = [f"ns\\Cls{i:05d}" for i in range(f_filter)]
|
||||
classes = [f"ns\\Cls{(i * 31) % f_filter:05d}" for i in range(c_classes)]
|
||||
|
||||
t0 = time.perf_counter()
|
||||
filter_set = {k.lower(): True for k in filter_list}
|
||||
hits = 0
|
||||
for cls in classes:
|
||||
if cls.lower() in filter_set:
|
||||
hits += 1
|
||||
return time.perf_counter() - t0
|
||||
|
||||
|
||||
TRIALS = 3
|
||||
CASES = [(100, 100), (500, 500), (1000, 1000), (5000, 5000), (10000, 1000)]
|
||||
|
||||
|
||||
def run():
|
||||
lines = []
|
||||
header = "=== psalm-0001: FileFilter.allowsClass in_array vs array_fill_keys ==="
|
||||
print(header); lines.append(header)
|
||||
for c, f in CASES:
|
||||
df = min(bench_defective(c, f) for _ in range(TRIALS))
|
||||
fx = min(bench_fixed(c, f) for _ in range(TRIALS))
|
||||
speedup = (df / fx) if fx > 0 else float("inf")
|
||||
line = f"C={c:<5} F={f:<5}: defective={df*1000:.3f}ms fixed={fx*1000:.3f}ms speedup={speedup:.1f}x"
|
||||
print(line); lines.append(line); sys.stdout.flush()
|
||||
return lines
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
7
defects/psalm/bench/results.txt
Normal file
7
defects/psalm/bench/results.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
=== psalm-0001: FileFilter.allowsClass in_array vs array_fill_keys ===
|
||||
C=100 F=100 : defective=0.209ms fixed=0.025ms speedup=8.4x
|
||||
C=500 F=500 : defective=5.134ms fixed=0.132ms speedup=39.0x
|
||||
C=1000 F=1000 : defective=21.317ms fixed=0.277ms speedup=77.1x
|
||||
C=5000 F=5000 : defective=583.254ms fixed=1.735ms speedup=336.2x
|
||||
C=10000 F=1000 : defective=221.369ms fixed=1.745ms speedup=126.8x
|
||||
|
||||
19
defects/psalm/bench/run_all.py
Normal file
19
defects/psalm/bench/run_all.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
#!/usr/bin/env python3
|
||||
import importlib.util, os, sys
|
||||
BENCH_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
def load_module(filename):
|
||||
path = os.path.join(BENCH_DIR, filename)
|
||||
spec = importlib.util.spec_from_file_location("mod", path)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
all_lines = []
|
||||
for fname in ["bench-psalm-0001.py"]:
|
||||
mod = load_module(fname)
|
||||
lines = mod.run()
|
||||
all_lines.extend(lines); all_lines.append("")
|
||||
print(); sys.stdout.flush()
|
||||
out_path = os.path.join(BENCH_DIR, "results.txt")
|
||||
with open(out_path, "w") as f:
|
||||
f.write("\n".join(all_lines) + "\n")
|
||||
print(f"results written to {out_path}"); sys.stdout.flush()
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
# UNDF: UNDF-2026-000001296
|
||||
# UNDF: UNDF-2026-XXXXXXXXX
|
||||
# CWE-407: Algorithmic Complexity -- O(C*F) -> O(C+F) in FileFilter::allowsClass
|
||||
#
|
||||
# Defect: allowsClass runs in_array(strtolower($cls), $this->fq_classlike_names, true)
|
||||
# on every class the analyzer visits. For C classes and F filter entries,
|
||||
# per-analysis cost is O(C*F). Psalm is already CPU-bound; this compounds
|
||||
# the scan time on large monorepos with large filter lists.
|
||||
#
|
||||
# Fix: Lazy-init a lowercase hash set ($fq_classlike_names_set) and probe via
|
||||
# isset() for O(1) per class. Built once per FileFilter instance and reused.
|
||||
#
|
||||
# Complexity gate (tests/test-psalm-cwe407.py):
|
||||
# C=F=1000: fixed must complete in <5ms
|
||||
# k-scaling 5x: time ratio must be <17.5x
|
||||
--- a/src/Psalm/Config/FileFilter.php
|
||||
+++ b/src/Psalm/Config/FileFilter.php
|
||||
@@ -74,6 +74,12 @@ class FileFilter
|
||||
*/
|
||||
protected array $fq_classlike_names = [];
|
||||
|
||||
+ /**
|
||||
+ * Lazy O(1) lookup set keyed by lowercased class name; rebuilt when
|
||||
+ * fq_classlike_names is set / mutated.
|
||||
+ */
|
||||
+ private ?array $fq_classlike_names_set = null;
|
||||
+
|
||||
/**
|
||||
* @var array<string>
|
||||
*/
|
||||
@@ -570,6 +576,8 @@ class FileFilter
|
||||
return true;
|
||||
}
|
||||
|
||||
public function allowsClass(string $fq_classlike_name): bool
|
||||
{
|
||||
if ($this->fq_classlike_patterns) {
|
||||
@@ -580,7 +588,14 @@ class FileFilter
|
||||
}
|
||||
}
|
||||
|
||||
- return in_array(strtolower($fq_classlike_name), $this->fq_classlike_names, 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)]);
|
||||
}
|
||||
|
||||
public function allowsMethod(string $method_id): bool
|
||||
Loading…
Add table
Add a link
Reference in a new issue