Skip to content

Commit 5e4a597

Browse files
committed
Resolve self::class, static::class, and parent::class to current class
in type resolution
1 parent 3304310 commit 5e4a597

3 files changed

Lines changed: 85 additions & 0 deletions

File tree

docs/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2828
- **Property `self`/`static` type resolution.** Properties declared with `@var self|null` or `static` type annotations now resolve to the owning class name instead of displaying the raw `self`/`static` keyword in hover and type inference.
2929
- **Magic `__get` property access.** Accessing undefined properties on objects with a `__get` method now resolves to the method's declared return type, even when `__get` has no template parameters (e.g. `SimpleXMLElement::$child` resolves to `SimpleXMLElement`).
3030
- **Magic `__call` method return type.** Calling undefined methods on objects with a `__call` method now resolves to `__call`'s declared return type for hover and type inference.
31+
- **`self::class` and `static::class` in template arguments.** Passing `self::class` or `static::class` to a method with `@template T` + `@param class-string<T>` now correctly resolves T to the enclosing class instead of failing to resolve or resolving to the called class.
3132
- **Multi-namespace function return type resolution.** In files with multiple `namespace { }` blocks, function return types were resolved against the first namespace instead of the function's own namespace. This caused incorrect type inference for variables assigned from function calls in later namespace blocks.
3233
- **Template inference through stub interfaces.** `@template-implements` on stub-loaded interfaces (e.g. `IteratorAggregate<int, Foo>`) now correctly propagates substituted return types to child methods that omit a return type annotation. Previously these resolved as untyped.
3334
- **Generic method return types from `@var` annotations.** When a variable is annotated with a generic type (e.g. `/** @var Collection<int, User> */ $items`), method calls on that variable now correctly substitute class-level template parameters into the return type. Previously, return types like `TValue` remained unsubstituted.

src/completion/call_resolution.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2444,6 +2444,19 @@ impl Backend {
24442444
.chars()
24452445
.all(|c| c.is_alphanumeric() || c == '_' || c == '\\')
24462446
{
2447+
// self::class / static::class / parent::class resolve relative
2448+
// to the class at the call site.
2449+
if name.eq_ignore_ascii_case("self") || name.eq_ignore_ascii_case("static") {
2450+
return ctx
2451+
.current_class
2452+
.map(|c| PhpType::Named(c.fqn().to_string()));
2453+
}
2454+
if name.eq_ignore_ascii_case("parent") {
2455+
return ctx
2456+
.current_class
2457+
.and_then(|c| c.parent_class.as_ref())
2458+
.map(|p| PhpType::Named(p.to_string()));
2459+
}
24472460
let resolved_name = if let Some(cls) = (ctx.class_loader)(name) {
24482461
cls.fqn().to_string()
24492462
} else {

tests/integration/completion_generics.rs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4979,6 +4979,77 @@ async fn test_method_template_general_does_not_break_class_string() {
49794979
}
49804980
}
49814981

4982+
/// `self::class` passed to a `class-string<T>` template parameter should
4983+
/// resolve T to the enclosing class, not the class that owns the method.
4984+
#[tokio::test]
4985+
async fn test_method_template_self_class_resolves_to_enclosing_class() {
4986+
let backend = create_test_backend();
4987+
4988+
let uri = Url::parse("file:///self_class_tpl.php").unwrap();
4989+
let text = concat!(
4990+
"<?php\n",
4991+
"class OrmService {\n",
4992+
" /**\n",
4993+
" * @template T of AbstractEntity\n",
4994+
" * @param class-string<T> $class\n",
4995+
" * @return T[]\n",
4996+
" */\n",
4997+
" public function getByQuery(string $class, string $query): array {}\n",
4998+
"}\n",
4999+
"class AbstractEntity {}\n",
5000+
"class Category extends AbstractEntity {\n",
5001+
" public function isVisible(): bool {}\n",
5002+
" public function getChildren(): array {\n",
5003+
" $children = (new OrmService())->getByQuery(self::class, 'SELECT 1');\n",
5004+
" foreach ($children as $child) {\n",
5005+
" $child->\n",
5006+
" }\n",
5007+
" }\n",
5008+
"}\n",
5009+
);
5010+
5011+
let open_params = DidOpenTextDocumentParams {
5012+
text_document: TextDocumentItem {
5013+
uri: uri.clone(),
5014+
language_id: "php".to_string(),
5015+
version: 1,
5016+
text: text.to_string(),
5017+
},
5018+
};
5019+
backend.did_open(open_params).await;
5020+
5021+
let line = text.lines().position(|l| l.contains("$child->")).unwrap() as u32;
5022+
let col = text.lines().nth(line as usize).unwrap().find("$child->").unwrap() as u32 + 8;
5023+
let completion_params = CompletionParams {
5024+
text_document_position: TextDocumentPositionParams {
5025+
text_document: TextDocumentIdentifier { uri },
5026+
position: Position { line, character: col },
5027+
},
5028+
work_done_progress_params: Default::default(),
5029+
partial_result_params: Default::default(),
5030+
context: None,
5031+
};
5032+
let result = backend.completion(completion_params).await.unwrap();
5033+
assert!(result.is_some(), "Completion should return results");
5034+
5035+
match result.unwrap() {
5036+
CompletionResponse::Array(items) => {
5037+
let method_names: Vec<&str> = items
5038+
.iter()
5039+
.filter(|i| i.kind == Some(CompletionItemKind::METHOD))
5040+
.map(|i| i.filter_text.as_deref().unwrap_or(&i.label))
5041+
.collect();
5042+
5043+
assert!(
5044+
method_names.contains(&"isVisible"),
5045+
"self::class should resolve T to Category, got methods: {:?}",
5046+
method_names
5047+
);
5048+
}
5049+
_ => panic!("Expected CompletionResponse::Array"),
5050+
}
5051+
}
5052+
49825053
/// $this->method($arg) inside a class body via assignment.
49835054
/// Verifies the AST-based resolve_rhs_expression path.
49845055
#[tokio::test]

0 commit comments

Comments
 (0)