Skip to content

Commit d55d0b3

Browse files
committed
Trait insteadof / as Conflict Resolution
1 parent 93c5563 commit d55d0b3

7 files changed

Lines changed: 1416 additions & 22 deletions

File tree

example.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,6 +1127,35 @@ public function arrayMapFallback(): void
11271127
}
11281128

11291129

1130+
// ── Trait insteadof / as Conflict Resolution ────────────────────────────────
1131+
1132+
/**
1133+
* Demonstrates trait conflict resolution with `insteadof` and `as`.
1134+
*
1135+
* When multiple traits define the same method, PHP requires explicit
1136+
* `insteadof` to pick a winner and `as` to create aliases.
1137+
*/
1138+
class TraitConflictDemo
1139+
{
1140+
use JsonSerializer, XmlSerializer {
1141+
JsonSerializer::serialize insteadof XmlSerializer;
1142+
XmlSerializer::serialize as serializeXml;
1143+
JsonSerializer::serialize as private internalSerialize;
1144+
}
1145+
1146+
public function demo(): void
1147+
{
1148+
// Try: $this-> — offers serialize (from JsonSerializer), serializeXml (alias),
1149+
// internalSerialize (alias), toJson, toXml, and demo
1150+
$this->serialize(); // JsonSerializer wins via insteadof
1151+
$this->serializeXml(); // XmlSerializer::serialize aliased
1152+
$this->internalSerialize(); // JsonSerializer::serialize aliased as private
1153+
$this->toJson(); // non-conflicting method from JsonSerializer
1154+
$this->toXml(); // non-conflicting method from XmlSerializer
1155+
}
1156+
}
1157+
1158+
11301159
// ═══════════════════════════════════════════════════════════════════════════
11311160
// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
11321161
// ┃ SCAFFOLDING — Supporting definitions below this line. ┃
@@ -1153,6 +1182,16 @@ public function log(string $message): void;
11531182

11541183
// ─── Traits ─────────────────────────────────────────────────────────────────
11551184

