Skip to main content

php_lsp/
type_query.rs

1//! Position → resolved-type queries backed by mir's body analysis.
2//!
3//! mir's `FileAnalyzer::analyze` already resolves a [`mir_analyzer::Type`] for
4//! every expression it visits and records it as a [`mir_analyzer::ResolvedSymbol`]
5//! (see `DocumentStore::cached_analysis`, which retains the result across LSP
6//! requests). This module is the thin glue that maps an LSP cursor onto those
7//! recorded symbols — replacing the hand-rolled, short-name-only tracker in
8//! `type_map` for the variable/expression-type cases.
9//!
10//! ## Contract callers must respect
11//!
12//! mir symbol spans are **end-exclusive and identifier-only**: the variable
13//! `$q` at bytes `76..78` is found by `symbol_at(76)` or `symbol_at(77)` but
14//! **not** `symbol_at(78)`. Callers must pass a byte offset that lands strictly
15//! inside the token of interest — for a variable, `word_range_at(..).start`
16//! (the `$`) is always inside. The primitive is intentionally dumb: it does no
17//! offset fudging, because only the caller has the AST context to pick a
18//! correct in-token offset without grabbing an adjacent token.
19
20use mir_analyzer::{FileAnalysis, Type};
21use mir_types::Atomic;
22
23/// The resolved mir type recorded at `offset`, or `None` if no recorded symbol
24/// covers it. `offset` is a byte offset that must land strictly inside the
25/// token of interest (see the module-level contract).
26pub(crate) fn type_at_offset(analysis: &FileAnalysis, offset: u32) -> Option<&Type> {
27    analysis.symbol_at(offset).map(|s| &s.resolved_type)
28}
29
30/// The class/enum FQCN named by a single atomic, if any. Covers object types
31/// (`TNamedObject`, `self`/`static`/`parent`) and enum-case literals
32/// (`Suit::Hearts` → `Suit`), which is what member/declaration lookups want.
33fn atomic_class_fqcn(atomic: &Atomic) -> Option<&str> {
34    atomic.named_object_fqcn().or(match atomic {
35        Atomic::TLiteralEnumCase { enum_fqcn, .. } => Some(enum_fqcn.as_ref()),
36        _ => None,
37    })
38}
39
40/// The fully-qualified class names named by `ty` — one per named-object atomic,
41/// so a union `A|B` yields `["A", "B"]`. Generic parameters are stripped
42/// (`Collection<User>` → `Collection`), since callers searching for a class
43/// declaration match on the bare FQCN. Scalar/array/callable types yield an
44/// empty vec.
45///
46/// Names are returned exactly as mir produces them: fully qualified with no
47/// leading `\` (e.g. `App\Svc\User`), already resolved through the file's
48/// namespace and `use` imports.
49///
50/// **TParent caveat**: `Atomic::TParent { fqcn }` carries the *containing*
51/// class's FQCN (e.g. `"ChildClass"`), not the actual parent. This function
52/// returns that fqcn as-is, which is correct for `self`/`static` navigation
53/// but wrong for `parent`. type_definition.rs works around this by bypassing
54/// mir for `parent`-typed parameters. The proper fix is a mir API
55/// `AnalysisSession::resolve_parent_fqcn(fqcn) → Option<String>` that looks
56/// up the extends chain; once available, callers can substitute it here.
57pub(crate) fn class_names(ty: &Type) -> Vec<String> {
58    ty.types
59        .iter()
60        .filter_map(atomic_class_fqcn)
61        .map(str::to_owned)
62        .collect()
63}
64
65/// The single receiver class FQCN for member resolution — the first named
66/// object in `ty`. `None` if `ty` names no class.
67pub(crate) fn primary_class_name(ty: &Type) -> Option<String> {
68    ty.types
69        .iter()
70        .find_map(atomic_class_fqcn)
71        .map(str::to_owned)
72}