Try to offer member completions after `->`, `?->`, or `::`. Resolves the subject to one or more `ClassInfo` values, merges inherited members, and builds completion items filtered by access kind and visibility. Returns `None` when there is no access operator before the cursor or when resolution produces no results.
(
&self,
uri: &str,
content: &str,
position: Position,
ctx: &FileContext,
)
| 893 | /// Returns `None` when there is no access operator before the cursor |
| 894 | /// or when resolution produces no results. |
| 895 | fn try_member_access_completion( |
| 896 | &self, |
| 897 | uri: &str, |
| 898 | content: &str, |
| 899 | position: Position, |
| 900 | ctx: &FileContext, |
| 901 | ) -> Option<CompletionResponse> { |
| 902 | // ── Primary path: AST-based detection via symbol map ──────── |
| 903 | // The symbol map's `MemberAccess` correctly handles `(new Foo)->`, |
| 904 | // call-result chains, array access chains, and null-safe chains. |
| 905 | // Fall back to text scanning when the symbol map has no hit. |
| 906 | let target = self |
| 907 | .extract_completion_target_from_symbol_map(uri, content, position) |
| 908 | .or_else(|| super::target::extract_completion_target(content, position))?; |
| 909 | |
| 910 | let cursor_offset = position_to_offset(content, position); |
| 911 | let current_class = find_class_at_offset(&ctx.classes, cursor_offset); |
| 912 | |
| 913 | let class_loader = self.class_loader(ctx); |
| 914 | let function_loader = self.function_loader(ctx); |
| 915 | |
| 916 | // `static::` in a final class is equivalent to `self::` but |
| 917 | // suggests the class can be subclassed — which it can't. |
| 918 | // Suppress suggestions to nudge the developer toward `self::`. |
| 919 | let suppress = target.subject == "static" && current_class.is_some_and(|cc| cc.is_final); |
| 920 | |
| 921 | // Wrap resolution + inheritance merging in catch_unwind so |
| 922 | // that a stack overflow (e.g. from deep trait/inheritance |
| 923 | // resolution when the subject is a call expression like |
| 924 | // `collect($x)->`) doesn't crash the LSP server process. |
| 925 | // The variable-resolution path already has its own |
| 926 | // catch_unwind, but the direct call-expression path |
| 927 | // (resolve_call_return_types_expr → type_hint_to_classes_typed → |
| 928 | // class_loader → find_or_load_class → parse_php → |
| 929 | // resolve_class_with_inheritance) does not. |
| 930 | let member_items = crate::util::catch_panic_unwind_safe( |
| 931 | "member-access completion", |
| 932 | uri, |
| 933 | Some(position), |
| 934 | || { |
| 935 | let candidates = if suppress { |
| 936 | vec![] |
| 937 | } else { |
| 938 | let rctx = ResolutionCtx { |
| 939 | current_class, |
| 940 | all_classes: &ctx.classes, |
| 941 | content, |
| 942 | cursor_offset, |
| 943 | class_loader: &class_loader, |
| 944 | resolved_class_cache: Some(&self.resolved_class_cache), |
| 945 | function_loader: Some(&function_loader), |
| 946 | scope_var_resolver: None, |
| 947 | is_in_static_method: false, |
| 948 | }; |
| 949 | let mut resolved = super::resolver::resolve_target_classes( |
| 950 | &target.subject, |
| 951 | target.access_kind, |
| 952 | &rctx, |
no test coverage detected