1185+
trait JsonSerializer {
1186+
public function serialize(): string { return '{}'; }
1187+
public function toJson(): string { return $this->serialize(); }
1188+
}
1189+
1190+
trait XmlSerializer {
1191+
public function serialize(): string { return '<xml/>'; }
1192+
public function toXml(): string { return $this->serialize(); }
1193+
}
1194+
11561195
trait HasTimestamps
11571196
{
11581197
protected ?string $createdAt = null;

src/definition/member.rs

Lines changed: 88 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -123,13 +123,47 @@ impl Backend {
123123
// 4. Try each candidate class and pick the first one where the
124124
// member actually exists (directly or via inheritance).
125125
for target_class in &candidates {
126+
// Check if the member name is a trait `as` alias on this class.
127+
// If so, resolve to the original method name and (optionally) the
128+
// source trait so we jump to the actual method definition rather
129+
// than failing to find an alias that only exists after inheritance
130+
// resolution.
131+
let (effective_name, alias_trait) =
132+
Self::resolve_trait_alias(target_class, member_name);
133+
134+
// If we know the exact source trait from the alias, go directly
135+
// to that trait's method definition.
136+
if let Some(ref trait_name) = alias_trait {
137+
if let Some(trait_info) = class_loader(trait_name) {
138+
if Self::classify_member(&trait_info, &effective_name, access_hint).is_some() {
139+
if let Some((class_uri, class_content)) =
140+
self.find_class_file_content(&trait_info.name, uri, content)
141+
&& let Some(member_position) = Self::find_member_position(
142+
&class_content,
143+
&effective_name,
144+
MemberKind::Method,
145+
)
146+
&& let Ok(parsed_uri) = Url::parse(&class_uri)
147+
{
148+
return Some(Location {
149+
uri: parsed_uri,
150+
range: Range {
151+
start: member_position,
152+
end: member_position,
153+
},
154+
});
155+
}
156+
}
157+
}
158+
}
159+
126160
let declaring_class =
127-
Self::find_declaring_class(target_class, member_name, &class_loader)
161+
Self::find_declaring_class(target_class, &effective_name, &class_loader)
128162
.unwrap_or_else(|| target_class.clone());
129163

130164
// Check that the member is actually present on the declaring class.
131165
let member_kind =
132-
match Self::classify_member(&declaring_class, member_name, access_hint) {
166+
match Self::classify_member(&declaring_class, &effective_name, access_hint) {
133167
Some(k) => k,
134168
None => continue, // member not on this candidate, try next
135169
};
@@ -138,7 +172,7 @@ impl Backend {
138172
if let Some((class_uri, class_content)) =
139173
self.find_class_file_content(&declaring_class.name, uri, content)
140174
&& let Some(member_position) =
141-
Self::find_member_position(&class_content, member_name, member_kind)
175+
Self::find_member_position(&class_content, &effective_name, member_kind)
142176
&& let Ok(parsed_uri) = Url::parse(&class_uri)
143177
{
144178
return Some(Location {
@@ -155,15 +189,45 @@ impl Backend {
155189
// and try the original (non-iterating) logic so we at least get
156190
// partial results when possible.
157191
let target_class = &candidates[0];
158-
let declaring_class = Self::find_declaring_class(target_class, member_name, &class_loader)
159-
.unwrap_or_else(|| target_class.clone());
160192

161-
let member_kind = Self::classify_member(&declaring_class, member_name, access_hint)?;
193+
let (effective_name, alias_trait) =
194+
Self::resolve_trait_alias(target_class, member_name);
195+
196+
// Direct trait lookup for aliased members in the fallback path.
197+
if let Some(ref trait_name) = alias_trait {
198+
if let Some(trait_info) = class_loader(trait_name) {
199+
if let Some((class_uri, class_content)) =
200+
self.find_class_file_content(&trait_info.name, uri, content)
201+
&& let Some(member_position) = Self::find_member_position(
202+
&class_content,
203+
&effective_name,
204+
MemberKind::Method,
205+
)
206+
&& let Ok(parsed_uri) = Url::parse(&class_uri)
207+
{
208+
return Some(Location {
209+
uri: parsed_uri,
210+
range: Range {
211+
start: member_position,
212+
end: member_position,
213+
},
214+
});
215+
}
216+
}
217+
}
218+
219+
let declaring_class =
220+
Self::find_declaring_class(target_class, &effective_name, &class_loader)
221+
.unwrap_or_else(|| target_class.clone());
222+
223+
let member_kind =
224+
Self::classify_member(&declaring_class, &effective_name, access_hint)?;
162225

163226
let (class_uri, class_content) =
164227
self.find_class_file_content(&declaring_class.name, uri, content)?;
165228

166-
let member_position = Self::find_member_position(&class_content, member_name, member_kind)?;
229+
let member_position =
230+
Self::find_member_position(&class_content, &effective_name, member_kind)?;
167231

168232
let parsed_uri = Url::parse(&class_uri).ok()?;
169233
Some(Location {
@@ -380,6 +444,23 @@ impl Backend {
380444
///
381445
/// Returns `Some(ClassInfo)` of the declaring class, or `None` if the
382446
/// member cannot be found in any ancestor.
447+
/// Resolve a trait `as` alias on a class.
448+
///
449+
/// If `member_name` matches a trait alias declared on the class, returns
450+
/// the original method name and (optionally) the source trait name.
451+
/// Otherwise returns `member_name` unchanged with no trait hint.
452+
fn resolve_trait_alias(class: &ClassInfo, member_name: &str) -> (String, Option<String>) {
453+
for alias in &class.trait_aliases {
454+
if alias.alias.as_deref() == Some(member_name) {
455+
return (
456+
alias.method_name.clone(),
457+
alias.trait_name.clone(),
458+
);
459+
}
460+
}
461+
(member_name.to_string(), None)
462+
}
463+
383464
fn find_declaring_class(
384465
class: &ClassInfo,
385466
member_name: &str,

src/inheritance.rs

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use std::collections::HashMap;
1515

1616
use crate::Backend;
1717
use crate::types::Visibility;
18-
use crate::types::{ClassInfo, MethodInfo, PropertyInfo};
18+
use crate::types::{ClassInfo, MethodInfo, PropertyInfo, TraitAlias, TraitPrecedence};
1919

2020
impl Backend {
2121
/// Resolve a class together with all inherited members from its parent
@@ -48,6 +48,8 @@ impl Backend {
4848
&mut merged,
4949
&class.used_traits,
5050
&class.use_generics,
51+
&class.trait_precedences,
52+
&class.trait_aliases,
5153
class_loader,
5254
0,
5355
);
@@ -96,6 +98,8 @@ impl Backend {
9698
&mut merged,
9799
&parent.used_traits,
98100
&parent.use_generics,
101+
&parent.trait_precedences,
102+
&parent.trait_aliases,
99103
class_loader,
100104
0,
101105
);
@@ -303,6 +307,8 @@ impl Backend {
303307
merged: &mut ClassInfo,
304308
trait_names: &[String],
305309
use_generics: &[(String, Vec<String>)],
310+
trait_precedences: &[TraitPrecedence],
311+
trait_aliases: &[TraitAlias],
306312
class_loader: &dyn Fn(&str) -> Option<ClassInfo>,
307313
depth: u32,
308314
) {
@@ -331,6 +337,8 @@ impl Backend {
331337
merged,
332338
&trait_info.used_traits,
333339
&trait_info.use_generics,
340+
&trait_info.trait_precedences,
341+
&trait_info.trait_aliases,
334342
class_loader,
335343
depth + 1,
336344
);
@@ -360,6 +368,8 @@ impl Backend {
360368
merged,
361369
&parent.used_traits,
362370
&parent.use_generics,
371+
&parent.trait_precedences,
372+
&parent.trait_aliases,
363373
class_loader,
364374
parent_depth + 1,
365375
);
@@ -403,11 +413,46 @@ impl Backend {
403413

404414
// Merge trait methods — skip if already present.
405415
// Apply generic substitution if a `@use` mapping exists.
416+
// Also skip methods excluded by `insteadof` declarations.
406417
for method in &trait_info.methods {
418+
// Check if this method from this trait is excluded by an
419+
// `insteadof` declaration. For example, if the class has
420+
// `TraitA::method insteadof TraitB`, then when merging
421+
// TraitB's methods, `method` should be skipped.
422+
let excluded = trait_precedences.iter().any(|p| {
423+
p.method_name == method.name
424+
&& p.insteadof.iter().any(|excluded_trait| {
425+
excluded_trait == trait_name
426+
|| short_name(excluded_trait) == short_name(trait_name)
427+
})
428+
});
429+
if excluded {
430+
continue;
431+
}
432+
407433
if merged.methods.iter().any(|m| m.name == method.name) {
408434
continue;
409435
}
410436
let mut method = method.clone();
437+
438+
// Apply visibility-only `as` changes (no alias name).
439+
// For example, `TraitA::method as protected` changes the
440+
// visibility of `method` without creating an alias.
441+
for alias in trait_aliases {
442+
if alias.method_name == method.name
443+
&& alias.alias.is_none()
444+
&& let Some(vis) = alias.visibility
445+
{
446+
// Check trait name matches (if specified)
447+
let name_matches = alias.trait_name.as_ref().is_none_or(|t| {
448+
t == trait_name || short_name(t) == short_name(trait_name)
449+
});
450+
if name_matches {
451+
method.visibility = vis;
452+
}
453+
}
454+
}
455+
411456
if !trait_subs.is_empty() {
412457
apply_substitution_to_method(&mut method, &trait_subs);
413458
}
@@ -433,6 +478,51 @@ impl Backend {
433478
}
434479
merged.constants.push(constant.clone());
435480
}
481+
482+
// Apply `as` alias declarations that create new method names.
483+
// For example, `TraitB::method as traitBMethod` creates a copy
484+
// of `method` accessible as `traitBMethod`.
485+
for alias in trait_aliases {
486+
// Only process aliases that have a new name.
487+
let alias_name = match &alias.alias {
488+
Some(name) => name,
489+
None => continue,
490+
};
491+
492+
// Check trait name matches (if specified).
493+
let name_matches = alias
494+
.trait_name
495+
.as_ref()
496+
.is_none_or(|t| t == trait_name || short_name(t) == short_name(trait_name));
497+
if !name_matches {
498+
continue;
499+
}
500+
501+
// Find the source method in this trait.
502+
let source_method = trait_info
503+
.methods
504+
.iter()
505+
.find(|m| m.name == alias.method_name);
506+
let source_method = match source_method {
507+
Some(m) => m,
508+
None => continue,
509+
};
510+
511+
// Skip if an alias with this name already exists.
512+
if merged.methods.iter().any(|m| m.name == *alias_name) {
513+
continue;
514+
}
515+
516+
let mut aliased = source_method.clone();
517+
aliased.name = alias_name.clone();
518+
if let Some(vis) = alias.visibility {
519+
aliased.visibility = vis;
520+
}
521+
if !trait_subs.is_empty() {
522+
apply_substitution_to_method(&mut aliased, &trait_subs);
523+
}
524+
merged.methods.push(aliased);
525+
}
436526
}
437527
}
438528
}

src/parser/ast_update.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,23 @@ impl Backend {
238238
.map(|t| Self::resolve_name(t, use_map, namespace))
239239
.collect();
240240

241+
// Resolve trait names in `insteadof` precedence adaptations
242+
for prec in &mut class.trait_precedences {
243+
prec.trait_name = Self::resolve_name(&prec.trait_name, use_map, namespace);
244+
prec.insteadof = prec
245+
.insteadof
246+
.iter()
247+
.map(|t| Self::resolve_name(t, use_map, namespace))
248+
.collect();
249+
}
250+
251+
// Resolve trait names in `as` alias adaptations
252+
for alias in &mut class.trait_aliases {
253+
if let Some(ref t) = alias.trait_name {
254+
alias.trait_name = Some(Self::resolve_name(t, use_map, namespace));
255+
}
256+
}
257+
241258
// Resolve mixin names to fully-qualified names
242259
class.mixins = class
243260
.mixins

0 commit comments

Comments
 (0)