Skip to content

Commit d0d9658

Browse files
committed
chore(mago): reduce analyze debt and wire baseline-aware composer scripts
1 parent 44888b8 commit d0d9658

11 files changed

Lines changed: 225 additions & 480 deletions

File tree

STATIC_ANALYSIS_DEBT_REDUCTION.md

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
# Static Analysis Debt Reduction Plan (Mago `analyze`)
2+
3+
Plan to gradually pay down the static-analysis tech debt currently suppressed by
4+
`mago-analyze-baseline.toml`. CI stays green at every step; we only ever shrink
5+
the baseline, never grow it.
6+
7+
_Generated 2026-06-29 against mago 1.42.0._
8+
9+
## Background
10+
11+
`composer analyze:all` (raw `mago analyze`, no baseline) reports **7,198 issues**
12+
(4,792 errors, 2,235 warnings, 171 help). CI is nonetheless green because
13+
`.github/workflows/build.yml` runs analyze **with** the baseline:
14+
15+
```
16+
./phpmyfaq/src/libs/bin/mago analyze --baseline=mago-analyze-baseline.toml --reporting-format=github
17+
```
18+
19+
The baseline (`mago-analyze-baseline.toml`, 3,377 entries — each with a `count`
20+
field, summing to the 7,198 occurrences) records every existing issue as accepted
21+
debt. CI only fails on **new** issues introduced beyond the baseline.
22+
23+
"Reduce the errors and warnings" therefore means: **fix real issues and remove the
24+
corresponding entries from the baseline**, so the ledger shrinks toward empty.
25+
26+
### Composer scripts (Phase 0 — done)
27+
28+
- `composer analyze` — baseline-aware, mirrors CI (green when no new issues).
29+
- `composer analyze:all` — raw run, shows the full 7,198-issue debt.
30+
- `composer analyze:baseline` — regenerate the baseline after fixing code:
31+
removes outdated entries (fixed issues) without hiding new ones, keeps a `.bkp`.
32+
33+
The lint side has the analogous `mago-lint-baseline.toml`; the same approach
34+
applies to `composer lint` if/when we choose to mirror it.
35+
36+
## The fix → shrink workflow
37+
38+
For every batch of fixes:
39+
40+
1. Fix the code in a small, reviewable PR (one file or a small group).
41+
2. `composer analyze:baseline` — drops the now-fixed entries from the baseline.
42+
3. Commit the shrunken `mago-analyze-baseline.toml` alongside the code change.
43+
4. CI (`--baseline`) stays green; the diff on the baseline file shows progress.
44+
45+
Guardrails to consider adding to CI:
46+
47+
- `--verify-baseline` / `--fail-on-out-of-sync-baseline` so the ledger cannot
48+
silently drift out of sync with the code.
49+
50+
## What the 7,198 issues are (by root cause)
51+
52+
Overwhelmingly **type-safety** findings. Counts are occurrences from `analyze:all`.
53+
54+
| Cluster | ~Count | Root cause | Fix pattern |
55+
|---|---:|---|---|
56+
| `mixed-*` — argument (1,245), assignment (1,440), property-access (527), operand (387), array-access (272), method-access (253), return (195) | **~4,700** | Untyped data from request input (`Filter::filterVar`, `json_decode` objects), DB rows, and untyped properties | Type the boundaries: typed request accessors/DTOs, typed DB-row shapes, annotate properties & returns |
57+
| Null-safety — `possible-method-access-on-null` (348), `possibly-null-argument` (253), `possibly-null-operand` (155), `nullable-return-statement` (56), `possibly-null-property-access` (20) | **~900** | Missing null guards; nullable returns not declared | Guard clauses, `?->`, accurate `?T` return types |
58+
| `non-existent-method` (100), `-property` (80), `-constant` (76), `-function` (16) | **~270** | **Likely real bugs** or missing type info on dynamic objects | Inspect each: fix genuine defects; annotate magic `__get`/`__call` with `@property`/`@method` |
59+
| `less-specific-*` — return, nested-argument, argument, nested-return | **~225** | Generics/array shapes wider than declared | Tighten `@param`/`@return` shapes & generics |
60+
| Redundant / dead / unused (help) — `redundant-comparison/-logical-operation/-null-coalesce/-cast`, `unused-property` | **~170** | Cleanups | Mostly auto-fixable |
61+
62+
### Hot spots by directory
63+
64+
| Directory | ~Issues |
65+
|---|---:|
66+
| `Controller/Administration/Api` | 1,120 |
67+
| `Controller/Administration` | 377 |
68+
| `Controller/Frontend` | 318 |
69+
| `Database` | 298 |
70+
| `Faq` | 255 |
71+
| `Controller/Frontend/Api` | 252 |
72+
| `Setup` | 229 |
73+
| `Administration`, `User`, `Auth`, `Helper` | ~200 each |
74+
75+
The controllers dominate because request input is `mixed` at its source — typing a
76+
handful of input boundaries cascades into removing dozens of downstream `mixed-*`
77+
errors each.
78+
79+
## Phased plan
80+
81+
### Phase 0 — Make debt visible & safe to chip at — DONE
82+
- Baseline-aware `composer analyze` + `analyze:all` + `analyze:baseline` scripts.
83+
- This document records the fix → shrink workflow.
84+
- TODO (optional): add `--verify-baseline` to CI.
85+
86+
### Phase 1 — Free wins (in progress)
87+
88+
> **Important finding: do NOT trust `mago analyze --fix` blindly here.**
89+
> On mago 1.42.0 the "8 safe fixes" included changes that **break the code**:
90+
> `Translation.php` got `$x = &self::$translation?->loadedLanguages;` — taking a
91+
> reference to a nullsafe chain is a **fatal parse error** ("Cannot take reference
92+
> of a nullsafe chain", confirmed via `php -l`). Others removed `(string)` casts
93+
> feeding directly into SQL `escape()` on `mixed` data (reducing safety) and added
94+
> pointless `?->` to inner index expressions whose outer access stayed `->`.
95+
> All 8 were reverted. **Every auto-fix must be reviewed individually with `php -l`
96+
> + the full test suite; the "safe" classification is not reliable.**
97+
98+
Hand-reviewed cleanups instead. **Done (commit-ready, all 6586 tests green, lint clean):**
99+
100+
| Change | File(s) | Baseline entries removed |
101+
|---|---|---|
102+
| De-promote unused `$tableName` property (param still used) | `Configuration/ConfigurationRepository.php` | 1 |
103+
| Remove dead injected `Ldap` dep + redundant constructor | `Controller/Administration/Api/LdapController.php` | 1 |
104+
| Remove dead injected `Comments` dep + unused import | `Controller/Administration/NewsController.php` | 1 |
105+
| Remove dead injected `Configuration` dep + import; update call site + test | `Service/McpServer/McpSdkRuntime.php`, `PhpMyFaqMcpServer.php`, `McpSdkRuntimeTest.php` | 1 |
106+
107+
Baseline: 3377 → 3373 entries. Verified each property was genuinely unused
108+
(no `$this->` access, no trait/reflection usage, call sites/tests updated).
109+
110+
**Deferred (need deeper review before touching):**
111+
- `RouteCollectionBuilder::$configuration` — unused, but removal ripples into
112+
`Kernel::loadRoutes()` (the `$configuration` fetch + closure capture become
113+
dead) and `RouteCollectionBuilderTest`; routing is critical-path.
114+
- `Configuration.php` `$config` / `$logger` / `$pluginManager` — central class;
115+
confirm no dynamic/trait/reflection access before removing.
116+
117+
**Remaining Phase 1 candidates:** the `redundant-*` help cluster — but treat each
118+
with suspicion: a "redundant condition" often reflects mago's mistaken type
119+
inference, and removing it can drop a real defensive check. Review case-by-case.
120+
121+
### Phase 2 — `non-existent-*` triage (in progress)
122+
123+
> **Finding: the ~270 `non-existent-*` are NOT real bugs.** Triage showed they are
124+
> all **analyzer-modeling gaps** — missing definition files, an unstubbed
125+
> extension, and traits analyzed in isolation. The code is correct at runtime.
126+
> The "likely real bugs" assumption in the original plan was wrong.
127+
128+
Root causes and remediation:
129+
130+
| Sub-category | Count | Root cause | Action |
131+
|---|---:|---|---|
132+
| `non-existent-function` | 16 | All `sqlsrv_*` — mago ships no stubs for the SQL Server extension | **Left baselined** (ignored for now, by request — a `sqlsrv` stub was prototyped then reverted; revisit if/when the SQL Server driver gets attention) |
133+
| `non-existent-constant` | 76 → 1 | `PMF_*`/`AAD_OAUTH_*` defined in files outside mago's analysis path | **Done:** added the 3 tracked constant files + `phpmyfaq/src/stubs/azure.php` to `includes`. 1 left (`PMF_MULTI_INSTANCE_CONFIG_DIR`, a runtime multisite `define()` behind a `defined()` guard — left baselined) |
134+
| `non-existent-method` | 100 | Traits analyzed standalone (member lives on host class), and containers typed as a base class (`Auth`) when methods live on the driver interface (`AuthDriverInterface`) | **Deferred** — needs trait annotations / tighter type hints (see below) |
135+
| `non-existent-property` | 80 | Same trait-standalone gap (e.g. `ConfigurationMethodsTrait::$config` is `Configuration::$config`) | **Deferred** — trait annotations |
136+
137+
**Phase 2 results so far:** 7194 → 7086 issues (**108 false positives cleared**),
138+
baseline 3373 → 3301 entries. **Zero logic changes, zero runtime risk** — only
139+
`mago.toml` config and one analyzer-only stub file (`src/stubs/azure.php`, never
140+
autoloaded; PSR-4 maps `phpMyFAQ\` to `phpmyfaq/src/phpMyFAQ` only, so
141+
`src/stubs/*` is invisible at runtime). The `sqlsrv_*` findings (18) are left
142+
baselined by request. CI green throughout.
143+
144+
> **This validated the Phase 1 decision to keep `Configuration::$config`.** It was
145+
> flagged `unused-property` only because `ConfigurationMethodsTrait` (which reads
146+
> `$this->config`) is analyzed in isolation — the property is very much used.
147+
148+
**Deferred: trait & interface typing (180 findings).** This is the real remaining
149+
work and needs care, not a stub:
150+
- **Traits** (`ConfigurationMethodsTrait`, `CurrentUserSessionLookupTrait`,
151+
`CurrentUserAccountStateTrait`, …): tell the analyzer what the host provides —
152+
e.g. a `@phpstan-require-extends`/`@phpstan-require-implements` bound or
153+
`@property`/`@method` docblocks on the trait. Verify mago honors the chosen
154+
mechanism on a single trait before rolling out.
155+
- **Base-vs-interface typing**: e.g. `CurrentUser::$authContainer` is typed
156+
`Auth` but the code calls `isValidLogin()`/`checkCredentials()` which live on
157+
`AuthDriverInterface`. Tighten the property/return type to the interface (or a
158+
union). A genuine type-precision improvement, but touches auth-critical code —
159+
do it deliberately with the full suite as the gate.
160+
161+
### Phase 3 — Type the request boundary (the big lever, multi-week, file-by-file)
162+
- Order: `Controller/Administration/Api` → other `Controller/*``Database``Faq`.
163+
- Introduce typed accessors for request input and typed DB-row shapes so `mixed`
164+
collapses at the source rather than fixing 4,700 symptoms individually.
165+
- One PR per file / small group; regenerate baseline per PR for measurable progress.
166+
- This phase accounts for the bulk (~4,700) of the ledger.
167+
168+
### Phase 4 — Null-safety & specificity (ongoing)
169+
- Add guard clauses / nullsafe operators and accurate nullable return types (~900).
170+
- Tighten `less-specific-*` array shapes and generics (~225).
171+
172+
### Phase 5 — Ratchet
173+
- As each category reaches zero, optionally promote it to a hard gate so it cannot
174+
regress, and continue shrinking toward a baseline-free state.
175+
176+
## Sequencing rationale
177+
178+
Phases 1–2 are low-risk, high-signal, and surface real bugs quickly. Phase 3 is
179+
where most of the volume actually lives, because it is a small number of `mixed`
180+
*sources*, not 4,700 independent fixes — typing each boundary pays down many
181+
downstream findings at once.

composer.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,9 @@
123123
},
124124
"minimum-stability": "stable",
125125
"scripts": {
126-
"analyze": "./phpmyfaq/src/libs/bin/mago analyze",
126+
"analyze": "./phpmyfaq/src/libs/bin/mago analyze --baseline=mago-analyze-baseline.toml",
127+
"analyze:all": "./phpmyfaq/src/libs/bin/mago analyze",
128+
"analyze:baseline": "./phpmyfaq/src/libs/bin/mago analyze --baseline=mago-analyze-baseline.toml --remove-outdated-baseline-entries --backup-baseline",
127129
"format": "./phpmyfaq/src/libs/bin/mago format",
128130
"format:dry-run": "./phpmyfaq/src/libs/bin/mago format --dry-run",
129131
"format:fix": "./phpmyfaq/src/libs/bin/mago format --fix",

0 commit comments

Comments
 (0)