Skip to content

Commit 83fdcb7

Browse files
committed
Ignore invalid PHPDoc types
1 parent 87f9b33 commit 83fdcb7

6 files changed

Lines changed: 174 additions & 107 deletions

File tree

docs/CHANGELOG.md

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
- **Semantic Tokens.** Type-aware syntax highlighting that goes beyond what a TextMate grammar can achieve. Classes, interfaces, enums, traits, methods, properties, parameters, variables, functions, constants, and template parameters all get distinct token types. Modifiers convey declaration sites, static access, readonly, deprecated, and abstract status.
13-
- **Inlay hints.** Parameter name and by-reference indicators appear at call sites (`textDocument/inlayHint`). Hints are suppressed when the argument already makes the parameter obvious: variable names matching the parameter, property accesses with a matching trailing identifier, string literals whose content matches, well-known single-parameter functions like `count` and `strlen`, and spread arguments. Named arguments never receive a redundant hint.
13+
- **Inlay hints.** Parameter name and by-reference indicators appear at call sites (`textDocument/inlayHint`). Hints are suppressed when the argument already makes the parameter obvious: variable names matching the parameter, property accesses with a matching trailing identifier, string literals whose content matches, well-known single-parameter functions like `count` and `strlen`, and spread arguments. Named arguments never receive a redundant hint. Mixed positional and named argument ordering is handled correctly.
1414
- **PHPStan diagnostics.** PHPStan errors appear inline as you edit, using PHPStan's editor mode (`--tmp-file` / `--instead-of`). Auto-detects `vendor/bin/phpstan` or `$PATH`. Runs in a dedicated background worker with a 2-second debounce and at most one process at a time, so native diagnostics are never blocked. Configurable via `[phpstan]` in `.phpantom.toml` (`command`, `memory-limit`, `timeout`). "Ignore PHPStan error" and "Remove unnecessary @phpstan-ignore" code actions manage inline ignore comments.
15-
- **Formatting.** Built-in PHP formatting via mago-formatter (PER-CS 2.0 style). Formatting works out of the box without any external tools. Projects that depend on php-cs-fixer or PHP_CodeSniffer in their `composer.json` `require-dev` automatically use those tools instead (both can run in sequence). Per-tool command overrides and disable switches in `[formatting]` in `.phpantom.toml`.
15+
- **Formatting.** Built-in PHP formatting via mago-formatter (PER-CS 2.0 style). Formatting works out of the box without any external tools. Projects that depend on php-cs-fixer or PHP_CodeSniffer in their `composer.json` `require-dev` automatically use those tools instead (both can run in sequence). Per-tool command overrides and disable switches in `[formatting]` in `.phpantom.toml`. External formatters run without blocking completions or other requests.
1616
- **Document Symbols.** The outline sidebar and breadcrumbs now show classes, interfaces, traits, enums, methods, properties, constants, and standalone functions with correct nesting, icons, visibility detail, and deprecation tags.
1717
- **Workspace Symbols.** "Go to Symbol in Workspace" (Ctrl+T / Cmd+T) searches across all indexed files including vendor classes. Results include namespace context and deprecation markers, sorted by relevance.
1818
- **Type Hierarchy.** "Show Type Hierarchy" on any class, interface, trait, or enum reveals its supertypes and subtypes with full up-and-down navigation through the inheritance tree, including cross-file resolution and transitive relationships.
@@ -24,19 +24,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2424
- **Implementation error diagnostic.** Concrete classes that fail to implement all required methods from their interfaces or abstract parents are now flagged with an Error-severity diagnostic on the class name. The existing "Implement missing methods" quick-fix appears inline alongside the error. Cyclic hierarchies are handled gracefully.
2525
- **Argument count diagnostic.** Flags function and method calls that pass too few or too many arguments. Variadic parameters and argument unpacking are handled correctly.
2626
- **Change visibility.** Code action on any method, property, constant, or promoted constructor parameter offers to change its visibility (`public`, `protected`, `private`).
27-
- **Update docblock.** Code action on a function or method whose existing docblock is out of sync with its signature. Adds missing `@param` tags, removes stale ones, reorders to match the signature, fixes contradicted types, and removes redundant `@return void`. Refinement types and unrelated tags are preserved.
27+
- **Update docblock.** Code action on a function or method whose existing docblock is out of sync with its signature. Adds missing `@param` tags, removes stale ones, reorders to match the signature, fixes contradicted types, and removes redundant `@return void`. Refinement types and unrelated tags are preserved. Handles `@param $name` (no type) correctly.
2828
- **PHPDoc block generation.** Typing `/**` above any declaration generates a docblock skeleton. Tags are only emitted when the native type hint needs enrichment. Properties and constants always get `@var`. Class-likes with templated parents or interfaces get `@extends`/`@implements` tags. Uncaught exceptions get `@throws` with auto-import. Works both via completion and on-type formatting.
2929
- **PHPDoc `@var` completion.** Inline `@var` above variable assignments sorts first and pre-fills the inferred type when available. Template parameters from `@template` enrich `@param`, `@return`, and `@var` type hints.
3030
- **File rename on class rename.** Renaming a class whose file follows PSR-4 naming now also renames the file to match. The file is only renamed when it contains a single class-like declaration and the editor supports file rename operations.
3131
- **`@see` and `@link` improvements.** `@see` references in docblocks now work with go-to-definition (class, member, and function forms). Hover popups show all `@link` and `@see` URLs as clickable links. Deprecation diagnostics include `@see` targets when the `@deprecated` docblock references them.
3232
- **Progress indicators.** Go to Implementation and Find References now show a progress indicator in the editor while scanning.
3333

