Skip to content

Commit 6efa489

Browse files
committed
Add argument type mismatch diagnostics and fix property narrowing cache
1 parent 58b4b65 commit 6efa489

51 files changed

Lines changed: 8355 additions & 1350 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/ARCHITECTURE.md

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -976,11 +976,16 @@ Files that are not open (vendor files loaded via PSR-4 on demand) do not get a s
976976

977977
## Diagnostic Philosophy
978978

979-
PHPantom's diagnostics assert **type coverage**, not bug detection. The question they answer is: "can the LSP resolve every class, member, and function call in this project?" When the answer is yes, completion works everywhere with no dead spots, and downstream tools like PHPStan get the type information they need to find real bugs at every strictness level.
979+
PHPantom earns trust through two promises:
980980

981-
This is a deliberate division of labour. PHPStan only complains about missing types at levels 6, 9, and 10. PHPantom fills those gaps cheaply and immediately so PHPStan can focus on logic errors rather than fighting incomplete type information. The two tools complement each other at any PHPStan level: PHPantom rejects structural problems fast (unknown class, unknown member, wrong argument count, unimplemented interface method), and PHPStan hunts for semantic bugs (type mismatches, unreachable code, null safety violations) with full type context available.
981+
1. **Provide suggestions for everything the developer expects.** Completion, hover, go-to-definition, and every other LSP feature should work on every symbol in the project with no dead spots.
982+
2. **Stay silent on everything the developer knows works.** Every diagnostic PHPantom reports must be something that is genuinely wrong, not something that is merely imprecise or unconventional. If the code runs without errors, PHPantom should not complain about it.
982983

983-
The native diagnostic categories reflect this philosophy:
984+
Most PHP developers have learned to ignore IDE errors. They have seen tools produce walls of false positives from bugs in the inference engine, mistakes in stubs, or overly strict rules applied to code that works perfectly well in production. The result is that developers treat red squiggles as noise and test in production to see if a problem is real. PHPantom exists to break that cycle. When a diagnostic appears, it must mean something. A single false positive costs more trust than ten missed true positives, because it teaches the developer to stop reading.
985+
986+
### Type Coverage Diagnostics
987+
988+
The core diagnostic categories assert **type coverage**: can the LSP resolve every class, member, and function call in the project?
984989

985990
| Diagnostic | What it asserts |
986991
|---|---|
@@ -990,11 +995,35 @@ The native diagnostic categories reflect this philosophy:
990995
| Unknown members | Every member access resolves to a declared member. |
991996
| Unknown functions | Every function call resolves to a declared function. |
992997
| Argument count | Call sites match the target's parameter count. |
998+
| Argument type mismatch | Every argument's type is definitely compatible with the parameter's declared type. |
993999
| Implementation errors | Concrete classes satisfy their interface/abstract contracts. |
9941000
| Undefined variables | Every variable read has a prior definition in scope. |
9951001
| Deprecated usage | The developer is aware of deprecation. |
9961002

