phpantom_lsp 0.7.0

Fast PHP language server with deep type intelligence. Generics, Laravel, PHPStan annotations. Ready in an instant.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
# PHPantom — Diagnostics

Items are ordered by **impact** (descending), then **effort** (ascending)
within the same impact tier.

| Label      | Scale                                                                                                                  |
| ---------- | ---------------------------------------------------------------------------------------------------------------------- |
| **Impact** | **Critical**, **High**, **Medium-High**, **Medium**, **Low-Medium**, **Low**                                           |
| **Effort** | **Low** (≤ 1 day), **Medium** (2-5 days), **Medium-High** (1-2 weeks), **High** (2-4 weeks), **Very High** (> 1 month) |

---

## Severity philosophy

PHPantom assigns diagnostic severity based on runtime consequences:

| Severity        | Criteria                                                                                                                                                                                                                                                                                                                                                                                     | Examples                                                                                                                                                                                                                                                                      |
| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Error**       | Would crash at runtime. The code is definitively wrong.                                                                                                                                                                                                                                                                                                                                      | Member access on a scalar type (`$int->foo()`). Calling a function that doesn't exist (`doesntExist()`).                                                                                                                                                                      |
| **Warning**     | Likely wrong but could work for reasons we can't verify statically. The types are poor but the code might be correct at runtime.                                                                                                                                                                                                                                                             | Accessing a member that doesn't exist on a non-final class (`$user->grantAccess()` where `User` has no such method but a subclass might). Unknown class in a type position (`Class 'Foo' not found`). Subject type resolved to an unknown class so members can't be verified. |
| **Hint**        | The codebase lacks type information. Off by default or very subtle. Poorly typed PHP is so common that showing these by default would be noise for most users. Anyone who does care about type safety is likely running PHPStan already. Unless our engine becomes very strong, these diagnostics either expose our own inference gaps or bother users who never opted into static analysis. | `mixed` subject member access (opt-in via `unresolved-member-access`). Deprecated symbol usage (rendered as strikethrough).                                                                                                                                                   |
| **Information** | Advisory. Something the developer might want to know.                                                                                                                                                                                                                                                                                                                                        | Unused `use` import (rendered as dimmed). Unresolved type in a PHPDoc tag.                                                                                                                                                                                                    |

---

## D4. Unused variable diagnostic

**Impact: Medium · Effort: Medium**

Flag variables that are assigned but never read. This is one of the
most common issues in PHP codebases and catches dead code, typos in
variable names, and forgotten refactoring leftovers.

PHPantom already has an undefined-variable diagnostic
(`undefined_variable` in `diagnostics/undefined_variables.rs`) that
tracks variable definitions and reads through scope analysis. The
unused-variable diagnostic is the dual: a variable that has a
definition site but zero read sites within the same scope.

**Severity:** Information (rendered as dimmed text). Assigned-but-
unread variables are not bugs per se (the code still runs), but they
are strong signals of dead code or typos. Information severity avoids
alarming users while still making the issue visible.

**Diagnostic code:** `unused_variable` (matches the planned CLI fix
rule FX2).

### Scope

1. **Local variables in function/method bodies.** A variable assigned
   inside a function or method body that is never read before the
   scope ends. Parameters count as assignments; an unused parameter
   in a non-abstract, non-interface method is flagged.
2. **Foreach bindings.** `foreach ($items as $key => $value)` where
   `$key` or `$value` is never read inside the loop body. Convention:
   variables named `$_` or starting with `$_` are exempt (intentional
   discard).
3. **Catch variables.** `catch (Exception $e)` where `$e` is never
   read. Same `$_` exemption applies.

### Exclusions

- Variables in the global scope (scripts, templates).
- Variables passed by reference (`&$var`) to functions, since the
  callee may use them as out-parameters.
- Variables used inside closures or arrow functions that capture them
  (explicit `use ($var)` or implicit capture).
- Compact() calls that reference the variable by string name.
- Variables used in string interpolation (`"Hello $name"`).
- Variables whose RHS has side effects (method calls, function calls)
  should still be flagged, but with a detail note that removing the
  assignment would also remove the side effect.

### PHPStan parallel