34-
### Fixed
35-
36-
- **Double-dollar in docblock variable completion.** Typing `$` or `$va` after a type in a `@param` tag no longer produces `$$var` in editors like Helix and Neovim that do not treat `$` as a word character. Completions now replace the typed prefix in place.
37-
- **Closure and arrow function variable scope.** Variable name completion now correctly respects PHP scoping rules for anonymous functions and arrow functions. Parameters of a closure are visible inside its body, `use`-captured variables appear alongside them, and `$this` is available when the closure is defined in an instance method. Outer method locals that were not captured do not leak in. Arrow function parameters are now visible inside the arrow body while the enclosing scope's variables remain accessible, matching PHP's implicit capture behaviour.
38-
- **Namespace alias completion.** Typing a class name through a namespace alias (e.g. `OA\Re` with `use OpenApi\Attributes as OA`) now correctly suggests classes under the aliased namespace such as `OA\Response` and `OA\RequestBody`. Previously only unrelated classes matched because the alias was not expanded before prefix matching.
39-
4034
### Changed
4135

4236
- **Pull diagnostics.** Diagnostics are now delivered via the LSP 3.17 pull model (`textDocument/diagnostic`) when the editor supports it. The editor requests diagnostics only for visible files, and cross-file invalidation uses `workspace/diagnostic/refresh` instead of recomputing every open tab. Clients without pull support fall back to the previous push model automatically.
@@ -47,23 +41,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4741

4842
### Fixed
4943

