Skip to content

Commit 43d2ea8

Browse files
committed
Fix self:: and parent:: lacking content
1 parent 68ca359 commit 43d2ea8

5 files changed

Lines changed: 223 additions & 39 deletions

File tree

src/completion/builder.rs

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,9 @@ impl Backend {
9898
/// `is_self_or_ancestor` should be `true` when the cursor is inside the
9999
/// target class itself or inside a class that (transitively) extends the
100100
/// target. When `true`, `__construct` is offered for `::` access
101-
/// (e.g. `self::__construct()`, `parent::__construct()`,
102-
/// `ClassName::__construct()` from within a subclass). When `false`,
103-
/// magic methods are suppressed entirely.
101+
/// (e.g. `self::__construct()`, `static::__construct()`,
102+
/// `parent::__construct()`, `ClassName::__construct()` from within a
103+
/// subclass). When `false`, magic methods are suppressed entirely.
104104
pub(crate) fn build_completion_items(
105105
target_class: &ClassInfo,
106106
access_kind: AccessKind,
@@ -115,9 +115,9 @@ impl Backend {
115115

116116
// Methods — filtered by static / instance, excluding magic methods
117117
for method in &target_class.methods {
118-
// `__construct` is meaningful to call explicitly via `::` or
119-
// `parent::` when inside the same class or a subclass
120-
// (e.g. `parent::__construct(...)`, `self::__construct()`).
118+
// `__construct` is meaningful to call explicitly via `::` when
119+
// inside the same class or a subclass (e.g.
120+
// `parent::__construct(...)`, `self::__construct()`).
121121
// Outside of that relationship, magic methods are suppressed.
122122
let is_constructor = method.name.eq_ignore_ascii_case("__construct");
123123
if Self::is_magic_method(&method.name) {
@@ -145,12 +145,14 @@ impl Backend {
145145

146146
let include = match access_kind {
147147
AccessKind::Arrow => !method.is_static,
148-
// `::` normally shows only static methods, but `__construct`
149-
// is an exception — it's an instance method that is routinely
150-
// called via `parent::__construct(...)`, `self::__construct()`,
151-
// `static::__construct()`, or even `ClassName::__construct()`.
148+
// External `ClassName::` shows only static methods, but
149+
// `__construct` is an exception — it's an instance method
150+
// that is routinely called via `ClassName::__construct()`
151+
// from within a subclass.
152152
AccessKind::DoubleColon => method.is_static || is_constructor,
153-
// parent:: shows both static and non-static methods
153+
// `self::`, `static::`, and `parent::` show both static and
154+
// non-static methods (PHP allows calling instance methods
155+
// via `::` from within the class hierarchy).
154156
AccessKind::ParentDoubleColon => true,
155157
AccessKind::Other => true,
156158
};
@@ -187,9 +189,9 @@ impl Backend {
187189
continue;
188190
}
189191

190-
// Static properties accessed via `::` or `parent::` need the `$`
191-
// prefix (e.g. `self::$path`), while instance properties via `->`
192-
// use the bare name (e.g. `$this->path`).
192+
// Static properties accessed via `::` need the `$` prefix
193+
// (e.g. `self::$path`, `ClassName::$path`), while instance
194+
// properties via `->` use the bare name (e.g. `$this->path`).
193195
let display_name = if access_kind == AccessKind::DoubleColon
194196
|| access_kind == AccessKind::ParentDoubleColon
195197
{

src/server.rs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -376,14 +376,19 @@ impl LanguageServer for Backend {
376376
};
377377

378378
if !candidates.is_empty() {
379-
// `parent::` is syntactically `::` but semantically
380-
// different: it shows both static and instance members
381-
// while excluding private ones.
382-
let effective_access = if target.subject == "parent" {
383-
AccessKind::ParentDoubleColon
384-
} else {
385-
target.access_kind
386-
};
379+
// `parent::`, `self::`, and `static::` are syntactically
380+
// `::` but semantically different from external static
381+
// access: they show both static and instance members
382+
// (PHP allows `self::nonStaticMethod()` etc. from an
383+
// instance context). `parent::` additionally excludes
384+
// private members, which is handled by visibility
385+
// filtering below.
386+
let effective_access =
387+
if matches!(target.subject.as_str(), "parent" | "self" | "static") {
388+
AccessKind::ParentDoubleColon
389+
} else {
390+
target.access_kind
391+
};
387392

388393
// Merge completion items from all candidate classes,
389394
// deduplicating by label so ambiguous variables show

src/types.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,15 @@ pub enum AccessKind {
8888
Arrow,
8989
/// Completion triggered after `::` (static access).
9090
DoubleColon,
91-
/// Completion triggered after `parent::`.
91+
/// Completion triggered after `parent::`, `self::`, or `static::`.
9292
///
93-
/// This is an oddball: it shows both static **and** instance methods
94-
/// (since PHP allows `parent::nonStaticMethod()` from a child class),
95-
/// plus constants and static properties — but excludes private members.
93+
/// All three keywords use `::` syntax but differ from external static
94+
/// access (`ClassName::`): they show both static **and** instance
95+
/// methods (PHP allows `self::nonStaticMethod()`,
96+
/// `static::nonStaticMethod()`, and `parent::nonStaticMethod()` from
97+
/// an instance context), plus constants and static properties.
98+
/// Visibility filtering (e.g. excluding private members for `parent::`)
99+
/// is handled separately via `current_class_name`.
96100
ParentDoubleColon,
97101
/// No specific access operator detected (e.g. inside class body).
98102
Other,

tests/completion_access_kind.rs

Lines changed: 184 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ async fn test_completion_arrow_shows_only_non_static() {
196196
}
197197

198198
#[tokio::test]
199-
async fn test_completion_double_colon_shows_only_static_and_constants() {
199+
async fn test_completion_self_double_colon_shows_all_members() {
200200
let backend = create_test_backend();
201201

202202
let uri = Url::parse("file:///dcolon.php").unwrap();
@@ -262,33 +262,35 @@ async fn test_completion_double_colon_shows_only_static_and_constants() {
262262
// Should include static method `create`
263263
assert!(
264264
method_names.contains(&"create"),
265-
"DoubleColon should include static method 'create'"
265+
"self:: should include static method 'create'"
266266
);
267-
// Should NOT include non-static methods `run` and `helper`
267+
// self:: shows both static and non-static methods (PHP allows
268+
// calling instance methods via `self::` from within the class).
268269
assert!(
269-
!method_names.contains(&"run"),
270-
"DoubleColon should exclude non-static method 'run'"
270+
method_names.contains(&"run"),
271+
"self:: should include non-static method 'run'"
271272
);
272273
assert!(
273-
!method_names.contains(&"helper"),
274-
"DoubleColon should exclude non-static method 'helper'"
274+
method_names.contains(&"helper"),
275+
"self:: should include non-static method 'helper'"
275276
);
276277

277278
// Should include static property `$instance` (with $ prefix for :: access)
278279
assert!(
279280
property_names.contains(&"$instance"),
280-
"DoubleColon should include static property '$instance'"
281+
"self:: should include static property '$instance'"
281282
);
282-
// Should NOT include non-static property `count` (or `$count`)
283+
// Non-static properties are still excluded — `self::$count`
284+
// is not valid PHP for instance properties.
283285
assert!(
284286
!property_names.contains(&"count") && !property_names.contains(&"$count"),
285-
"DoubleColon should exclude non-static property 'count'"
287+
"self:: should exclude non-static property 'count'"
286288
);
287289

288290
// Should include constant `MAX`
289291
assert!(
290292
constant_names.contains(&"MAX"),
291-
"DoubleColon should include constant 'MAX'"
293+
"self:: should include constant 'MAX'"
292294
);
293295
}
294296
_ => panic!("Expected CompletionResponse::Array"),
@@ -900,3 +902,174 @@ async fn test_double_colon_shows_class_keyword() {
900902
_ => panic!("Expected CompletionResponse::Array"),
901903
}
902904
}
905+
906+
// ─── self:: and static:: show the same symbols as parent:: ──────────────────
907+
908+
/// `self::` and `static::` should show both static **and** non-static methods,
909+
/// just like `parent::` does, because PHP allows calling instance methods via
910+
/// `self::method()` and `static::method()` from within the class hierarchy.
911+
#[tokio::test]
912+
async fn test_self_and_static_show_non_static_methods_like_parent() {
913+
let backend = create_test_backend();
914+
915+
let uri = Url::parse("file:///self_static_parity.php").unwrap();
916+
let text = concat!(
917+
"<?php\n",
918+
"class Animal {\n",
919+
" public function breathe(): void {}\n",
920+
" public static function kingdom(): string { return 'Animalia'; }\n",
921+
" public const LEGS = 4;\n",
922+
" public static string $species = '';\n",
923+
" public int $age = 0;\n",
924+
"}\n",
925+
"class Dog extends Animal {\n",
926+
" public function bark(): void {}\n",
927+
" function testSelf() {\n",
928+
" self::\n",
929+
" }\n",
930+
" function testStatic() {\n",
931+
" static::\n",
932+
" }\n",
933+
"}\n",
934+
);
935+
936+
let open_params = DidOpenTextDocumentParams {
937+
text_document: TextDocumentItem {
938+
uri: uri.clone(),
939+
language_id: "php".to_string(),
940+
version: 1,
941+
text: text.to_string(),
942+
},
943+
};
944+
backend.did_open(open_params).await;
945+
946+
// ── self:: (line 11, character 14) ──
947+
let self_params = CompletionParams {
948+
text_document_position: TextDocumentPositionParams {
949+
text_document: TextDocumentIdentifier { uri: uri.clone() },
950+
position: Position {
951+
line: 11,
952+
character: 14,
953+
},
954+
},
955+
work_done_progress_params: WorkDoneProgressParams::default(),
956+
partial_result_params: PartialResultParams::default(),
957+
context: None,
958+
};
959+
960+
let self_result = backend.completion(self_params).await.unwrap();
961+
assert!(self_result.is_some(), "self:: should return completions");
962+
963+
let self_items = match self_result.unwrap() {
964+
CompletionResponse::Array(items) => items,
965+
_ => panic!("Expected CompletionResponse::Array"),
966+
};
967+
968+
let self_method_names: Vec<&str> = self_items
969+
.iter()
970+
.filter(|i| i.kind == Some(CompletionItemKind::METHOD))
971+
.map(|i| i.filter_text.as_deref().unwrap())
972+
.collect();
973+
let self_property_names: Vec<&str> = self_items
974+
.iter()
975+
.filter(|i| i.kind == Some(CompletionItemKind::PROPERTY))
976+
.map(|i| i.label.as_str())
977+
.collect();
978+
let self_constant_names: Vec<&str> = self_items
979+
.iter()
980+
.filter(|i| i.kind == Some(CompletionItemKind::CONSTANT))
981+
.map(|i| i.label.as_str())
982+
.collect();
983+
984+
// self:: should show both static and non-static methods
985+
assert!(
986+
self_method_names.contains(&"breathe"),
987+
"self:: should include inherited non-static 'breathe', got: {:?}",
988+
self_method_names
989+
);
990+
assert!(
991+
self_method_names.contains(&"kingdom"),
992+
"self:: should include inherited static 'kingdom', got: {:?}",
993+
self_method_names
994+
);
995+
assert!(
996+
self_method_names.contains(&"bark"),
997+
"self:: should include own non-static 'bark', got: {:?}",
998+
self_method_names
999+
);
1000+
// Static properties shown, instance properties excluded
1001+
assert!(
1002+
self_property_names.contains(&"$species"),
1003+
"self:: should include static property '$species', got: {:?}",
1004+
self_property_names
1005+
);
1006+
assert!(
1007+
!self_property_names.contains(&"age") && !self_property_names.contains(&"$age"),
1008+
"self:: should exclude non-static property 'age', got: {:?}",
1009+
self_property_names
1010+
);
1011+
// Constants shown
1012+
assert!(
1013+
self_constant_names.contains(&"LEGS"),
1014+
"self:: should include constant 'LEGS', got: {:?}",
1015+
self_constant_names
1016+
);
1017+
1018+
// ── static:: (line 14, character 16) ──
1019+
let static_params = CompletionParams {
1020+
text_document_position: TextDocumentPositionParams {
1021+
text_document: TextDocumentIdentifier { uri },
1022+
position: Position {
1023+
line: 14,
1024+
character: 16,
1025+
},
1026+
},
1027+
work_done_progress_params: WorkDoneProgressParams::default(),
1028+
partial_result_params: PartialResultParams::default(),
1029+
context: None,
1030+
};
1031+
1032+
let static_result = backend.completion(static_params).await.unwrap();
1033+
assert!(
1034+
static_result.is_some(),
1035+
"static:: should return completions"
1036+
);
1037+
1038+
let static_items = match static_result.unwrap() {
1039+
CompletionResponse::Array(items) => items,
1040+
_ => panic!("Expected CompletionResponse::Array"),
1041+
};
1042+
1043+
let static_method_names: Vec<&str> = static_items
1044+
.iter()
1045+
.filter(|i| i.kind == Some(CompletionItemKind::METHOD))
1046+
.map(|i| i.filter_text.as_deref().unwrap())
1047+
.collect();
1048+
let static_constant_names: Vec<&str> = static_items
1049+
.iter()
1050+
.filter(|i| i.kind == Some(CompletionItemKind::CONSTANT))
1051+
.map(|i| i.label.as_str())
1052+
.collect();
1053+
1054+
// static:: should also show both static and non-static methods
1055+
assert!(
1056+
static_method_names.contains(&"breathe"),
1057+
"static:: should include inherited non-static 'breathe', got: {:?}",
1058+
static_method_names
1059+
);
1060+
assert!(
1061+
static_method_names.contains(&"kingdom"),
1062+
"static:: should include inherited static 'kingdom', got: {:?}",
1063+
static_method_names
1064+
);
1065+
assert!(
1066+
static_method_names.contains(&"bark"),
1067+
"static:: should include own non-static 'bark', got: {:?}",
1068+
static_method_names
1069+
);
1070+
assert!(
1071+
static_constant_names.contains(&"LEGS"),
1072+
"static:: should include constant 'LEGS', got: {:?}",
1073+
static_constant_names
1074+
);
1075+
}

tests/completion_variables.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -499,8 +499,8 @@ async fn test_completion_static_double_colon() {
499499
"Should include static 'create'"
500500
);
501501
assert!(
502-
!method_names.contains(&"run"),
503-
"Should exclude non-static 'run'"
502+
method_names.contains(&"run"),
503+
"static:: should include non-static 'run'"
504504
);
505505
assert!(
506506
constant_names.contains(&"MAX"),

0 commit comments

Comments
 (0)