Skip to content

Commit 399b2b1

Browse files
calebdwAJenbo
authored andcommitted
feat(completion): include static methods on instance access
Closes #162 Signed-off-by: Anders Jenbo <anders@jenbo.dk>
1 parent d04de94 commit 399b2b1

9 files changed

Lines changed: 96 additions & 24 deletions

File tree

docs/CHANGELOG.md

Lines changed: 1 addition & 0 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+
- **Static methods complete on instance access.** Member completion after `->` now offers a class's static methods alongside its instance methods, since PHP lets you call a static method through an instance (`$obj->make()`). Static properties remain excluded, as they are only reachable via `::`. Contributed by @calebdw in https://github.com/AJenbo/phpantom_lsp/pull/174.
1213
- **Array-callable navigation.** Method-name strings in array callables — `[Controller::class, 'method']` and `[$object, 'method']` — now resolve like a real member reference. This makes go-to-definition, find-references, and rename work on Laravel controller actions such as `Route::get('/', [IndexPageController::class, 'indexPage'])`.
1314
- **Array-callable method completion.** Typing inside the method-name string of an array callable (`[Controller::class, '|']`) now offers method name completions from the resolved class, including inherited and trait methods. Works with `Class::class` constants, `$this`, and typed variables. (thanks @calebdw)
1415
- **Magic methods complete when implemented.** Magic methods declared on a class (`__invoke`, `__toString`, `__call`, and the rest) are now offered in member completion, so explicit calls like `$x->__invoke()` autocomplete and support go-to-definition. They are sorted below the regular methods so they never appear at the top of the list.

src/completion/builder.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,8 @@ pub(crate) fn build_method_label(method: &MethodInfo) -> String {
283283
/// Build completion items for a resolved class, filtered by access kind
284284
/// and visibility scope.
285285
///
286-
/// - `Arrow` access: returns only non-static methods and properties.
286+
/// - `Arrow` access: returns both instance and static methods, but only
287+
/// non-static properties.
287288
/// - `DoubleColon` access: returns only static methods, static properties, and constants.
288289
/// - `ParentDoubleColon` access: returns both static and non-static methods,
289290
/// static properties, and constants — but excludes private members.
@@ -352,7 +353,9 @@ pub(crate) fn build_completion_items(
352353
}
353354

354355
let include = match access_kind {
355-
AccessKind::Arrow => !method.is_static,
356+
// PHP allows calling static methods through an instance, so
357+
// surface them in `->` completion as well.
358+
AccessKind::Arrow => true,
356359
// External `ClassName::` shows only static methods, but
357360
// `__construct` is an exception — it's an instance method
358361
// that is routinely called via `ClassName::__construct()`
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
// test: static method not shown on instance -> access
1+
// test: static method shown on instance -> access
22
// feature: completion
33
// Adapted from phpactor WorseClassMemberCompletorTest 'shows static member on instance method'
4-
// PHPantom does not show static methods on instance -> access, which is correct
4+
// PHP allows calling static methods through an instance, so completion should show them.
55
// expect: hello(
6-
// expect_absent: goodbye(
6+
// expect: goodbye(
77
---
88
<?php
99

@@ -13,4 +13,4 @@ class BarBar {
1313
}
1414

1515
$bar = new BarBar();
16-
$bar-><>
16+
$bar-><>
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
// test: static method hidden from instance -> access
1+
// test: static method shown on instance -> access
22
// feature: completion
33
// Adapted from phpactor WorseClassMemberCompletorTest 'shows static member on instance method'
4-
// PHPantom deliberately hides static-only members from -> access
4+
// PHP allows calling static methods through an instance, so completion should show them.
55
// expect: hello(
6-
// expect_absent: goodbye(
6+
// expect: goodbye(
77
---
88
<?php
99

@@ -13,4 +13,4 @@ class BarBar {
1313
}
1414

1515
$bar = new BarBar();
16-
$bar-><>
16+
$bar-><>

tests/integration/completion_access_kind.rs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -163,10 +163,11 @@ async fn test_completion_arrow_shows_only_non_static() {
163163
method_names.contains(&"helper"),
164164
"Arrow should include non-static method 'helper'"
165165
);
166-
// Should NOT include static method `create`
166+
// PHP allows calling static methods through an instance,
167+
// so `->` access should include static methods too.
167168
assert!(
168-
!method_names.contains(&"create"),
169-
"Arrow should exclude static method 'create'"
169+
method_names.contains(&"create"),
170+
"Arrow should include static method 'create' (PHP allows static calls via instance)"
170171
);
171172

172173
// Should include non-static property `count`
@@ -350,10 +351,11 @@ async fn test_completion_arrow_with_partial_typed_identifier() {
350351
"Should include instanceMethod"
351352
);
352353
assert!(method_names.contains(&"test"), "Should include test");
353-
// Should NOT include static method even with partial typing
354+
// PHP allows calling static methods through an instance,
355+
// so `->` access should include static methods too.
354356
assert!(
355-
!method_names.contains(&"staticMethod"),
356-
"Should exclude staticMethod when using ->"
357+
method_names.contains(&"staticMethod"),
358+
"Should include staticMethod when using -> (PHP allows static calls via instance)"
357359
);
358360
}
359361
_ => panic!("Expected CompletionResponse::Array"),

tests/integration/completion_cross_file.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -959,10 +959,10 @@ async fn test_cross_file_classmap_resolution() {
959959
"Should include instance method 'render' resolved via classmap, got {:?}",
960960
method_names
961961
);
962-
// Static method should not appear via ->
962+
// PHP allows calling static methods through an instance.
963963
assert!(
964-
!method_names.contains(&"create"),
965-
"Should exclude static 'create' from -> access"
964+
method_names.contains(&"create"),
965+
"Should include static 'create' on -> access (PHP allows static calls via instance)"
966966
);
967967
}
968968
_ => panic!("Expected CompletionResponse::Array"),

