Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/completion/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,8 @@ pub(crate) fn build_method_label(method: &MethodInfo) -> String {
/// Build completion items for a resolved class, filtered by access kind
/// and visibility scope.
///
/// - `Arrow` access: returns only non-static methods and properties.
/// - `Arrow` access: returns both instance and static methods, but only
/// non-static properties.
/// - `DoubleColon` access: returns only static methods, static properties, and constants.
/// - `ParentDoubleColon` access: returns both static and non-static methods,
/// static properties, and constants — but excludes private members.
Expand Down Expand Up @@ -352,7 +353,9 @@ pub(crate) fn build_completion_items(
}

let include = match access_kind {
AccessKind::Arrow => !method.is_static,
// PHP allows calling static methods through an instance, so
// surface them in `->` completion as well.
AccessKind::Arrow => true,
// External `ClassName::` shows only static methods, but
// `__construct` is an exception — it's an instance method
// that is routinely called via `ClassName::__construct()`
Expand Down
8 changes: 4 additions & 4 deletions tests/fixtures/completion/instance_no_static_only.fixture
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// test: static method not shown on instance -> access
// test: static method shown on instance -> access
// feature: completion
// Adapted from phpactor WorseClassMemberCompletorTest 'shows static member on instance method'
// PHPantom does not show static methods on instance -> access, which is correct
// PHP allows calling static methods through an instance, so completion should show them.
// expect: hello(
// expect_absent: goodbye(
// expect: goodbye(
---
<?php

Expand All @@ -13,4 +13,4 @@ class BarBar {
}

$bar = new BarBar();
$bar-><>
$bar-><>
8 changes: 4 additions & 4 deletions tests/fixtures/member_access/static_on_instance.fixture
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
// test: static method hidden from instance -> access
// test: static method shown on instance -> access
// feature: completion
// Adapted from phpactor WorseClassMemberCompletorTest 'shows static member on instance method'
// PHPantom deliberately hides static-only members from -> access
// PHP allows calling static methods through an instance, so completion should show them.
// expect: hello(
// expect_absent: goodbye(
// expect: goodbye(
---
<?php

Expand All @@ -13,4 +13,4 @@ class BarBar {
}

$bar = new BarBar();
$bar-><>
$bar-><>
66 changes: 66 additions & 0 deletions tests/integration/completion_properties.rs
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,72 @@ async fn test_completion_private_method_hidden_outside_class() {
}
}

#[tokio::test]
async fn test_completion_instance_access_includes_public_static_methods() {
let backend = create_test_backend();

let uri = Url::parse("file:///vis_instance_static_method.php").unwrap();
let text = concat!(
"<?php\n",
"class Service {\n",
" public static function make(): self { return new self(); }\n",
" public function run(): void {}\n",
"}\n",
"$svc = new Service();\n",
"$svc->\n",
);

backend
.did_open(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "php".to_string(),
version: 1,
text: text.to_string(),
},
})
.await;

let result = backend
.completion(CompletionParams {
text_document_position: TextDocumentPositionParams {
text_document: TextDocumentIdentifier { uri },
position: Position {
line: 6,
character: 6,
},
},
work_done_progress_params: WorkDoneProgressParams::default(),
partial_result_params: PartialResultParams::default(),
context: None,
})
.await
.unwrap();

assert!(result.is_some(), "Should return completions");
match result.unwrap() {
CompletionResponse::Array(items) => {
let method_names: Vec<&str> = items
.iter()
.filter(|i| i.kind == Some(CompletionItemKind::METHOD))
.map(|i| i.filter_text.as_deref().unwrap())
.collect();

assert!(
method_names.contains(&"run"),
"Should include instance method 'run', got: {:?}",
method_names
);
assert!(
method_names.contains(&"make"),
"Should include static method 'make' on instance access, got: {:?}",
method_names
);
}
_ => panic!("Expected CompletionResponse::Array"),
}
}

/// `$this->` inside the same class should show private and protected members.
#[tokio::test]
async fn test_completion_private_and_protected_visible_inside_own_class() {
Expand Down
Loading