Skip to content

Latest commit

 

History

History
283 lines (205 loc) · 15.1 KB

File metadata and controls

283 lines (205 loc) · 15.1 KB

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.

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.

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. Tighten argument type mismatch diagnostic (Phase 2)

Impact: High · Effort: Medium

is_type_compatible in src/diagnostics/type_errors.rs silences several cases that are genuine bugs at runtime. Phase 1 was intentionally permissive to avoid false positives while the engine matured; Phase 2 tightens the remaining gaps. PHPStan and Psalm already flag most of these.

1. Nullable arg → non-nullable param (lines 264–271)

Currently silenced with a MAYBE comment ("developer may have guarded against null"). This is the #1 source of runtime TypeError in PHP 8+. Both PHPStan and Psalm flag it. Should be reported at least as Warning severity, since the null path may be unguarded.

2. void as argument (lines 94–96)

Currently silenced conservatively. Passing the return value of a void function is always a bug — PHP 8 returns null but the call site clearly misunderstands the API. Should be Error severity.

3. intstring type juggling (lines 313–322)

Currently unconditionally accepted because we can't know the strict_types setting. Under declare(strict_types=1) this is a TypeError. Consider detecting the declare statement in the file and flagging when strict types are enabled. When the declare is absent or set to 0, keep the current permissive behaviour.

4. Union any-member-compatible threshold (lines 189–213)

Currently: if ANY single member of an arg union is compatible with the param, the entire union passes. Combined with the other permissive rules above, this creates cascading permissiveness (e.g. null|BadType passes a string param because null is not checked, then BadType is the "any" member that gets skipped). Consider requiring all non-null members to be compatible, or at least flagging when a majority of members are incompatible.

5. Reverse hierarchy acceptance (Direction 2)

Currently: when the arg type is a supertype of the param type (e.g. CarbonInterface passed to Carbon), the diagnostic is silenced for all non-final classes because "the value might be the narrower type at runtime." This means the diagnostic can only catch type errors between completely unrelated classes, which severely limits its value. Passing Animal where Dog is expected is silently accepted.

This is the single largest gap in the diagnostic. Tightening it requires control-flow analysis (instanceof guards, assert calls) to know whether the broader type was actually narrowed before the call site. Without CFA, the false positive rate would be high. Consider reporting at Warning severity with a message like "argument type Animal is broader than expected Dog; verify the value was narrowed before this call."


D15. Unused parameter diagnostic

Impact: Low · Effort: Low

Flag function and method parameters that are never read inside the body. This was intentionally excluded from D4 (unused variable diagnostic) because without suppression support (D5), false positives are unavoidable: callbacks, interface implementations, and framework conventions (e.g. Laravel event listeners) often require specific parameter signatures even when not all parameters are used.

Prerequisite: D5 (diagnostic suppression intelligence) must ship first so users can silence false positives with // @phpantom-ignore or a configuration-level exclusion.

Scope

  1. Function and method parameters (including closures and arrow functions) that are never read inside their body.
  2. Constructor parameters that are not promoted and never read.

Exclusions

  • Parameters named $_ or starting with $_ (intentional discard).
  • Promoted constructor parameters (they are property assignments).
  • Parameters in abstract methods and interface method signatures (no body to check).