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.
49pub(crate) fn class_names(ty: &Type) -> Vec<String> {
50    ty.types
51        .iter()
52        .filter_map(atomic_class_fqcn)
53        .map(str::to_owned)
54        .collect()
55}
56
57/// The single receiver class FQCN for member resolution — the first named
58/// object in `ty`. `None` if `ty` names no class.
59pub(crate) fn primary_class_name(ty: &Type) -> Option<String> {
60    ty.types
61        .iter()
62        .find_map(atomic_class_fqcn)
63        .map(str::to_owned)
64}