44+
- **Closure and arrow function variable scope.** Variable name completion now correctly respects PHP scoping rules for anonymous functions and arrow functions. Parameters of a closure are visible inside its body, `use`-captured variables appear alongside them, and `$this` is available when the closure is defined in an instance method. Outer method locals that were not captured do not leak in. Arrow function parameters are now visible inside the arrow body while the enclosing scope's variables remain accessible, matching PHP's implicit capture behaviour.
45+
- **Namespace alias completion.** Typing a class name through a namespace alias (e.g. `OA\Re` with `use OpenApi\Attributes as OA`) now correctly suggests classes under the aliased namespace such as `OA\Response` and `OA\RequestBody`. Previously only unrelated classes matched because the alias was not expanded before prefix matching.
5046
- **Virtual property merging.** Native type hints are now considered when determining virtual property specificity. Previously only docblock types were compared, causing properties with native PHP type declarations (e.g., `public string $name`) to be incorrectly overridden by less specific virtual properties.
47+
- **Native type override compatibility.** A docblock type only overrides a native type hint when it is a compatible refinement. For example, `class-string<Foo>` can refine `string` and `positive-int` can refine `int`, but `array<int>` no longer incorrectly overrides `string`. Previously any docblock type with generic parameters was accepted regardless of compatibility.
5148
- **PHPStan pseudo-type recognition.** Types like `non-positive-int`, `non-negative-int`, `non-zero-int`, `lowercase-string`, `truthy-string`, `callable-object`, and many other PHPStan pseudo-types are now recognized across the entire pipeline. Previously they could be misresolved as class names, flagged as contradictions in docblock updates, or missing from PHPDoc completion suggestions.
52-
- **PHPStan diagnostics.** Fixed a path matching false positive where files with similar name suffixes (e.g. `AFoo.php` vs `Foo.php`) could receive each other's PHPStan diagnostics.
53-
- **Update docblock action.** Docblocks containing `@param $name` with no type (e.g. `@param $name Some description`) are now parsed correctly. Previously the parameter name was consumed as the type token, causing the action to add a duplicate `@param mixed $name` tag.
5449
- **Rename updates imports.** Renaming a class now updates `use` statement FQNs (last segment only), preserves explicit aliases, and introduces an alias when the new name collides with an existing import in the same file. Previously, `use` statements were left unchanged, breaking the file.
5550
- **Trait alias go-to-definition.** Clicking a trait alias (e.g. `$this->__foo()` from `use Foo { foo as __foo; }`) now jumps to the trait method instead of the class's own same-named method.
56-
- **Diagnostics.** Enums that implement interfaces are now checked for missing methods, matching the existing behaviour for concrete classes. Scalar member access errors now detect method-return chains where an intermediate call returns a scalar type. By-reference `@param` annotations no longer produce a false "unknown class" diagnostic.
51+
- **Diagnostics.** Enums that implement interfaces are now checked for missing methods, matching the existing behaviour for concrete classes. Scalar member access errors now detect method-return chains where an intermediate call returns a scalar type. By-reference `@param` annotations no longer produce a false "unknown class" diagnostic. Duplicate diagnostics from different analysis phases are now reliably collapsed into a single entry per range. Deprecated-usage checks no longer block the instant diagnostic push.
5752
- **Hover on empty arrays.** `[]` and `array()` literals now show `array` on hover instead of nothing.
5853
- **Catch clause completion.** Throwable interfaces and abstract exception classes now appear in catch clause completions. Previously only concrete, non-abstract classes were offered.
59-
- **Inlay hints with named arguments.** Parameter name hints now map correctly when named and positional arguments are mixed. Previously, positional arguments were matched by their index in the argument list, so `greet(city: 'NYC', 'Alice')` would label `'Alice'` as `age:` instead of `name:`.
6054
- **Type-hint and PHPDoc completion.** Traits are now excluded from completions in parameter types, return types, property types, and PHPDoc type tags. `@throws` continues to use Throwable-filtered completion.
6155
- **Position encoding.** All LSP position conversions now correctly count UTF-16 code units, matching the LSP specification. Files containing emoji or supplementary Unicode characters no longer produce incorrect positions for completions, hover, go-to-definition, references, highlights, or code actions.
62-
- **Deprecated-usage diagnostics.** Deprecated-usage checks no longer block the instant Phase 1 diagnostic push. They now run in Phase 2 (slow) alongside other type-resolution-dependent checks, so syntax errors and unused-import warnings appear without delay.
63-
- **Formatting responsiveness.** The formatting handler no longer blocks the async runtime while waiting for external tools. Completion, hover, and other requests remain responsive while php-cs-fixer or PHP_CodeSniffer runs.
64-
- **PHPStan stale diagnostics.** Closing a file while PHPStan is running no longer leaves stale diagnostics in the cache. Previously the PHPStan worker could finish and write results back after the cache was cleared, causing old diagnostics to reappear on the next open.
65-
- **Graceful shutdown.** Background workers (diagnostic, PHPStan) now stop promptly when the editor closes. Previously, a running PHPStan process could continue consuming CPU and memory for up to 60 seconds after shutdown.
66-
- **Diagnostic deduplication.** Duplicate diagnostics from different analysis phases (fast, slow, PHPStan) are now reliably collapsed into a single entry per range. Previously, non-adjacent duplicates could survive because the dedup pass only removed consecutive matches, and diagnostics with different wording but the same range were treated as distinct.
6756

6857
## [0.5.0] - 2026-03-12
6958

docs/todo/bugs.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,5 +101,3 @@ on by default.
101101
- `src/config.rs` — new field + accessor + default config comment.
102102
- `src/diagnostics/argument_count.rs` — gate the too-many block.
103103
- `tests/` — update or add tests asserting the default-off behaviour.
104-
105-
---

src/code_actions/update_docblock.rs

Lines changed: 18 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ use tower_lsp::lsp_types::*;
2424
use crate::Backend;
2525
use crate::completion::phpdoc::generation::enrichment_plain;
2626
use crate::completion::source::throws_analysis;
27+
use crate::docblock::is_compatible_refinement;
2728
use crate::docblock::type_strings::split_type_token;
2829
use crate::types::ClassInfo;
2930
use crate::util::offset_to_position;
@@ -728,26 +729,22 @@ fn is_type_contradiction(doc_type: &str, native_type: &str) -> bool {
728729
return false;
729730
}
730731

