Skip to content

Commit 605f9b5

Browse files
committed
Diagnostics: unresolved class/interface implemented, and show templates in hover
1 parent e850bcb commit 605f9b5

15 files changed

Lines changed: 1651 additions & 11 deletions

File tree

docs/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
- **Find References.** "Find All References" locates every usage of a symbol across the project. Supports classes, interfaces, traits, enums, methods, properties, constants, functions, and variables. Variable references are scoped to the enclosing function or closure. Cross-file scanning lazily indexes user files on demand (vendor and stub files are excluded, matching PhpStorm's behaviour). The workspace walk respects `.gitignore` rules, so generated/cached directories (blade cache, Symfony `var/cache/`, `node_modules/`, etc.) are automatically skipped.
1313
- **`#[Deprecated]` attribute support.** PHPantom now reads the `#[Deprecated]` attribute used by phpstorm-stubs (~362 elements) in addition to docblock `@deprecated` tags. The `reason` and `since` fields appear in hover, completion strikethrough, and deprecation diagnostics. When both a docblock tag and an attribute are present, the docblock message takes priority. Works on classes, interfaces, traits, enums, methods, properties, constants, and standalone functions.
1414
- **Deprecation diagnostics for variable member access.** Calling a deprecated method or accessing a deprecated property through a variable (e.g. `$svc->oldMethod()`) now produces a strikethrough diagnostic. Previously only `self::`, `static::`, `$this->`, and explicit class name accesses were checked.
15+
- **Unknown class diagnostics.** Class references that PHPantom cannot resolve through any phase (use-map, local classes, same-namespace, class_index, classmap, PSR-4, stubs) are underlined with a warning. The diagnostic pairs with the "Import Class" code action so pressing the quick-fix shortcut on the warning offers to add the missing `use` statement in one step. Template parameters, type aliases (`@phpstan-type`, `@phpstan-import-type`), and attribute classes are excluded to avoid false positives.
1516
- **Document highlighting.** Placing the cursor on a symbol highlights all other occurrences in the current file. Variables are scoped to their enclosing function or closure. Assignment targets, parameters, foreach bindings, and catch variables are marked as writes; all other occurrences are reads. Class names, members, functions, and constants highlight file-wide.
1617

1718
### Changed
@@ -45,6 +46,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4546
- **`(new Canvas())->easel` property access.** Parenthesized `new` expressions on the left side of `->` now resolve correctly for variable type inference.
4647
- **Array function resolution.** `array_pop`, `array_filter`, `array_values`, `end`, and `array_map` now resolve element types correctly when the array comes from a method call chain or property access.
4748
- **Hover on variable definition sites.** Hovering a parameter, foreach binding, or catch variable no longer shows a redundant popup when the type is already visible in the signature. Assignment variables still show their resolved type.
49+
- **Inline `@var` annotations no longer leak across scopes.** A `/** @var Type $var */` annotation in one method no longer affects hover or completion for a same-named variable in a different method or class.
50+
- **Docblock tag parsing in description text.** Tags like `@throws` appearing mid-sentence in docblock descriptions (e.g. `"filtered out of @throws suggestions."`) are no longer mistakenly parsed as tags. Only `@` at a valid tag position (after the `* ` line prefix) is recognized.
4851

4952
## [0.4.0] - 2026-03-01
5053

docs/todo/diagnostics.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ within the same impact tier.
1111
---
1212

1313
## 2. Resolution-failure diagnostics
14-
**Impact: Medium · Effort: Medium**
14+
**Impact: Medium · Effort: Medium** *(partially done — unresolved class/interface implemented)*
1515

1616
Report diagnostics only for symbols and types that PHPantom's own engine
1717
failed to resolve. This is **not** a general PHP linter — we don't check
@@ -34,7 +34,7 @@ unresolved types (the code may still run fine) and **Hint** or
3434

3535
| Diagnostic | Trigger | Severity | Example |
3636
|---|---|---|---|
37-
| Unresolved class/interface | A type hint, `extends`, `implements`, `new`, or `::` reference that `find_or_load_class` cannot resolve after all phases (ast_map → classmap → PSR-4 → stubs) | Warning | `Class 'App\Foo' not found` |
37+
| Unresolved class/interface | A type hint, `extends`, `implements`, `new`, or `::` reference that `find_or_load_class` cannot resolve after all phases (ast_map → classmap → PSR-4 → stubs) | Warning | `Class 'App\Foo' not found` | *Done — `diagnostics::unknown_classes` module* |
3838
| Unresolved function | A function call that `find_or_load_function` cannot resolve (global functions, namespaced functions, stubs) | Warning | `Function 'do_thing' not found` |
3939
| Unresolved member access | `->method()` or `->property` on a type we *did* resolve, but the member doesn't exist after full resolution (inheritance + virtual providers) | Warning | `Method 'frobnicate' not found on class 'App\Bar'` |
4040
| Unresolved type in PHPDoc | A `@return`, `@param`, `@var`, `@throws`, `@mixin`, or `@extends` tag references a class that cannot be resolved | Information | `Type 'SomeAlias' in @return could not be resolved` |

example.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1767,6 +1767,14 @@ public function toArray(): array { return []; }
17671767

17681768

17691769

1770+
// ── Diagnostic: Unknown Class ───────────────────────────────────────────────
1771+
// `MutateArrayInsertSpec` and `Cluster` below are not imported and cannot be
1772+
// resolved — they get a yellow "Class 'X' not found" warning underline.
1773+
// This diagnostic fires for any ClassReference that PHPantom cannot resolve
1774+
// through use-map, local classes, same-namespace, class_index, classmap,
1775+
// PSR-4, or stubs. It pairs with the "Import Class" code action: press
1776+
// Ctrl+. (Cmd+. on Mac) on the warning to import the class in one step.
1777+
17701778
// ── Code Action: Import Class ───────────────────────────────────────────────
17711779
// Place cursor on `MutateArrayInsertSpec` and press Ctrl+. (or Cmd+. on Mac)
17721780
// to see "Import `Couchbase\MutateArrayInsertSpec`" in the quick-fix menu.

src/code_actions/import_class.rs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use tower_lsp::lsp_types::*;
1111

1212
use crate::Backend;
1313
use crate::completion::use_edit::{analyze_use_block, build_use_edit, use_import_conflicts};
14+
use crate::diagnostics::unknown_classes::UNKNOWN_CLASS_CODE;
1415

1516
use crate::symbol_map::SymbolKind;
1617
use crate::util::short_name;
@@ -120,6 +121,23 @@ impl Backend {
120121
Err(_) => continue,
121122
};
122123

124+
// Find any unknown_class diagnostics from the request context
125+
// that overlap this span so we can attach them to the code
126+
// action. This lets editors show the import action as a
127+
// quick-fix for the diagnostic.
128+
let matching_diagnostics: Vec<Diagnostic> = params
129+
.context
130+
.diagnostics
131+
.iter()
132+
.filter(|d| {
133+
matches!(
134+
&d.code,
135+
Some(NumberOrString::String(code)) if code == UNKNOWN_CLASS_CODE
136+
)
137+
})
138+
.cloned()
139+
.collect();
140+
123141
for fqn in &candidates {
124142
// Skip candidates that would conflict with an existing
125143
// import (e.g. a different class with the same short name
@@ -142,7 +160,11 @@ impl Backend {
142160
out.push(CodeActionOrCommand::CodeAction(CodeAction {
143161
title,
144162
kind: Some(CodeActionKind::QUICKFIX),
145-
diagnostics: None,
163+
diagnostics: if matching_diagnostics.is_empty() {
164+
None
165+
} else {
166+
Some(matching_diagnostics.clone())
167+
},
146168
edit: Some(WorkspaceEdit {
147169
changes: Some(changes),
148170
document_changes: None,

src/diagnostics/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,12 @@
88
//! strikethrough in most editors).
99
//! - **Unused `use` dimming** — dim `use` declarations that are not
1010
//! referenced anywhere in the file with `DiagnosticTag::Unnecessary`.
11+
//! - **Unknown class diagnostics** — report `ClassReference` spans that
12+
//! cannot be resolved through any resolution phase (use-map, local
13+
//! classes, same-namespace, class_index, classmap, PSR-4, stubs).
1114
1215
mod deprecated;
16+
pub(crate) mod unknown_classes;
1317
mod unused_imports;
1418

1519
use tower_lsp::lsp_types::*;
@@ -59,6 +63,9 @@ impl Backend {
5963
// ── Unused `use` dimming ────────────────────────────────────────
6064
self.collect_unused_import_diagnostics(uri_str, content, &mut diagnostics);
6165

66+
// ── Unknown class references ────────────────────────────────────
67+
self.collect_unknown_class_diagnostics(uri_str, content, &mut diagnostics);
68+
6269
client.publish_diagnostics(uri, diagnostics, None).await;
6370
}
6471

0 commit comments

Comments
 (0)