@@ -32,6 +32,19 @@ enum MemberKind {
3232 Constant ,
3333}
3434
35+ /// Hint about whether the member access looks like a method call or a property
36+ /// access. Used to disambiguate when a class has both a method and a property
37+ /// with the same name (e.g. `id()` method vs `$id` property).
38+ #[ derive( Debug , Clone , Copy , PartialEq , Eq ) ]
39+ enum MemberAccessHint {
40+ /// Followed by `(` — looks like a method call.
41+ MethodCall ,
42+ /// No `(` after the name — looks like a property / constant access.
43+ PropertyAccess ,
44+ /// Cannot determine (fallback to original order).
45+ Unknown ,
46+ }
47+
3548impl Backend {
3649 /// Handle a "go to definition" request.
3750 ///
@@ -337,6 +350,9 @@ impl Backend {
337350 return None ;
338351 }
339352
353+ // Determine whether this looks like a method call or property access.
354+ let access_hint = Self :: detect_member_access_hint ( content, position, member_name) ;
355+
340356 // 4. Try each candidate class and pick the first one where the
341357 // member actually exists (directly or via inheritance).
342358 for target_class in & candidates {
@@ -345,10 +361,11 @@ impl Backend {
345361 . unwrap_or_else ( || target_class. clone ( ) ) ;
346362
347363 // Check that the member is actually present on the declaring class.
348- let member_kind = match Self :: classify_member ( & declaring_class, member_name) {
349- Some ( k) => k,
350- None => continue , // member not on this candidate, try next
351- } ;
364+ let member_kind =
365+ match Self :: classify_member ( & declaring_class, member_name, access_hint) {
366+ Some ( k) => k,
367+ None => continue , // member not on this candidate, try next
368+ } ;
352369
353370 // Locate the file that contains the declaring class.
354371 if let Some ( ( class_uri, class_content) ) =
@@ -374,7 +391,7 @@ impl Backend {
374391 let declaring_class = Self :: find_declaring_class ( target_class, member_name, & class_loader)
375392 . unwrap_or_else ( || target_class. clone ( ) ) ;
376393
377- let member_kind = Self :: classify_member ( & declaring_class, member_name) ?;
394+ let member_kind = Self :: classify_member ( & declaring_class, member_name, access_hint ) ?;
378395
379396 let ( class_uri, class_content) =
380397 self . find_class_file_content ( & declaring_class. name , uri, content) ?;
@@ -653,17 +670,103 @@ impl Backend {
653670 /// checking the class's parsed information.
654671 ///
655672 /// Returns `None` if the member is not found in the class.
656- fn classify_member ( class : & ClassInfo , member_name : & str ) -> Option < MemberKind > {
657- if class. methods . iter ( ) . any ( |m| m. name == member_name) {
658- return Some ( MemberKind :: Method ) ;
673+ fn classify_member (
674+ class : & ClassInfo ,
675+ member_name : & str ,
676+ hint : MemberAccessHint ,
677+ ) -> Option < MemberKind > {
678+ let has_method = class. methods . iter ( ) . any ( |m| m. name == member_name) ;
679+ let has_property = class. properties . iter ( ) . any ( |p| p. name == member_name) ;
680+ let has_constant = class. constants . iter ( ) . any ( |c| c. name == member_name) ;
681+
682+ match hint {
683+ MemberAccessHint :: PropertyAccess => {
684+ // Prefer property/constant over method when there's no `()`.
685+ if has_property {
686+ return Some ( MemberKind :: Property ) ;
687+ }
688+ if has_constant {
689+ return Some ( MemberKind :: Constant ) ;
690+ }
691+ if has_method {
692+ return Some ( MemberKind :: Method ) ;
693+ }
694+ }
695+ MemberAccessHint :: MethodCall => {
696+ // Prefer method when followed by `()`.
697+ if has_method {
698+ return Some ( MemberKind :: Method ) ;
699+ }
700+ if has_property {
701+ return Some ( MemberKind :: Property ) ;
702+ }
703+ if has_constant {
704+ return Some ( MemberKind :: Constant ) ;
705+ }
706+ }
707+ MemberAccessHint :: Unknown => {
708+ // Default order: method, property, constant.
709+ if has_method {
710+ return Some ( MemberKind :: Method ) ;
711+ }
712+ if has_property {
713+ return Some ( MemberKind :: Property ) ;
714+ }
715+ if has_constant {
716+ return Some ( MemberKind :: Constant ) ;
717+ }
718+ }
719+ }
720+ None
721+ }
722+
723+ /// Determine whether the member name at the given position is followed by
724+ /// `(` (indicating a method call) or not (indicating property / constant
725+ /// access).
726+ fn detect_member_access_hint (
727+ content : & str ,
728+ position : Position ,
729+ member_name : & str ,
730+ ) -> MemberAccessHint {
731+ let lines: Vec < & str > = content. lines ( ) . collect ( ) ;
732+ let line = match lines. get ( position. line as usize ) {
733+ Some ( l) => * l,
734+ None => return MemberAccessHint :: Unknown ,
735+ } ;
736+ let chars: Vec < char > = line. chars ( ) . collect ( ) ;
737+ let col = ( position. character as usize ) . min ( chars. len ( ) ) ;
738+
739+ // Find the end of the member name by walking right from the cursor.
740+ let is_word_char = |c : char | c. is_alphanumeric ( ) || c == '_' ;
741+
742+ let mut end = col;
743+ // If cursor is on a word char, walk right to end of word.
744+ if end < chars. len ( ) && is_word_char ( chars[ end] ) {
745+ while end < chars. len ( ) && is_word_char ( chars[ end] ) {
746+ end += 1 ;
747+ }
748+ } else if end > 0 && is_word_char ( chars[ end - 1 ] ) {
749+ // Cursor is just past the word; `end` is already correct.
750+ } else {
751+ // Try to find the member name by searching forward from col.
752+ if let Some ( idx) = line[ col..] . find ( member_name) {
753+ end = col + idx + member_name. len ( ) ;
754+ } else {
755+ return MemberAccessHint :: Unknown ;
756+ }
659757 }
660- if class. properties . iter ( ) . any ( |p| p. name == member_name) {
661- return Some ( MemberKind :: Property ) ;
758+
759+ // Skip whitespace after the word.
760+ let mut i = end;
761+ while i < chars. len ( ) && chars[ i] . is_whitespace ( ) {
762+ i += 1 ;
662763 }
663- if class. constants . iter ( ) . any ( |c| c. name == member_name) {
664- return Some ( MemberKind :: Constant ) ;
764+
765+ if i < chars. len ( ) && chars[ i] == '(' {
766+ MemberAccessHint :: MethodCall
767+ } else {
768+ MemberAccessHint :: PropertyAccess
665769 }
666- None
667770 }
668771
669772 /// Walk up the inheritance chain to find the class that actually declares
@@ -679,7 +782,7 @@ impl Backend {
679782 const MAX_DEPTH : usize = 20 ;
680783
681784 // Check if this class directly declares the member.
682- if Self :: classify_member ( class, member_name) . is_some ( ) {
785+ if Self :: classify_member ( class, member_name, MemberAccessHint :: Unknown ) . is_some ( ) {
683786 return Some ( class. clone ( ) ) ;
684787 }
685788
@@ -695,7 +798,7 @@ impl Backend {
695798 for _ in 0 ..MAX_DEPTH {
696799 let parent_name = current. parent_class . as_ref ( ) ?;
697800 let parent = class_loader ( parent_name) ?;
698- if Self :: classify_member ( & parent, member_name) . is_some ( ) {
801+ if Self :: classify_member ( & parent, member_name, MemberAccessHint :: Unknown ) . is_some ( ) {
699802 return Some ( parent) ;
700803 }
701804 // Check traits used by the parent class.
@@ -731,7 +834,8 @@ impl Backend {
731834 } else {
732835 continue ;
733836 } ;
734- if Self :: classify_member ( & trait_info, member_name) . is_some ( ) {
837+ if Self :: classify_member ( & trait_info, member_name, MemberAccessHint :: Unknown ) . is_some ( )
838+ {
735839 return Some ( trait_info) ;
736840 }
737841 // Recurse into traits used by this trait.
0 commit comments