Skip to content

Commit a36b973

Browse files
committed
Handle special cases for parent:: static:: and self::r
1 parent d3a4046 commit a36b973

6 files changed

Lines changed: 644 additions & 17 deletions

File tree

example.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ public function getDisplayName(): string
304304

305305
// ─── Child Class (parent:: resolution) ──────────────────────────────────────
306306

307-
class AdminUser extends User
307+
final class AdminUser extends User
308308
{
309309
/** @var string[] */
310310
private array $permissions = [];
@@ -366,6 +366,7 @@ class Container
366366
* @template TClass
367367
* @param string|null $abstract
368368
* @return ($abstract is class-string<TClass> ? TClass : mixed)
369+
* @throws Exception
369370
*/
370371
public function make(?string $abstract = null): mixed
371372
{

src/completion/builder.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,20 @@ impl Backend {
9191

9292
// Methods — filtered by static / instance, excluding magic methods
9393
for method in &target_class.methods {
94+
// `__construct` is meaningful to call explicitly via `::` or
95+
// `parent::` (e.g. `parent::__construct(...)` in a child class),
96+
// so we only suppress it for `->` access. All other magic
97+
// methods are always suppressed.
98+
let is_constructor = method.name.eq_ignore_ascii_case("__construct");
9499
if Self::is_magic_method(&method.name) {
95-
continue;
100+
let allow = is_constructor
101+
&& matches!(
102+
access_kind,
103+
AccessKind::DoubleColon | AccessKind::ParentDoubleColon
104+
);
105+
if !allow {
106+
continue;
107+
}
96108
}
97109

98110
// parent:: excludes private members
@@ -104,7 +116,11 @@ impl Backend {
104116

105117
let include = match access_kind {
106118
AccessKind::Arrow => !method.is_static,
107-
AccessKind::DoubleColon => method.is_static,
119+
// `::` normally shows only static methods, but `__construct`
120+
// is an exception — it's an instance method that is routinely
121+
// called via `parent::__construct(...)`, `self::__construct()`,
122+
// `static::__construct()`, or even `ClassName::__construct()`.
123+
AccessKind::DoubleColon => method.is_static || is_constructor,
108124
// parent:: shows both static and non-static methods
109125
AccessKind::ParentDoubleColon => true,
110126
AccessKind::Other => true,

src/parser.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,8 @@ impl Backend {
557557
let start_offset = class.left_brace.start.offset;
558558
let end_offset = class.right_brace.end.offset;
559559

560+
let is_final = class.modifiers.contains_final();
561+
560562
classes.push(ClassInfo {
561563
name: class_name,
562564
methods,
@@ -567,6 +569,7 @@ impl Backend {
567569
parent_class,
568570
used_traits,
569571
mixins,
572+
is_final,
570573
});
571574
}
572575
Statement::Interface(iface) => {
@@ -625,6 +628,7 @@ impl Backend {
625628
parent_class,
626629
used_traits,
627630
mixins,
631+
is_final: false,
628632
});
629633
}
630634
Statement::Trait(trait_def) => {
@@ -679,6 +683,7 @@ impl Backend {
679683
parent_class: None,
680684
used_traits,
681685
mixins,
686+
is_final: false,
682687
});
683688
}
684689
Statement::Enum(enum_def) => {
@@ -736,6 +741,7 @@ impl Backend {
736741
let start_offset = enum_def.left_brace.start.offset;
737742
let end_offset = enum_def.right_brace.end.offset;
738743

744+
// Enums are implicitly final — they cannot be extended.
739745
classes.push(ClassInfo {
740746
name: enum_name,
741747
methods,
@@ -746,6 +752,7 @@ impl Backend {
746752
parent_class,
747753
used_traits,
748754
mixins,
755+
is_final: true,
749756
});
750757
}
751758
Statement::Namespace(namespace) => {

src/server.rs

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -326,16 +326,26 @@ impl LanguageServer for Backend {
326326
self.find_or_load_function(&candidates)
327327
};
328328

329-
let candidates = Self::resolve_target_classes(
330-
&target.subject,
331-
target.access_kind,
332-
current_class,
333-
&classes,
334-
&content,
335-
cursor_offset.unwrap_or(0),
336-
&class_loader,
337-
Some(&function_loader),
338-
);
329+
// `static::` in a final class is equivalent to `self::` but
330+
// suggests the class can be subclassed — which it can't.
331+
// Suppress suggestions to nudge the developer toward `self::`.
332+
let suppress =
333+
target.subject == "static" && current_class.is_some_and(|cc| cc.is_final);
334+
335+
let candidates = if suppress {
336+
vec![]
337+
} else {
338+
Self::resolve_target_classes(
339+
&target.subject,
340+
target.access_kind,
341+
current_class,
342+
&classes,
343+
&content,
344+
cursor_offset.unwrap_or(0),
345+
&class_loader,
346+
Some(&function_loader),
347+
)
348+
};
339349

340350
if !candidates.is_empty() {
341351
// `parent::` is syntactically `::` but semantically

src/types.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,4 +210,9 @@ pub struct ClassInfo {
210210
/// classes via magic methods (`__call`, `__get`, `__set`, etc.).
211211
/// Resolved to fully-qualified names during post-processing.
212212
pub mixins: Vec<String>,
213+
/// Whether the class is declared `final`.
214+
///
215+
/// Final classes cannot be extended, so `static::` is equivalent to
216+
/// `self::` and need not be offered as a separate completion subject.
217+
pub is_final: bool,
213218
}

0 commit comments

Comments
 (0)