Skip to content

Commit e7cbe8c

Browse files
committed
feat: suggest trait member overrides in completion
Typing a method or property name in a class body that uses a trait now suggests the trait's public and protected members as candidates, matching the existing override completion for parent classes and interfaces. Trait method replacements intentionally omit #[\Override] because PHP treats trait methods as copied into the class — adding the attribute on a trait-only method is a compile error. The completion detail shows 'trait' instead of 'override' to reflect this. Previously, classes that only used traits (no parent class, no interfaces) received no override suggestions at all because the handler bailed out early. That guard now also checks used_traits. Closes #276
1 parent ad2ec8a commit e7cbe8c

4 files changed

Lines changed: 298 additions & 70 deletions

File tree

docs/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- **`@phpstan-ignore` identifiers are highlighted and completed.** PHPStan ignore comments now highlight the `@phpstan-ignore` tag and each listed error identifier in both docblocks and ordinary `//` comments. Identifier completion works inside the comma-separated ignore list, using PHPStan diagnostic codes already seen in the current file while staying out of per-code parenthesized reasons. Contributed by @calebdw.
1313
- **Parent member override completion.** Typing a method name after `function` in a class body (for example `protected function get`) now suggests public and protected methods from parent classes and interfaces that can still be overridden or implemented, inserting a full signature snippet. The same flow suggests parent properties after `$` (for example `protected $tit`) and parent constants after `const`. Snippets insert `#[\Override]` above methods on PHP 8.3+, properties on PHP 8.5+, and constants on PHP 8.6+ (from `composer.json` / `config.platform.php`). Private members and ones already defined on the class are omitted. Contributed by @calebdw.
14+
- **Trait member override completion.** Typing a method or property name after `function` or `$` in a class that uses a trait now suggests the trait's public and protected members as override candidates. Unlike parent/interface overrides, trait method replacements do not insert `#[\Override]` (which would be a compile error for trait-only methods). Classes that only use traits without extending a parent or implementing an interface also receive suggestions. Contributed by @calebdw.
1415
- **Laravel schema dumps power Eloquent model properties.** PHPantom now scans Laravel database schema dumps from `database/schema` by default, reads `config/database.php` for connection drivers/defaults, and uses the parsed columns to synthesize Eloquent model properties with database types, nullability, and defaults in hover. Schema lookup respects model `$connection`, `$table`, Laravel `Connection`/`Table` attributes, dynamic table/connection overrides, and reloads when schema files or related config change. Contributed by @calebdw.
1516
- **Laravel migration scanning for Eloquent model properties.** PHPantom now parses Laravel migration files to infer database columns when schema dumps are not available or to overlay changes on top of dumps. Migrations are discovered from any non-vendor `database/migrations` directory (including nested modules like `modules/billing/database/migrations`), applied in global filename order, and support named and anonymous migration classes, `$connection` properties, `Schema::connection()` calls, `Blueprint::after()` nested closures, `virtualAs`/`storedAs` generated columns, and custom Blueprint macros registered via the existing macro scanner. Migration scanning is incremental: editing a single migration re-reads only that file and replays the cached plan over the base schema without re-reading other files. Configure with `[laravel.migrations] enabled` and `paths` in `.phpantom.toml`. Contributed by @calebdw.
1617
- **By-reference closure captures update outer variable types for immediately-invoked callables.** A closure passed to a callable parameter can now update the inferred type of variables captured with `use (&$var)` when the callable is considered immediately invoked. This follows PHPStan's defaults: function callable parameters are immediate unless marked with `@param-later-invoked-callable`, while method callable parameters are later-invoked unless marked with `@param-immediately-invoked-callable`. Contributed by @calebdw.

src/completion/context/override_completion.rs

Lines changed: 68 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -32,23 +32,25 @@ const METHOD_OVERRIDE_ATTR_MIN: PhpVersion = PhpVersion::new(8, 3);
3232
const PROPERTY_OVERRIDE_ATTR_MIN: PhpVersion = PhpVersion::new(8, 5);
3333
const CONSTANT_OVERRIDE_ATTR_MIN: PhpVersion = PhpVersion::new(8, 6);
3434