997-
None of these are type-compatibility checks, dead-code analysis, or control-flow reasoning. That is by design. A project that passes all of PHPantom's diagnostics has 100% type coverage from the LSP's perspective: every symbol is resolvable, every completion trigger produces results, and every hover shows a type.
1003+
A project that passes all of these has 100% type coverage from the LSP's perspective: every symbol is resolvable, every completion trigger produces results, and every hover shows a type.
1004+
1005+
This also serves as a deliberate division of labour with PHPStan. PHPStan only complains about missing types at levels 6, 9, and 10. PHPantom fills those gaps cheaply and immediately so PHPStan can focus on logic errors rather than fighting incomplete type information. PHPantom rejects structural problems fast (unknown class, unknown member, wrong argument count, unimplemented interface method), and PHPStan hunts for semantic bugs with full type context available.
1006+
1007+
### Type Compatibility Diagnostics: Yes / No / Maybe
1008+
1009+
Argument type mismatch diagnostics (`type_error.argument`) go beyond coverage into type-compatibility territory. Here the risk of false positives is highest, because PHP is a dynamically typed language with pervasive implicit coercion, and real-world codebases are full of patterns that are technically imprecise but functionally correct.
1010+
1011+
Every type check is classified into one of three buckets:
1012+
1013+
- **Yes** (definitely compatible). `Cat` passed to `Animal` where `Cat extends Animal`. No diagnostic.
1014+
- **No** (definitely incompatible). `array` passed to `int`. Diagnostic reported.
1015+
- **Maybe** (might work, might not). `?Carbon` passed to `Carbon`, bare `array` passed to `array<string>`, `HtmlString` passed to `string`. No diagnostic.
1016+
1017+
**Only "no" produces a diagnostic.** The "maybe" bucket is deliberately wide. If there is any reasonable interpretation under which the code could work at runtime, PHPantom stays silent. Examples of "maybe":
1018+
1019+
- **Nullable to non-nullable** (`?Carbon` to `Carbon`). The developer may have null-checked before the call. We cannot see the guard clause from the call site alone.
1020+
- **Bare array to typed array** (`array` to `array<string>`). A bare `array` is untyped. It might contain strings. We cannot prove otherwise.
1021+
- **Stringable objects to string** (`HtmlString` to `string`). PHP calls `__toString()` at runtime. We follow PHP's runtime behaviour.
1022+
- **Int to string**. PHP coerces integers to strings in many contexts and we cannot know the `strict_types` setting.
1023+
- **`list<X>` and `array<int, X>`**. Used interchangeably in practice. array<int, X> might have sequential-keys in actuality.
1024+
- **Interface to concrete subtype** (`CarbonInterface` to `Carbon`). The interface is broader, but in practice the value is almost always the expected concrete type.
1025+
1026+
This conservatism is the foundation for trust. Once developers learn that PHPantom's red squiggles are never false alarms, they start paying attention to them. At that point, a stricter mode can be offered as an opt-in for teams that want more help. But the default must be: if we are not certain, we stay silent.
9981027

9991028
## Analyze Command
10001029

docs/CHANGELOG.md

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- **Argument type mismatch diagnostics.** Flags function and method calls where an argument's resolved type is incompatible with the declared parameter type.
1213
- **Untyped property type inference from constructor.** Go-to-definition and completion now resolve property types that have no type declaration by inspecting the constructor body for `$this->prop = new ClassName()` assignments and promoted parameter defaults like `private $prop = new ClassName()`. PHPDoc generation also uses the inferred type: typing `/**` above an untyped property produces `@var Repository` instead of `@var mixed` when the constructor assigns `new Repository()`. This covers common patterns in legacy PHP codebases and PHP 8.1+ code using `new` in initializers. Contributed by @lucasacoutinho in https://github.com/AJenbo/phpantom_lsp/pull/81.
1314
- **PHPCS diagnostic proxy.** PHP_CodeSniffer violations are surfaced as LSP diagnostics. When `squizlabs/php_codesniffer` is in `require-dev`, PHPCS is auto-detected via Composer's bin-dir; otherwise it falls back to `$PATH`. Each violation uses the sniff name as the diagnostic code (e.g. `PSR12.Files.FileHeader.MissingPHPVersion`), maps PHPCS error/warning levels to LSP severity, and marks fixable violations in diagnostic data. Runs in a dedicated background worker with the same debounce and single-pending-URI design as the PHPStan proxy. Configurable under `[phpcs]` in `.phpantom.toml` with `command`, `standard`, and `timeout` options.
1415
- **Linked editing ranges.** Place the cursor on a variable and all occurrences within its definition region (from one assignment to the next) enter linked editing mode. Typing a new name updates every occurrence simultaneously without affecting reassigned uses of the same variable name. Ranges exclude the leading `$` sigil so that typing before a variable (e.g. wrapping it in a function call) does not propagate to other occurrences.
@@ -20,9 +21,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2021

2122
### Fixed
2223

