Skip to content

Commit ee51b33

Browse files
committed
Fix Go-to-definition on trait with alia
1 parent c64d2d7 commit ee51b33

3 files changed

Lines changed: 645 additions & 0 deletions

File tree

docs/CHANGELOG.md

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

5959
### Fixed
6060

61+
- **Go-to-definition works on trait `as` alias and `insteadof` declarations.** Clicking a method name or alias name inside a trait use adaptation block (e.g. `routeNotificationFor as _routeNotificationFor`) now jumps to the original method definition in the trait. Trait names in `insteadof` clauses also navigate to the trait declaration. Previously these tokens had no symbol mapping and go-to-definition returned nothing.
6162
- **Parallel file scanner panics no longer crash the server.** If a thread panics during workspace indexing (e.g. due to a malformed PHP file), the panic is caught and logged instead of killing the entire LSP process. Previously a single thread failure during class, PSR-4, or full-symbol scanning would propagate the panic and silently terminate PHP intelligence in the editor.
6263
- **Type alias array shape diagnostics no longer fire on object values.** When a method returns a `@phpstan-type` alias that expands to an array shape containing object values (e.g. `array{pen: Pen}`), accessing a method on the object value no longer triggers a false diagnostic. Type aliases are now expanded before walking array shape segments in the resolution pipeline.
6364
- **Inline array-element function calls resolve correctly in diagnostics.** `end($obj->items)->method()` no longer produces a false "unknown member" diagnostic. Previously the argument text for property access expressions was lost during symbol map extraction, causing the resolver to fall back to the native `mixed|false` return type instead of extracting the element type from the array's generic annotation.

src/symbol_map/extraction.rs

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -663,6 +663,35 @@ fn extract_from_class_member<'a>(member: &'a ClassLikeMember<'a>, ctx: &mut Extr
663663
&raw,
664664
));
665665
}
666+
667+
// Extract symbols from trait use adaptations (`{ ... }` block)
668+
// so that go-to-definition works on method names and trait
669+
// references inside `as` alias and `insteadof` declarations.
670+
if let TraitUseSpecification::Concrete(spec) = &trait_use.specification {
671+
// Collect trait names from the `use` list so we can use the
672+
// first one as a fallback subject for unqualified method
673+
// references (e.g. `method as alias` without `Trait::method`).
674+
let first_trait_name: Option<String> = trait_use
675+
.trait_names
676+
.iter()
677+
.next()
678+
.map(|id| id.value().to_string());
679+
680+
for adaptation in spec.adaptations.iter() {
681+
match adaptation {
682+
TraitUseAdaptation::Alias(alias_adapt) => {
683+
extract_from_trait_alias_adaptation(
684+
alias_adapt,
685+
first_trait_name.as_deref(),
686+
ctx,
687+
);
688+
}
689+
TraitUseAdaptation::Precedence(prec) => {
690+
extract_from_trait_precedence_adaptation(prec, ctx);
691+
}
692+
}
693+
}
694+
}
666695
}
667696
ClassLikeMember::EnumCase(enum_case) => {
668697
// Enum case values (backed enums).
@@ -673,6 +702,123 @@ fn extract_from_class_member<'a>(member: &'a ClassLikeMember<'a>, ctx: &mut Extr
673702
}
674703
}
675704