PHPStan does not have a built-in unused-variable rule, but third-party
rulesets (e.g. `phpstan-strict-rules`, `tomasvotruba/unused-public`)
report similar issues. When D4 ships, the PHPStan quick-fix
infrastructure should recognise our native `unused_variable` code so
that:

- The "Remove unused import" pattern can be extended to offer a
  "Remove unused variable" quick-fix (same code action kind).
- FX2 (`unused_variable` CLI fix rule) can consume our diagnostic
  directly without needing PHPStan.

### Implementation

1. Extend the scope collector (`scope_collector/mod.rs`) to track
   read sites per variable per frame (it already tracks definition
   sites for the undefined-variable diagnostic).
2. After processing a scope, iterate defined variables and flag any
   that have zero reads and are not in the exclusion list.
3. Emit diagnostics with `DiagnosticSeverity::HINT` and
   `DiagnosticTag::UNNECESSARY` so editors render unused variables
   as dimmed/faded text.
4. Add a code action (in `code_actions/`) to remove the assignment
   statement when the RHS is side-effect-free, or to prefix the
   variable with `$_` to suppress the diagnostic.

---

## D3. Deprecated rendering — chain subject resolution

**Impact: Low-Medium · Effort: Medium**

Chain subjects like `getHelper()->deprecatedMethod()` do not produce
a deprecated diagnostic because `resolve_subject_to_class_name` in
`diagnostics/deprecated.rs` returns `None` for non-variable,
non-keyword subjects (the `_ => None` arm). The function call return
type is never resolved, so the member deprecation check is skipped.

**Fix:** Route chain subjects through the completion/type-inference
pipeline to resolve the return type of the call before checking the
member for deprecation. The variable-resolution path already works
for `$var->deprecatedMethod()` via `resolve_variable_subject`; the
gap is function-call and method-call return types in subject position.

The following have been verified and are covered by tests:

- Deprecated class references in `new`, type hints, `extends`, and
  `implements` positions all render with strikethrough.
- Deprecated method calls, property accesses, and constants render
  with strikethrough (via both `$var->` and `ClassName::` subjects).
- Offset-based class resolution for `$this`/`self`/`static` resolves
  to the correct class in files with multiple class declarations.

---

## D5. Diagnostic suppression intelligence

**Impact: Medium · Effort: Medium**

When PHPantom proxies diagnostics from external tools, users need a way
to suppress specific warnings. Rather than forcing them to install a
separate extension or memorise each tool's suppression syntax, PHPantom
can offer **code actions to insert the correct suppression comment** for
the tool that produced the diagnostic.

PHPStan suppression is implemented: "Ignore PHPStan error" adds
`// @phpstan-ignore <identifier>` (appending to existing ignores when
present), and "Remove unnecessary @phpstan-ignore" cleans up unmatched
ignores reported by PHPStan. What remains:

### Remaining tools

- PHPCS: `// phpcs:ignore [Sniff.Name]` or `// phpcs:disable` /
  `// phpcs:enable` blocks.