35-
/// Collect public/protected methods from parents and interfaces that the
36-
/// current class can still override or implement.
35+
/// Collect public/protected methods from parents, interfaces, and
36+
/// directly-used traits that the current class can still override or
37+
/// implement.
38+
///
39+
/// The returned bool (`skip_override_attr`) is `true` for methods that
40+
/// come from a directly-used trait (where `#[\Override]` would be a
41+
/// compile error) and `false` for parent/interface methods.
3742
pub(crate) fn collect_overridable_methods(
3843
class: &ClassInfo,
3944
partial: &str,
4045
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
41-
) -> Vec<(MethodInfo, String)> {
42-
let mut own_names: HashSet<String> = class
46+
) -> Vec<(MethodInfo, String, bool)> {
47+
let own_names: HashSet<String> = class
4348
.methods
4449
.iter()
4550
.map(|m| m.name.to_lowercase())
4651
.collect();
4752

48-
// Trait methods already on this class count as implemented.
49-
collect_trait_method_names(&class.used_traits, class_loader, &mut own_names, 0);
50-
51-
let mut results: Vec<(MethodInfo, String)> = Vec::new();
53+
let mut results: Vec<(MethodInfo, String, bool)> = Vec::new();
5254
let mut seen: HashSet<String> = HashSet::new();
5355
let mut visited: HashSet<String> = HashSet::new();
5456

@@ -59,6 +61,7 @@ pub(crate) fn collect_overridable_methods(
5961
seen: &mut seen,
6062
visited: &mut visited,
6163
results: &mut results,
64+
skip_override_attr: false,
6265
};
6366

6467
collector.collect_from_parent_chain(&class.parent_class, 0);
@@ -74,27 +77,10 @@ pub(crate) fn collect_overridable_methods(
7477
collector.collect_from_interface(iface, 0);
7578
}
7679

77-
results
78-
}
80+
collector.skip_override_attr = true;
81+
collector.collect_from_traits(&class.used_traits, 0);
7982

80-
fn collect_trait_method_names(
81-
traits: &[crate::atom::Atom],
82-
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
83-
names: &mut HashSet<String>,
84-
depth: usize,
85-
) {
86-
if depth > crate::types::MAX_INHERITANCE_DEPTH as usize {
87-
return;
88-
}
89-
for tname in traits {
90-
let Some(tr) = class_loader(tname) else {
91-
continue;
92-
};
93-
for m in &tr.methods {
94-
names.insert(m.name.to_lowercase());
95-
}
96-
collect_trait_method_names(&tr.used_traits, class_loader, names, depth + 1);
97-
}
83+
results
9884
}
9985

10086
struct MethodCollector<'a> {
@@ -103,7 +89,8 @@ struct MethodCollector<'a> {
10389
own_names: &'a HashSet<String>,
10490
seen: &'a mut HashSet<String>,
10591
visited: &'a mut HashSet<String>,
106-
results: &'a mut Vec<(MethodInfo, String)>,
92+
results: &'a mut Vec<(MethodInfo, String, bool)>,
93+
skip_override_attr: bool,
10794
}
10895