705+
/// Extract symbol spans from a trait `as` alias adaptation.
706+
///
707+
/// For `TraitA::method as alias`:
708+
/// - `TraitA` gets a `ClassReference` span
709+
/// - `method` gets a `MemberAccess` span (subject = `TraitA`, static call)
710+
/// - `alias` gets a `MemberAccess` span (subject = `self`) so that
711+
/// `resolve_trait_alias` maps it back to the original method
712+
///
713+
/// For unqualified `method as alias`:
714+
/// - `method` gets a `MemberAccess` span using the first trait in the
715+
/// `use` list as the subject (or `self` as fallback)
716+
/// - `alias` gets a `MemberAccess` span (subject = `self`)
717+
fn extract_from_trait_alias_adaptation<'a>(
718+
alias_adapt: &'a TraitUseAliasAdaptation<'a>,
719+
first_trait_name: Option<&str>,
720+
ctx: &mut ExtractionCtx<'a>,
721+
) {
722+
match &alias_adapt.method_reference {
723+
TraitUseMethodReference::Absolute(abs) => {
724+
// Emit ClassReference for the trait name.
725+
let trait_raw = abs.trait_name.value().to_string();
726+
ctx.spans.push(class_ref_span(
727+
abs.trait_name.span().start.offset,
728+
abs.trait_name.span().end.offset,
729+
&trait_raw,
730+
));
731+
// Emit MemberAccess for the original method name.
732+
let method_name = abs.method_name.value.to_string();
733+
ctx.spans.push(SymbolSpan {
734+
start: abs.method_name.span.start.offset,
735+
end: abs.method_name.span.end.offset,
736+
kind: SymbolKind::MemberAccess {
737+
subject_text: trait_raw,
738+
member_name: method_name,
739+
is_static: true,
740+
is_method_call: true,
741+
},
742+
});
743+
}
744+
TraitUseMethodReference::Identifier(ident) => {
745+
// Unqualified reference: use the first trait name from the
746+
// `use` list, or fall back to `self`.
747+
let subject = first_trait_name.unwrap_or("self").to_string();
748+
let method_name = ident.value.to_string();
749+
ctx.spans.push(SymbolSpan {
750+
start: ident.span.start.offset,
751+
end: ident.span.end.offset,
752+
kind: SymbolKind::MemberAccess {
753+
subject_text: subject,
754+
member_name: method_name,
755+
is_static: true,
756+
is_method_call: true,
757+
},
758+
});
759+
}
760+
}
761+
762+
// Emit MemberAccess for the alias name (the `as` target).
763+
// Using `self` as the subject so that `resolve_trait_alias` on
764+
// the owning class maps the alias back to the original method.
765+
if let Some(ref alias_ident) = alias_adapt.alias {
766+
let alias_name = alias_ident.value.to_string();
767+
ctx.spans.push(SymbolSpan {
768+
start: alias_ident.span.start.offset,
769+
end: alias_ident.span.end.offset,
770+
kind: SymbolKind::MemberAccess {
771+
subject_text: "self".to_string(),
772+
member_name: alias_name,
773+
is_static: true,
774+
is_method_call: true,
775+
},
776+
});
777+
}
778+
}
779+
780+
/// Extract symbol spans from a trait `insteadof` precedence adaptation.
781+
///
782+
/// For `TraitA::method insteadof TraitB, TraitC`:
783+
/// - `TraitA` gets a `ClassReference` span
784+
/// - `method` gets a `MemberAccess` span (subject = `TraitA`, static call)
785+
/// - `TraitB` and `TraitC` each get a `ClassReference` span
786+
fn extract_from_trait_precedence_adaptation<'a>(
787+
prec: &'a TraitUsePrecedenceAdaptation<'a>,
788+
ctx: &mut ExtractionCtx<'a>,
789+
) {
790+
// Emit ClassReference for the trait name in the method reference.
791+
let trait_raw = prec.method_reference.trait_name.value().to_string();
792+
ctx.spans.push(class_ref_span(
793+
prec.method_reference.trait_name.span().start.offset,
794+
prec.method_reference.trait_name.span().end.offset,
795+
&trait_raw,
796+
));
797+
798+
// Emit MemberAccess for the method name.
799+
let method_name = prec.method_reference.method_name.value.to_string();
800+
ctx.spans.push(SymbolSpan {
801+
start: prec.method_reference.method_name.span.start.offset,
802+
end: prec.method_reference.method_name.span.end.offset,
803+
kind: SymbolKind::MemberAccess {
804+
subject_text: trait_raw,
805+
member_name: method_name,
806+
is_static: true,
807+
is_method_call: true,
808+
},
809+
});
810+
811+
// Emit ClassReference for each `insteadof` trait name.
812+
for ident in prec.trait_names.iter() {
813+
let raw = ident.value().to_string();
814+
ctx.spans.push(class_ref_span(
815+
ident.span().start.offset,
816+
ident.span().end.offset,
817+
&raw,
818+
));
819+
}
820+
}
821+
676822
fn extract_from_method<'a>(method: &'a Method<'a>, ctx: &mut ExtractionCtx<'a>) {
677823
// Method name — declaration site span for find-references and rename.
678824
let is_static = method.modifiers.iter().any(|m| m.is_static());

0 commit comments

Comments
 (0)