731-
// If the docblock type carries generics or callable signatures, it's
732-
// likely a refinement.
733-
if doc_type.contains('<') || doc_type.contains('{') || doc_type.contains('(') {
734-
// Check that the base type is compatible.
735-
let doc_base = base_type(doc_type).to_lowercase();
736-
let native_base = base_type(native_type).to_lowercase();
737-
if doc_base == native_base {
738-
return false;
739-
}
740-
// `list<X>` refines `array`.
741-
if native_base == "array" && (doc_base == "list" || doc_base == "array") {
742-
return false;
743-
}
744-
}
745-
746-
// PHPStan-style type refinements like `non-empty-string`, `positive-int`,
747-
// `class-string`, `literal-string`, etc. are refinements, not contradictions.
748-
let doc_lower = doc_type.to_lowercase();
749-
let native_lower = native_type.to_lowercase();
750-
if is_refinement_of(&doc_lower, &native_lower) {
732+
// Check whether the docblock type is a compatible refinement of the
733+
// native type (e.g. `class-string<Foo>` refines `string`,
734+
// `list<User>` refines `array`, `positive-int` refines `int`).
735+
// This uses the shared refinement checker that also guards
736+
// `resolve_effective_type`.
737+
let native_stripped = native_type
738+
.strip_prefix('\\')
739+
.unwrap_or(native_type)
740+
.strip_prefix('?')
741+
.unwrap_or(native_type.strip_prefix('\\').unwrap_or(native_type));
742+
let doc_stripped = doc_type
743+
.strip_prefix('\\')
744+
.unwrap_or(doc_type)
745+
.strip_prefix('?')
746+
.unwrap_or(doc_type.strip_prefix('\\').unwrap_or(doc_type));
747+
if is_compatible_refinement(doc_stripped, &native_stripped.to_ascii_lowercase()) {
751748
return false;
752749
}
753750

@@ -766,7 +763,7 @@ fn is_type_contradiction(doc_type: &str, native_type: &str) -> bool {
766763
if doc_bases.len() == 1 && native_bases.len() == 1 {
767764
let db = &doc_bases[0];
768765
let nb = &native_bases[0];
769-
if db != nb && !is_refinement_of(db, nb) {
766+
if db != nb && !is_compatible_refinement(db, nb) {
770767
return true;
771768
}
772769
}
@@ -788,68 +785,6 @@ fn normalize_type_for_comparison(t: &str) -> String {
788785
parts.join("|")
789786
}
790787

791-
/// Get the base type (before `<` or `{`).
792-
fn base_type(t: &str) -> &str {
793-
let t = t.strip_prefix('\\').unwrap_or(t);
794-
let t = t.strip_prefix('?').unwrap_or(t);
795-
if let Some(pos) = t.find('<') {
796-
&t[..pos]
797-
} else if let Some(pos) = t.find('{') {
798-
&t[..pos]
799-
} else {
800-
t
801-
}
802-
}
803-
804-
/// Check if `doc_type` is a known refinement of `native_type`.
805-
fn is_refinement_of(doc_type: &str, native_type: &str) -> bool {
806-
let refinements: &[(&str, &str)] = &[
807-
("non-empty-string", "string"),
808-
("non-falsy-string", "string"),
809-
("numeric-string", "string"),
810-
("literal-string", "string"),
811-
("class-string", "string"),
812-
("callable-string", "string"),
813-
("truthy-string", "string"),
814-
("lowercase-string", "string"),
815-
("uppercase-string", "string"),
816-
("non-empty-lowercase-string", "string"),
817-
("non-empty-uppercase-string", "string"),
818-
("non-empty-literal-string", "string"),
819-
("interface-string", "string"),
820-
("trait-string", "string"),
821-
("enum-string", "string"),
822-
("positive-int", "int"),
823-
("negative-int", "int"),
824-
("non-negative-int", "int"),
825-
("non-positive-int", "int"),
826-
("non-zero-int", "int"),
827-
("int-mask", "int"),
828-
("int-mask-of", "int"),
829-
("non-empty-array", "array"),
830-
("non-empty-list", "array"),
831-
("list", "array"),
832-
("associative-array", "array"),
833-
("callable-array", "array"),
834-
("callable-object", "object"),
835-
("closed-resource", "resource"),
836-
("open-resource", "resource"),
837-
];
838-
for &(refined, base) in refinements {
839-
// Handle both `doc_type` directly matching and `doc_type` having
840-
// generic params (e.g. `class-string<foo>`).
841-
let doc_base = if let Some(pos) = doc_type.find('<') {
842-
&doc_type[..pos]
843-
} else {
844-
doc_type
845-
};
846-
if doc_base == refined && native_type == base {
847-
return true;
848-
}
849-
}
850-
false
851-
}
852-
853788
/// Split a union type string into its components.
854789
fn split_union_types(t: &str) -> Vec<String> {
855790
t.split('|')

src/docblock/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ pub(crate) mod type_strings;
4444
// working without modification.
4545

4646
// Core tags
47+
pub(crate) use tags::is_compatible_refinement;
4748
pub use tags::{
4849
extract_all_param_tags, extract_deprecation_message, extract_deprecation_with_see,
4950
extract_link_urls, extract_mixin_tags, extract_param_closure_this, extract_param_description,

0 commit comments

Comments
 (0)