24+
- **Short-name collisions eliminated project-wide.** Two unrelated classes sharing a short name (e.g. `App\Carbon` vs `Vendor\Carbon`) were treated as identical across diagnostics, go-to-implementation, trait conflict resolution, throws analysis, and catch-clause filtering. Argument type mismatch diagnostics now use the class hierarchy walker for all Traversable, Stringable, and Countable checks instead of matching short names or suffixes.
25+
- **Transitive interface inheritance.** A class implementing an interface that extends another interface (e.g. `ResponseInterface` extends `MessageInterface`) is now correctly recognized as a subtype of the parent interface. Previously only directly-declared interfaces were checked.
26+
- **Fluent method chains in namespaced classes.** Chaining methods that return `static` or `self` now resolves correctly across namespaces, fixing broken completion and false diagnostics on builder patterns.
27+
- **Property narrowing after guard clauses.** `if (!$this->prop instanceof Foo) { return; }` now correctly narrows `$this->prop` to `Foo` in subsequent code. Previously all accesses to the same property shared a single cached resolution, ignoring guards.
28+
- **Hover on generic methods shows concrete return type.** Hovering over a method inherited from a generic trait now shows the substituted return type (e.g. `Model|null`) instead of the raw template parameter name. Applies to both method return types and property type hints on generic classes.
29+
- **False-positive undefined variable for by-reference parameters in namespaces.** Passing an undefined variable to a by-reference parameter no longer produces a false "undefined variable" diagnostic when the function or method is in a namespace. Instance method calls with by-reference parameters are also handled.
2330
- **Missing blank line between namespace and first import.** When auto-importing a class into a file that has a namespace declaration but no existing `use` statements and no blank line after the namespace, the inserted `use` statement is now preceded by a blank line (PSR-12 convention). Files that already have a blank line after the namespace are left unchanged.
24-
- **"Remove all unused imports" missing in braced namespaces.** The bulk action now appears when the cursor is on a `use` line inside a `namespace Foo { … }` block. Previously the brace-depth heuristic treated these as trait-use statements inside a class body.
25-
- **False-positive undefined variable on nested array access assignment.** `$b['a']['a'] = 'a'` no longer flags `$b` as undefined. The scope collector now correctly propagates write context through nested `ArrayAccess` and `ArrayAppend` expressions, so the base variable is recognized as defined.
31+
- **"Remove all unused imports" missing in braced namespaces.** The bulk action now appears when the cursor is on a `use` line inside a `namespace Foo { … }` block.
32+
- **False-positive undefined variable on nested array access assignment.** `$b['a']['a'] = 'a'` no longer flags `$b` as undefined. Write context now propagates through nested array access expressions so the base variable is recognized as defined.
33+
- **Exception types in `catch` clauses matched incorrectly across namespaces.** Thrown exception types are now matched against `catch` clause types regardless of whether short names or fully-qualified names are used. The `@throws` tag comparison in docblock update actions now resolves names through the class loader, fixing false "missing @throws" suggestions for root-namespace exceptions (e.g. `RuntimeException`) in namespaced files without an explicit `use` import.
34+
- **False "undefined variable" diagnostics for by-reference parameters in inherited static methods and constructors.** By-reference parameters in parent class static methods and constructors are now correctly resolved when called on a child class, preventing false diagnostics for variables defined via the call. Class lookup for by-reference resolution now tries the fully-qualified name first, preventing incorrect matches when multiple classes share a short name across namespaces.
35+
- **Nested `match(true)` expressions.** Multiple `match(true)` expressions sharing narrowed variable names no longer produce incorrect diagnostics.
36+
- **Variables prefixed with `$this` receiving stale type information.** Variables like `$thisItem` or `$thisValue` no longer receive stale type information from `$this`-related narrowing.
37+
- **False-positive type errors on generic class methods and templated static methods.** Class-level `@template` parameters (e.g. `Collection<User>`) are now substituted into method parameter types before checking argument compatibility. Method-level `@template` parameters (e.g. PHPUnit's `assertSame`) are resolved from call-site argument types, covering literals, variables, enum cases, property accesses, and method call return types.
2638

2739
## [0.7.0] - 2026-04-08
2840

0 commit comments

Comments
 (0)