Skip to content

Commit fbb2f7c

Browse files
committed
static/$this return types are no longer flagged when passed where a
`Stringable` object is accepted
1 parent 93c9b26 commit fbb2f7c

5 files changed

Lines changed: 25 additions & 35 deletions

File tree

docs/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6363

6464
### Fixed
6565

66+
- **`static`/`$this` return types are no longer flagged when passed where a `Stringable` object is accepted.** Passing a value typed `static(Foo)` or `$this(Foo)` to a `string` parameter reported a type mismatch even when `Foo` implements `Stringable`, which PHP accepts by calling `__toString()`. This hit any use of `SimpleXMLElement`, whose magic `__get` returns `static`, so code like `(string) $xml->Body->Message` reported a false positive on every argument passed to a `string` parameter.
6667
- **Inline `{@see}` references nested inside other docblock text are now found.** An inline `{@see Foo}` written in the description of another tag (`@param Type $x see {@see Foo}`), or nested inside another inline tag (`{@deprecated use {@see Bar} instead}`), was invisible to go-to-definition, find references, and rename: the previous scan located a reference by searching for the next `}` after `{@see `, which stopped at the first brace it met rather than the one that actually closed the tag. Inline `{@see}` references are now read from the same PHPDoc parse tree as everything else in the docblock.
6768
- **Docblock navigation works in `@method` and `@property` tags written across several lines.** A tag whose type wrapped onto a continuation line, such as a `@method Collection<int, Item> fetchAll(Filter $filter)` broken after the `<`, only ever had its first line read. Everything after it was invisible: go-to-definition, find references, and rename did nothing on the method or property name, on the type arguments, or on the parameter types, and the truncated first line was reported as a class named `Collection<` that resolved to nothing. Docblock positions now come from the PHPDoc grammar itself, so every name in such a tag is navigable wherever it sits.
6869
- **Docblock navigation lands on the right name in types that mix a `*` wildcard with a non-ASCII name.** In a type such as `@return Map<Café, *, User>`, go-to-definition, find references, and rename measured every name after the accented one against the wrong bytes, so clicking `User` resolved nothing (or the wrong symbol). The PHPStan `*` wildcard is now read directly by the type grammar rather than rewritten to `mixed` beforehand, which removes the byte-offset bookkeeping that was corrupting the positions.

docs/todo.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,6 @@ unlikely to move the needle for most users.
108108
| D5 | [External tool diagnostic suppression actions](todo/diagnostics.md#d5-external-tool-diagnostic-suppression-actions) | Low | Low |
109109
| D15 | [Unused parameter diagnostic](todo/diagnostics.md#d15-unused-parameter-diagnostic) | Low | Low |
110110
| | **[Bug Fixes](todo/bugs.md)** | | |
111-
| B1 | [`static`/`$this` return types rejected where a `Stringable` object is accepted](todo/bugs.md#b1-staticthis-return-types-are-rejected-where-a-stringable-object-is-accepted) | Medium | Low |
112111
| | **[Code Actions](todo/actions.md)** | | |
113112
| A40 | [Generate method from call](todo/actions.md#a40-generate-method-from-call) | Medium-High | Medium |
114113
| A41 | [Create class from non-existing name](todo/actions.md#a41-create-class-from-non-existing-name) | Medium | Medium |

docs/todo/bugs.md

Lines changed: 1 addition & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,37 +7,4 @@ pipeline so it produces correct data. Downstream consumers
77
(diagnostics, hover, completion, definition) should never need
88
to second-guess upstream output.
99

10-
## B1. `static`/`$this` return types are rejected where a `Stringable` object is accepted
11-
12-
**Impact: Medium · Effort: Low**
13-
14-
`type_mismatch_argument` flags passing a `static(Foo)` or `$this(Foo)`
15-
typed value to a `string` parameter even when `Foo` implements
16-
`Stringable`, which PHP accepts (it calls `__toString()`). Minimal
17-
reproduction:
18-
19-
```php
20-
class Node implements Stringable {
21-
private function __get($name): static {}
22-
public function __toString(): string { return ''; }
23-
}
24-
$n = new Node();
25-
throw new \Exception($n->Body->Msg);
26-
// Argument 1 ($message) expects string, got static(Node)
27-
```
28-
29-
The cause is `PhpType::is_object_like` in `src/php_type/mod.rs`: it
30-
answers `true` for `Named`, `Generic`, `ObjectShape` and `Nullable`, but
31-
`false` for `TypeKind::StaticType` and `TypeKind::ThisType`. The
32-
`Stringable` branch in
33-
`src/diagnostics/type_errors/compatibility.rs::is_type_compatible` is
34-
gated on `is_object_like()`, so it never runs for those two kinds and the
35-
check falls through to a mismatch. `base_name()` right next to it *does*
36-
handle both kinds, which is what makes the diagnostic message name the
37-
bound class correctly while the compatibility check ignores it.
38-
39-
Every other consumer of `is_object_like` is affected the same way, so fix
40-
it there rather than in the diagnostic. Real-world hit: any use of
41-
`SimpleXMLElement`, whose stubbed `__get` returns `static`, so
42-
`(string) $xml->Body->Message` style code reports a false positive on
43-
every argument passed to a `string` parameter.
10+
No outstanding items.

src/php_type/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1166,6 +1166,7 @@ impl PhpType {
11661166
TypeKind::Named(s) => s.eq_ignore_ascii_case("object") || !is_scalar_name(s),
11671167
TypeKind::Generic(g) => !is_scalar_name(&g.name),
11681168
TypeKind::ObjectShape(_) => true,
1169+
TypeKind::StaticType(s) | TypeKind::ThisType(s) => !is_scalar_name(s),
11691170
TypeKind::Nullable(inner) => inner.is_object_like(),
11701171
_ => false,
11711172
}

tests/integration/diagnostics_type_errors.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1426,6 +1426,28 @@ function test(): void {
14261426
);
14271427
}
14281428

1429+
#[test]
1430+
fn no_diagnostic_for_static_return_stringable_to_string() {
1431+
let php = r#"<?php
1432+
class Node implements Stringable {
1433+
public function __get($name): static {
1434+
return $this;
1435+
}
1436+
public function __toString(): string { return ''; }
1437+
}
1438+
1439+
function test(): void {
1440+
$n = new Node();
1441+
throw new \Exception($n->Body);
1442+
}
1443+
"#;
1444+
let diags = collect(php);
1445+
assert!(
1446+
!has_type_error(&diags),
1447+
"Should not flag static(Stringable) object passed to string, got: {diags:?}"
1448+
);
1449+
}
1450+
14291451
// ═══════════════════════════════════════════════════════════════════════════
14301452
// New rules: PHP type juggling
14311453
// ═══════════════════════════════════════════════════════════════════════════

0 commit comments

Comments
 (0)