10996
impl MethodCollector<'_> {
@@ -184,7 +171,11 @@ impl MethodCollector<'_> {
184171
if self.own_names.contains(&lower) || !self.seen.insert(lower) {
185172
continue;
186173
}
187-
self.results.push(((**method).clone(), declaring.clone()));
174+
self.results.push((
175+
(**method).clone(),
176+
declaring.clone(),
177+
self.skip_override_attr,
178+
));
188179
}
189180
}
190181
}
@@ -204,14 +195,10 @@ pub(crate) struct OverrideCompletionOpts<'a> {
204195
/// When `php_version >= 8.3`, each item also inserts `#[\Override]` on the
205196
/// line above the declaration via `additional_text_edits`.
206197
pub(crate) fn build_override_completions(
207-
methods: &[(MethodInfo, String)],
198+
methods: &[(MethodInfo, String, bool)],
208199
opts: &OverrideCompletionOpts<'_>,
209200
) -> Vec<CompletionItem> {
210-
let add_override = opts.php_version >= METHOD_OVERRIDE_ATTR_MIN;
211-
// Insert the attribute at the start of the declaration line. The
212-
// indent is included here because this edit is at column 0 of the
213-
// line (absolute), not mid-line like the name snippet.
214-
let override_edit = if add_override {
201+
let override_edit = if opts.php_version >= METHOD_OVERRIDE_ATTR_MIN {
215202
Some(TextEdit {
216203
range: Range {
217204
start: opts.line_start,
@@ -224,7 +211,7 @@ pub(crate) fn build_override_completions(
224211
};
225212

226213
let mut items = Vec::new();
227-
for (method, declaring) in methods {
214+
for (method, declaring, skip_override_attr) in methods {
228215
let params = format_params(method, opts.use_map, opts.file_namespace);
229216
let return_type = format_return_type(method, opts.use_map, opts.file_namespace);
230217
let label = if return_type.is_empty() {
@@ -249,12 +236,17 @@ pub(crate) fn build_override_completions(
249236
);
250237

251238
let sort_prefix = if method.is_abstract { "0" } else { "1" };
239+
let detail_kind = if *skip_override_attr {
240+
"trait"
241+
} else {
242+
"override"
243+
};
252244
let sort_text = format!("{sort_prefix}_{}", method.name.to_ascii_lowercase());
253245

254246
items.push(CompletionItem {
255247
label: label.clone(),
256248
kind: Some(CompletionItemKind::METHOD),
257-
detail: Some(format!("override · {}", short_name(declaring))),
249+
detail: Some(format!("{detail_kind} · {}", short_name(declaring))),
258250
filter_text: Some(method.name.to_string()),
259251
sort_text: Some(sort_text),
260252
insert_text: Some(insert_text.clone()),
@@ -263,7 +255,11 @@ pub(crate) fn build_override_completions(
263255
range: opts.replace_range,
264256
new_text: insert_text,
265257
})),
266-
additional_text_edits: override_edit.clone().map(|e| vec![e]),
258+
additional_text_edits: if *skip_override_attr {
259+
None
260+
} else {
261+
override_edit.clone().map(|e| vec![e])
262+
},
267263
label_details: Some(CompletionItemLabelDetails {
268264
detail: None,
269265
description: Some(short_name(declaring).to_string()),
@@ -282,13 +278,12 @@ pub(crate) fn collect_overridable_properties(
282278
class: &ClassInfo,
283279
partial: &str,
284280
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
285-
) -> Vec<(PropertyInfo, String)> {
286-
let mut own: HashSet<String> = class
281+
) -> Vec<(PropertyInfo, String, bool)> {
282+
let own: HashSet<String> = class
287283
.properties
288284
.iter()
289285
.map(|p| p.name.to_lowercase())
290286
.collect();
291-
collect_trait_property_names(&class.used_traits, class_loader, &mut own, 0);
292287

293288
let mut results = Vec::new();
294289
let mut seen = HashSet::new();
@@ -317,7 +312,7 @@ pub(crate) fn collect_overridable_properties(
317312
if own.contains(&lower) || !seen.insert(lower) {
318313
continue;
319314
}
320-
results.push((prop.clone(), declaring.clone()));
315+
results.push((prop.clone(), declaring.clone(), false));
321316
}
322317
let mut collector = PropertyCollector {
323318
partial,
@@ -326,32 +321,25 @@ pub(crate) fn collect_overridable_properties(
326321
seen: &mut seen,
327322
visited: &mut visited,
328323
results: &mut results,
324+
skip_override_attr: false,
329325
};
330326
collector.collect_from_traits(&parent.used_traits, depth + 1);
331327
parent_name = parent.parent_class;
332328
depth += 1;
333329
}
334-
results
335-
}
336330

337-
fn collect_trait_property_names(
338-
traits: &[crate::atom::Atom],
339-
class_loader: &dyn Fn(&str) -> Option<Arc<ClassInfo>>,
340-
names: &mut HashSet<String>,
341-
depth: usize,
342-
) {
343-
if depth > crate::types::MAX_INHERITANCE_DEPTH as usize {
344-
return;
345-
}
346-
for tname in traits {
347-
let Some(tr) = class_loader(tname) else {
348-
continue;
349-
};
350-
for p in &tr.properties {
351-
names.insert(p.name.to_lowercase());
352-
}
353-
collect_trait_property_names(&tr.used_traits, class_loader, names, depth + 1);
354-
}
331+
let mut collector = PropertyCollector {
332+
partial,
333+
class_loader,
334+
own: &own,
335+
seen: &mut seen,
336+
visited: &mut visited,
337+
results: &mut results,
338+
skip_override_attr: true,
339+
};
340+
collector.collect_from_traits(&class.used_traits, 0);
341+
342+
results
355343
}
356344

357345
struct PropertyCollector<'a> {
@@ -360,7 +348,8 @@ struct PropertyCollector<'a> {
360348
own: &'a HashSet<String>,
361349
seen: &'a mut HashSet<String>,
362350
visited: &'a mut HashSet<String>,
363-
results: &'a mut Vec<(PropertyInfo, String)>,
351+
results: &'a mut Vec<(PropertyInfo, String, bool)>,
352+
skip_override_attr: bool,
364353
}
365354

366355
impl PropertyCollector<'_> {
@@ -394,7 +383,8 @@ impl PropertyCollector<'_> {
394383
if self.own.contains(&lower) || !self.seen.insert(lower) {
395384
continue;
396385
}
397-
self.results.push((prop.clone(), declaring.clone()));
386+
self.results
387+
.push((prop.clone(), declaring.clone(), self.skip_override_attr));
398388
}
399389
}
400390
}
@@ -451,7 +441,7 @@ pub(crate) fn collect_overridable_constants(
451441
/// Inserts `name = default` when the parent has an initializer so the
452442
/// user can override `protected $attributes = []` style members in one go.
453443
pub(crate) fn build_property_override_completions(
454-
props: &[(PropertyInfo, String)],
444+
props: &[(PropertyInfo, String, bool)],
455445
opts: &NameOverrideCompletionOpts<'_>,
456446
) -> Vec<CompletionItem> {
457447
let override_edit = if opts.php_version >= PROPERTY_OVERRIDE_ATTR_MIN {
@@ -466,7 +456,7 @@ pub(crate) fn build_property_override_completions(
466456
None
467457
};
468458
let mut items = Vec::new();
469-
for (prop, declaring) in props {
459+
for (prop, declaring, skip_override_attr) in props {
470460
let type_str = prop
471461
.native_type_hint
472462
.as_ref()
@@ -484,18 +474,27 @@ pub(crate) fn build_property_override_completions(
484474
(None, Some(d)) => format!("${} = {}", prop.name, d),
485475
(None, None) => format!("${}", prop.name),
486476
};
477+
let detail_kind = if *skip_override_attr {
478+
"trait"
479+
} else {
480+
"override"
481+
};
487482
items.push(CompletionItem {
488483
label,
489484
kind: Some(CompletionItemKind::PROPERTY),
490-
detail: Some(format!("override · {}", short_name(declaring))),
485+
detail: Some(format!("{detail_kind} · {}", short_name(declaring))),
491486
filter_text: Some(prop.name.to_string()),
492487
sort_text: Some(format!("0_{}", prop.name.to_ascii_lowercase())),
493488
insert_text: Some(insert.clone()),
494489
text_edit: Some(CompletionTextEdit::Edit(TextEdit {
495490
range: opts.replace_range,
496491
new_text: insert,
497492
})),
498-
additional_text_edits: override_edit.clone().map(|e| vec![e]),
493+
additional_text_edits: if *skip_override_attr {
494+
None
495+
} else {
496+
override_edit.clone().map(|e| vec![e])
497+
},
499498
label_details: Some(CompletionItemLabelDetails {
500499
detail: None,
501500
description: Some(short_name(declaring).to_string()),

src/completion/handler/member_access.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,10 @@ impl Backend {
322322
) {
323323
return None;
324324
}
325-
if class.parent_class.is_none() && class.interfaces.is_empty() {
325+
if class.parent_class.is_none()
326+
&& class.interfaces.is_empty()
327+
&& class.used_traits.is_empty()
328+
{
326329
return None;
327330
}
328331

0 commit comments

Comments
 (0)