- PHPMD (3.0): `#[SuppressWarnings(RuleName::class)]` as a PHP attribute.
- For PHPantom's own diagnostics: support `@suppress phpantom.*`
  in docblocks (matching PHP Tools' convention) and a config flag
  `phpantom.diagnostics.enabled: bool` (default `true`).

**Prerequisites:** Each tool needs a diagnostic proxy before its
suppression actions can be wired up.

---

## D6. Unreachable code diagnostic

**Impact: Low-Medium · Effort: Low**

Dim code that appears after unconditional control flow exits:
`return`, `throw`, `exit`, `die`, `continue`, `break`. This is a
Phase 1 (fast) diagnostic since it requires only AST structure, not
type resolution.

### Behaviour

| Scenario                                           | Rendering                           |
| -------------------------------------------------- | ----------------------------------- |
| Code after `return $x;` in same block              | Dimmed (DiagnosticTag::UNNECESSARY) |
| Code after `throw new \Exception()`                | Dimmed                              |
| Code after `exit(1)` or `die()`                    | Dimmed                              |
| Code after `continue` or `break` in a loop         | Dimmed                              |
| Code after `if (...) { return; } else { return; }` | Dimmed (both branches exit)         |

Severity: **Hint** with `DiagnosticTag::UNNECESSARY` so editors dim
the text rather than underlining it. This matches how unused imports
are rendered.

### Implementation

Walk the AST statement list. After encountering a statement that
unconditionally exits the current scope (return, throw, expression
statement containing `exit`/`die`), mark all subsequent statements in
the same block as unreachable. The span covers from the start of the
first unreachable statement to the end of the last statement in the
block.

Phase 1 only handles the simple single-block case. Whole-branch
analysis (both if/else branches exit) is a future refinement.

### Debugging value

When our type engine silently resolves a method to a `never` return
type (e.g. an incorrectly resolved overload), unreachable code after
the call becomes visible, signalling the bug.

---

## D10. PHPMD diagnostic proxy

**Impact: Low · Effort: Medium**

Proxy PHPMD (PHP Mess Detector) diagnostics into the editor, following
the same pattern as the existing PHPStan proxy. PHPMD 3.0 (once
released) is the target version. It will get a `[phpmd]` TOML section
with `command`, `timeout`, and tool-specific options mirroring the
`[phpstan]` schema.

### Prerequisites

- PHPMD 3.0 must be released. Current 2.x output formats and rule
  naming may change.
- The diagnostic suppression code action (D5) should support PHPMD's
  `@SuppressWarnings(PHPMD.[RuleName])` syntax once the proxy exists.

### Implementation

1. Add a `[phpmd]` section to the config schema in `src/config.rs`
   with `command` (default `"vendor/bin/phpmd"`), `timeout`, and
   an `enabled` flag.
2. Run PHPMD with XML or JSON output on the current file (or changed
   files) and parse the results into LSP diagnostics.
3. Map PHPMD rule names to diagnostic codes so that suppression
   actions (D5) can insert the correct `@SuppressWarnings` annotation.
4. Respect the same debounce and queueing logic used by the PHPStan
   proxy to avoid overwhelming the tool on rapid edits.

---

## D11. Invalid class-like kind in context

**Impact: Medium · Effort: Low**

PHP accepts certain class-like names syntactically in positions where
they are guaranteed to fail at runtime or be silently useless. These
are not parse errors, so `php -l` does not catch them. PHPStan catches
some of these (e.g. `new` on an abstract class) but not all. A
dedicated diagnostic rule can flag them all consistently using the
same context knowledge that the completion system already applies
(the `ClassNameContext` enum and `TypeHint` variant).

The rule table:

| Position                   | What to flag                                  | Severity | Runtime behavior                          |
| -------------------------- | --------------------------------------------- | -------- | ----------------------------------------- |
| `new X`                    | Abstract class, interface, trait, enum        | Error    | Fatal error: Cannot instantiate           |
| `throw new X`              | Non-Throwable class                           | Error    | Fatal error: Cannot throw                 |
| `throw new X`              | Abstract class, interface, trait, enum        | Error    | Fatal error: Cannot instantiate           |
| `$x instanceof X`          | Trait                                         | Warning  | Always evaluates to `false`               |
| `catch (X $e)`             | Trait                                         | Warning  | Never catches anything                    |
| `catch (X $e)`             | Non-Throwable class or interface              | Error    | Never catches, uncaught exception crashes |
| `class A extends X`        | Final class                                   | Error    | Fatal error: Cannot extend final class    |
| `class A implements X`     | Class, trait, enum                            | Error    | Fatal error: Not an interface             |
| `interface A extends X`    | Class, trait, enum                            | Error    | Fatal error: Not an interface             |
| `class A { use X; }`       | Class, interface, enum                        | Error    | Fatal error: Not a trait                  |
| `function f(X $p)`, `): X` | Trait                                         | Warning  | Type check always fails                   |
| `public X $prop`           | Trait                                         | Warning  | Type check always fails                   |
| `@param X`, `@return X`    | Trait                                         | Hint     | Documents unsatisfiable constraint        |
| `@throws X`                | Non-Throwable class or interface, trait, enum | Hint     | Documents impossible throw                |

**Why Warning for traits in type positions (not Error).** PHP does not
reject the code at parse time or class loading time. The fatal
`TypeError` only occurs at the specific call site when a value actually
reaches the type check. Code paths that are never executed with a
mismatched value will run without error. This is different from `class
extends final` which crashes unconditionally when the class is loaded.

**Why Hint for PHPDoc.** PHPDoc has no runtime enforcement at all. A
trait in `@param` is useless documentation but does not crash anything.
This aligns with the severity philosophy: hints are for code quality
issues that static analysis enthusiasts care about.

**Implementation:**

1. During AST extraction (or as a post-parse diagnostic pass), walk
   class declarations and check `extends`, `implements`, and `use`
   references against loaded `ClassInfo` entries. If the referenced
   class is loaded and its kind does not match the position, emit
   a diagnostic.

2. For `new X`, `throw new X`, `instanceof X`, and `catch (X)`, scan
   expression nodes in method bodies. Resolve `X` to a `ClassInfo`
   (if loaded) and check kind/modifier compatibility.

3. For native type hints, scan parameter types, return types, and
   property types. Resolve each class-like reference and check for
   trait kind.

4. For PHPDoc types, scan `@param`, `@return`, `@var`, and `@throws`
   tags. Resolve each class-like reference. Flag traits in type
   positions and non-Throwable types in `@throws`.

5. Only flag references where the target class is loaded (in
   `ast_map` or stubs). Unknown classes should not be flagged here
   (that is D4's job). This avoids false positives from unloaded
   classmap entries where the kind is unknown.

**Relationship to completion filtering.** The completion context
detector (`ClassNameContext` enum in `class_completion.rs`) and this
diagnostic rule use the same underlying knowledge (which kinds are
valid in which positions). The completion system already prevents the
user from inserting a wrong kind; this diagnostic catches wrong kinds
that are already in the code. Both should share the same rule table
to stay in sync.

---

## D12. Mago linter integration (optional diagnostics)

**Impact: Medium · Effort: Medium**

PHPantom already depends on several mago crates (`mago-syntax`,
`mago-docblock`, `mago-names`, `mago-formatter`, `mago-span`). The
`mago-linter` crate provides ~159 lint rules covering redundancy,
best practices, clarity, consistency, correctness, and deprecation.
Integrating it as an optional diagnostics provider would give users
"PHPStan-lite" diagnostics without requiring PHPStan to be installed.

**Integration approach:** call `Linter::lint()` on the parsed AST
(already available), convert `IssueCollection` to LSP `Diagnostic`s,
convert `TextEdit` fixes to LSP `CodeAction`s. The linter is AST-only
(no type inference), so it is fast.

Offer as opt-in via `.phpantom.toml` configuration. Default to
disabled so it does not conflict with users who already run PHPStan
or Psalm. Mark with `source: "mago"` to distinguish from PHPantom's
own diagnostics.

**Notable rules:** `no-redundant-method-override`,
`str-contains`/`str-starts-with` modernization,
`prefer-arrow-function`, `constant-condition`, `no-self-assignment`,
`explicit-nullable-param`, `valid-docblock`.

## D13. Unify diagnostic subject resolution with completion/hover

`unknown_members.rs` has two secondary resolvers that run their own
independent type resolution when `resolve_target_classes_expr` returns
empty:

- `resolve_scalar_subject_type` (~130 lines) re-resolves variables,
  property chains, and call expressions to detect scalar types.
- `resolve_unresolvable_class_subject` (~80 lines) re-resolves
  variables and call expressions to detect class names that can't be
  loaded.

Both duplicate logic from `resolver.rs` and
`variable/resolution.rs` but can diverge, producing diagnostics for
types that completion and hover cannot see (or vice versa).

### Goal

The diagnostic path should use the same resolution result that
completion and hover use. All three consumers should see identical
outcomes for the same subject text at the same cursor position.

### Approach

Extend the shared resolver's return type (or add a secondary result)
to carry scalar type information and unresolvable class names
alongside the resolved `ClassInfo` list. The diagnostic collector
would then inspect this enriched result instead of running its own
resolution. This eliminates the secondary resolvers entirely.

### Files

- `src/diagnostics/unknown_members.rs` — remove
  `resolve_scalar_subject_type` and `resolve_unresolvable_class_subject`
- `src/completion/resolver.rs` — enrich the resolution result

---

## D14. PHPCS diagnostic proxy

**Impact: Low · Effort: Medium**

Proxy PHP_CodeSniffer (PHPCS) diagnostics into the editor, following
the same pattern as the existing PHPStan proxy. PHPCS reports coding
standard violations (PSR-12, PSR-1, custom sniffs) and is widely used
in PHP projects for enforcing style and detecting common mistakes.

### Implementation

1. Add a `[phpcs]` section to the config schema in `src/config.rs`
   with `command` (default `"vendor/bin/phpcs"`), `timeout`,
   `standard` (default: project's `phpcs.xml` or `phpcs.xml.dist`,
   falling back to `PSR12`), and an `enabled` flag.
2. Run PHPCS with `--report=json` on the current file and parse the
   JSON output into LSP diagnostics. Each violation maps to a
   diagnostic with the sniff name as the code (e.g.
   `PSR12.Files.FileHeader.MissingPHPVersion`).
3. Map PHPCS severity levels (`error` / `warning`) to LSP
   `DiagnosticSeverity::Error` and `DiagnosticSeverity::Warning`.
4. Mark fixable violations (PHPCS reports `fixable: true` per
   violation) so that a companion code action can run `phpcbf` to
   auto-fix them.
5. Respect the same debounce and queueing logic used by the PHPStan
   proxy to avoid overwhelming the tool on rapid edits.
6. Support `phpcs:ignore` and `phpcs:disable` / `phpcs:enable`
   suppression comments when diagnostic suppression intelligence
   (D5) is implemented.

---

## D15. Type error diagnostics

**Impact: Medium-High · Effort: High**

Report type mismatches that would cause runtime errors or indicate
logical bugs: passing a value of the wrong type to a function,
returning the wrong type from a function, or assigning an incompatible
type to a typed property.

This is the bread-and-butter of static analysis tools like PHPStan
(levels 5-9) and Psalm. PHPantom already resolves types for
completion, hover, and variable inference. Surfacing type errors
reuses that infrastructure to catch bugs without requiring an external
tool.

### Severity

- **Error** for mismatches that would cause a `TypeError` at runtime
  (e.g. passing `string` to a parameter typed `int` with strict
  types enabled, or returning `null` from a non-nullable return type).
- **Warning** for mismatches that PHP would coerce silently in weak
  mode but that are almost certainly bugs (e.g. passing an `array`
  where `string` is expected).

### Scope (phased rollout)

**Phase 1 — Argument type mismatches.** When calling a function or
method whose parameter types are known (native hints or `@param`
docblock), check each argument's resolved type against the declared
parameter type. Flag clear mismatches (e.g. `string` passed to `int`,
`null` passed to non-nullable). Skip `mixed`, unresolved types, and
union types where any branch matches (to avoid false positives).

**Phase 2 — Return type mismatches.** When a function or method has a
declared return type (native or `@return`), check the types of values
in `return` statements against the declared type. Flag clear
mismatches. This catches the common "forgot to return early" pattern
where `null` falls through a non-nullable return type.

**Phase 3 — Property type mismatches.** When assigning to a typed
property (`public int $count`), check the RHS type against the
declared property type. This catches assignment bugs that PHP would
reject at runtime with a `TypeError`.

### Design constraints

- **Conservative.** Only flag mismatches where we are confident the
  types are incompatible. When in doubt (unresolved types, `mixed`,
  complex generics), do not flag. False positives are worse than
  missed true positives for a feature that competes with PHPStan.
- **Respect `strict_types`.** In files with `declare(strict_types=1)`,
  coercions like `int` to `string` are errors. In files without it,
  PHP's type juggling rules apply and only truly incompatible types
  (e.g. `array` to `string`) should be flagged.
- **Diagnostic code:** `type_error` with subcodes like
  `type_error.argument`, `type_error.return`, `type_error.property`
  for filtering and suppression.

### Dependencies

- Accurate type resolution for function/method parameters (already
  available via completion and hover infrastructure).
- `declare(strict_types=1)` detection (need to check file-level
  declare statements).
- Subtype checking utility (is type A assignable to type B?) — a
  shared helper that would also benefit other diagnostics. The
  existing `is_subtype_of` in the type hierarchy module covers class
  hierarchies; scalar and union type compatibility needs to be added.

### Quick-fix integration

When a type mismatch is detected, offer code actions:

- **Add type cast** (e.g. `(int)$value` when passing `string` to
  `int` parameter).
- **Add null check** when passing a nullable type to a non-nullable
  parameter.
- **Update docblock** when the declared `@param` or `@return` type is
  too narrow for the actual usage.