tests/integration/completion_properties.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,72 @@ async fn test_completion_private_method_hidden_outside_class() {
500500
}
501501
}
502502

503+
#[tokio::test]
504+
async fn test_completion_instance_access_includes_public_static_methods() {
505+
let backend = create_test_backend();
506+
507+
let uri = Url::parse("file:///vis_instance_static_method.php").unwrap();
508+
let text = concat!(
509+
"<?php\n",
510+
"class Service {\n",
511+
" public static function make(): self { return new self(); }\n",
512+
" public function run(): void {}\n",
513+
"}\n",
514+
"$svc = new Service();\n",
515+
"$svc->\n",
516+
);
517+
518+
backend
519+
.did_open(DidOpenTextDocumentParams {
520+
text_document: TextDocumentItem {
521+
uri: uri.clone(),
522+
language_id: "php".to_string(),
523+
version: 1,
524+
text: text.to_string(),
525+
},
526+
})
527+
.await;
528+
529+
let result = backend
530+
.completion(CompletionParams {
531+
text_document_position: TextDocumentPositionParams {
532+
text_document: TextDocumentIdentifier { uri },
533+
position: Position {
534+
line: 6,
535+
character: 6,
536+
},
537+
},
538+
work_done_progress_params: WorkDoneProgressParams::default(),
539+
partial_result_params: PartialResultParams::default(),
540+
context: None,
541+
})
542+
.await
543+
.unwrap();
544+
545+
assert!(result.is_some(), "Should return completions");
546+
match result.unwrap() {
547+
CompletionResponse::Array(items) => {
548+
let method_names: Vec<&str> = items
549+
.iter()
550+
.filter(|i| i.kind == Some(CompletionItemKind::METHOD))
551+
.map(|i| i.filter_text.as_deref().unwrap())
552+
.collect();
553+
554+
assert!(
555+
method_names.contains(&"run"),
556+
"Should include instance method 'run', got: {:?}",
557+
method_names
558+
);
559+
assert!(
560+
method_names.contains(&"make"),
561+
"Should include static method 'make' on instance access, got: {:?}",
562+
method_names
563+
);
564+
}
565+
_ => panic!("Expected CompletionResponse::Array"),
566+
}
567+
}
568+
503569
/// `$this->` inside the same class should show private and protected members.
504570
#[tokio::test]
505571
async fn test_completion_private_and_protected_visible_inside_own_class() {

tests/integration/completion_property_chains.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1154,10 +1154,10 @@ async fn test_var_property_chain_no_static_members() {
11541154
"Should include instance method 'get' from Cache. Got: {:?}",
11551155
labels
11561156
);
1157-
// Static method `flush` should NOT appear for `->` access
1157+
// PHP allows calling static methods through an instance.
11581158
assert!(
1159-
!labels.iter().any(|l| l.starts_with("flush")),
1160-
"Should NOT include static method 'flush' via ->. Got: {:?}",
1159+
labels.iter().any(|l| l.starts_with("flush")),
1160+
"Should include static method 'flush' via -> (PHP allows static calls via instance). Got: {:?}",
11611161
labels
11621162
);
11631163
}

tests/integration/completion_variables.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -408,8 +408,8 @@ async fn test_completion_new_self_variable() {
408408
"Should include non-static 'build'"
409409
);
410410
assert!(
411-
!method_names.contains(&"create"),
412-
"Should exclude static 'create' via ->"
411+
method_names.contains(&"create"),
412+
"Should include static 'create' via -> (PHP allows static calls via instance)"
413413
);
414414
}
415415
_ => panic!("Expected CompletionResponse::Array"),

0 commit comments

Comments
 (0)