diff --git a/src/completion/builder.rs b/src/completion/builder.rs index 09d89380..f3a02448 100644 --- a/src/completion/builder.rs +++ b/src/completion/builder.rs @@ -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. @@ -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()` diff --git a/tests/fixtures/completion/instance_no_static_only.fixture b/tests/fixtures/completion/instance_no_static_only.fixture index 6e56e747..b85e8735 100644 --- a/tests/fixtures/completion/instance_no_static_only.fixture +++ b/tests/fixtures/completion/instance_no_static_only.fixture @@ -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( --- <> \ No newline at end of file +$bar-><> diff --git a/tests/fixtures/member_access/static_on_instance.fixture b/tests/fixtures/member_access/static_on_instance.fixture index ec01fc89..6df33275 100644 --- a/tests/fixtures/member_access/static_on_instance.fixture +++ b/tests/fixtures/member_access/static_on_instance.fixture @@ -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( --- <> \ No newline at end of file +$bar-><> diff --git a/tests/integration/completion_properties.rs b/tests/integration/completion_properties.rs index 5733b1bc..4feb0bd6 100644 --- a/tests/integration/completion_properties.rs +++ b/tests/integration/completion_properties.rs @@ -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!( + "\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() {