devscout_rs/query/member.rs
1// Member-seed resolution: `Member`, `Type.Member` and `Namespace.Type.Member`
2// spellings, reached only once `resolve_symbol`'s type ladder has already
3// missed (see `symbol.rs`) -- a name that resolves to a type never reaches
4// this module. A member is never a `graph::Def`: the extractor records one as
5// a `graph::GraphName` (method/property/field/event/enum-member) carrying an
6// `owner` def id, so resolving one means scanning `graph.names`, not
7// `by_simple_name`.
8
9use std::collections::HashMap;
10
11use crate::suggest::MEMBER_KINDS;
12
13use super::index::{DefSite, GraphIndex};
14
15/// Splits a member seed into its trailing member name and, when the seed
16/// carries a `.`, the qualifier before it (`Type` for `Type.Member`,
17/// `Namespace.Type` for `Namespace.Type.Member`). A seed with no `.` is a
18/// bare member name and carries no qualifier.
19pub(crate) fn split_member_seed(seed: &str) -> (&str, Option<&str>) {
20 match seed.rfind('.') {
21 Some(pos) => (&seed[pos + 1..], Some(&seed[..pos])),
22 None => (seed, None),
23 }
24}
25
26// Whether `owner_id` (a def id) is named by `qualifier`: an exact match, or a
27// dotted-suffix match -- the same tail rule `resolve_symbol`'s own dotted step
28// applies to a type id (see `symbol.rs`), so `Type.Member` and
29// `Namespace.Type.Member` both reach a def however deeply its own namespace
30// nests, without this module restating what "named by a dotted qualifier"
31// means.
32fn owner_matches_qualifier(owner_id: &str, qualifier: &str) -> bool {
33 owner_id == qualifier || owner_id.ends_with(&format!(".{qualifier}"))
34}
35
36/// Every type declaring a member named `name`, in name-index order, each with
37/// every site it declares it at.
38///
39/// The same shape `refs.rs`'s own `member_owners` used to build by hand, now
40/// shared so `impact`/`tests` can resolve a member seed too. `qualifier`,
41/// when present, narrows the result to owners [`owner_matches_qualifier`]
42/// admits; `None` (a bare seed) admits every owner, unchanged from the
43/// bare-member behavior this replaces. Restricted to [`MEMBER_KINDS`] and an
44/// owner already in the index -- a markup or resource name carries no owner
45/// and never reaches here.
46pub fn qualified_member_owners(
47 index: &GraphIndex,
48 name: &str,
49 qualifier: Option<&str>,
50) -> Vec<(String, Vec<DefSite>)> {
51 let mut out: Vec<(String, Vec<DefSite>)> = Vec::new();
52 let mut at: HashMap<&str, usize> = HashMap::new();
53 for n in &index.graph.names {
54 if n.name != name
55 || n.owner.is_empty()
56 || !index.by_id.contains_key(&n.owner)
57 || !MEMBER_KINDS.contains(&n.kind.as_str())
58 {
59 continue;
60 }
61 if let Some(q) = qualifier {
62 if !owner_matches_qualifier(&n.owner, q) {
63 continue;
64 }
65 }
66 let site = DefSite {
67 file: n.file.clone(),
68 line: n.line,
69 };
70 match at.get(n.owner.as_str()) {
71 Some(&i) => out[i].1.push(site),
72 None => {
73 at.insert(n.owner.as_str(), out.len());
74 out.push((n.owner.clone(), vec![site]));
75 }
76 }
77 }
78 out
79}
80
81/// One row of an ambiguous member seed: the declaring type, the member name,
82/// and the FIRST site that type declares it at.
83///
84/// Never a bare type list (a candidate carries the member's own name and
85/// declaration site, not just the type's) -- what a member-ambiguous answer
86/// renders instead of reusing an ambiguous type's own `{id, def site, kind}`
87/// rows.
88#[derive(Debug, Clone, PartialEq)]
89pub struct MemberCandidate {
90 /// The def id of the declaring type.
91 pub owner: String,
92 /// The member's own name.
93 pub name: String,
94 /// The file the member is declared in.
95 pub file: String,
96 /// The line the member is declared at.
97 pub line: usize,
98}
99
100/// Whether resolving `seed` as a member's declaring type, ignoring inbound
101/// references entirely (unlike `refs.rs`'s own edge-verified fallback).
102///
103/// `impact`/`tests` answer AS the member's unique declaring type, so all they
104/// need is that the member exists at all, never that something already
105/// references it.
106#[derive(Debug, Clone, PartialEq)]
107pub enum MemberSeedResolution {
108 /// Exactly one type declares the member; its def id.
109 Resolved(String),
110 /// More than one type declares the member.
111 Ambiguous(Vec<MemberCandidate>),
112 /// No type in the graph declares a member by this name (and qualifier,
113 /// when the seed carried one).
114 NotFound,
115}
116
117/// Resolve `seed` (`Member`, `Type.Member`, or `Namespace.Type.Member`) to
118/// the def id of the type that uniquely declares it.
119///
120/// Used by `impact`/`tests`, which answer as the member's declaring type
121/// rather than building a member-shaped model of their own (that shape is
122/// `refs.rs`'s, reused wholesale by `read.rs`).
123pub fn resolve_member_seed(index: &GraphIndex, seed: &str) -> MemberSeedResolution {
124 let (name, qualifier) = split_member_seed(seed);
125 let owners = qualified_member_owners(index, name, qualifier);
126 match owners.len() {
127 0 => MemberSeedResolution::NotFound,
128 1 => MemberSeedResolution::Resolved(owners[0].0.clone()),
129 _ => MemberSeedResolution::Ambiguous(member_candidates(&owners, name)),
130 }
131}
132
133/// One [`MemberCandidate`] row per owner in `owners`, first site only.
134pub fn member_candidates(owners: &[(String, Vec<DefSite>)], name: &str) -> Vec<MemberCandidate> {
135 owners
136 .iter()
137 .map(|(owner, sites)| MemberCandidate {
138 owner: owner.clone(),
139 name: name.to_string(),
140 file: sites[0].file.clone(),
141 line: sites[0].line,
142 })
143 .collect()
144}
145
146/// The synthetic, fully-qualified seed `--pick` re-resolves against.
147///
148/// Names the exact owner a candidate row named, so the ladder that answered
149/// the first time answers again, narrowed to one candidate -- no extra
150/// resolution path only `--pick` takes.
151pub fn qualified_seed(candidate: &MemberCandidate) -> String {
152 format!("{}.{}", candidate.owner, candidate.name)
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn split_member_seed_separates_the_trailing_name_from_a_dotted_qualifier() {
161 assert_eq!(split_member_seed("Stow"), ("Stow", None));
162 assert_eq!(split_member_seed("Bin.Stow"), ("Stow", Some("Bin")));
163 assert_eq!(
164 split_member_seed("Acme.Storage.Bin.Stow"),
165 ("Stow", Some("Acme.Storage.Bin"))
166 );
167 }
168
169 #[test]
170 fn owner_matches_qualifier_accepts_an_exact_id_or_a_dotted_suffix() {
171 assert!(owner_matches_qualifier("Acme.Storage.Bin", "Bin"));
172 assert!(owner_matches_qualifier("Acme.Storage.Bin", "Storage.Bin"));
173 assert!(owner_matches_qualifier(
174 "Acme.Storage.Bin",
175 "Acme.Storage.Bin"
176 ));
177 assert!(!owner_matches_qualifier("Acme.Storage.Bin", "Vault"));
178 assert!(
179 !owner_matches_qualifier("Acme.Storage.Bin", "torage.Bin"),
180 "a suffix match starts on a dot boundary, not mid-segment"
181 );
182 }
183}