Skip to main content

harn_modules/
references.rs

1//! Inverse of [`super::ModuleGraph::definition_of`].
2//!
3//! The graph already records where a name is defined. This module walks
4//! retained ASTs and records every use that resolves to that same
5//! [`super::DefSite`], so find-references and `harn graph` answer from
6//! resolution rather than a bare-string match.
7
8use std::collections::{HashMap, HashSet};
9use std::path::PathBuf;
10
11use harn_lexer::{Lexer, Span, Token, TokenKind};
12use harn_parser::{
13    lexical::{resolved_identifier_bindings, BindingId},
14    visit::walk_program_interpolated,
15    Node,
16};
17
18use super::{DefKind, DefSite, ModuleGraph, ParsedModuleSource};
19
20/// One use of a name that resolved to a [`DefSite`].
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct RefSite {
23    pub file: PathBuf,
24    pub span: Span,
25    pub name: String,
26}
27
28/// Resolution-backed reference index for a built module graph.
29///
30/// Built from the same graph that answers go-to-definition. Two same-named
31/// symbols in different modules stay distinct because each use is keyed by
32/// the `DefSite` it resolved to, not by the identifier text.
33#[derive(Debug, Clone, Default)]
34pub struct ReferenceIndex {
35    /// Every file whose AST was walked. A consumer can tell whether the
36    /// answer covers the tree it asked about.
37    pub files: Vec<PathBuf>,
38    /// True when at least one walked file came from an unsaved buffer
39    /// rather than disk. The LSP has those; `harn graph` does not.
40    pub has_unsaved_buffers: bool,
41    by_def: HashMap<DefKey, Vec<RefSite>>,
42    definitions: HashMap<DefKey, DefSite>,
43    site_defs: Vec<(RefSite, DefKey)>,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Hash)]
47struct DefKey {
48    file: PathBuf,
49    name: String,
50    start: usize,
51    end: usize,
52}
53
54impl DefKey {
55    fn from_def(def: &DefSite) -> Self {
56        Self {
57            file: def.file.clone(),
58            name: def.name.clone(),
59            start: def.span.start,
60            end: def.span.end,
61        }
62    }
63}
64
65impl ReferenceIndex {
66    /// Definition resolved at one identifier token.
67    ///
68    /// This positional lookup is required for lexical shadowing: a file and
69    /// name alone cannot distinguish a local binding from an imported symbol.
70    pub fn definition_at(
71        &self,
72        file: &std::path::Path,
73        name: &str,
74        offset: usize,
75    ) -> Option<DefSite> {
76        let file = super::canonical_path(file);
77        self.site_defs
78            .iter()
79            .filter(|(site, _)| {
80                site.file == file
81                    && site.name == name
82                    && offset >= site.span.start
83                    && offset <= site.span.end
84            })
85            .min_by_key(|(site, _)| site.span.end.saturating_sub(site.span.start))
86            .and_then(|(_, key)| self.definitions.get(key))
87            .cloned()
88    }
89
90    /// Uses that resolve to `def`, including the definition site itself.
91    pub fn references_to(&self, def: &DefSite) -> Vec<RefSite> {
92        self.by_def
93            .get(&DefKey::from_def(def))
94            .cloned()
95            .unwrap_or_default()
96    }
97
98    /// Every resolved use → definition edge, sorted for deterministic tests
99    /// and `harn graph --json`.
100    pub fn edges(&self) -> Vec<ReferenceEdge> {
101        let mut edges = Vec::new();
102        for (key, refs) in &self.by_def {
103            for site in refs {
104                edges.push(ReferenceEdge {
105                    from: site.clone(),
106                    to_file: key.file.clone(),
107                    to_name: key.name.clone(),
108                });
109            }
110        }
111        edges.sort_by(|left, right| {
112            left.from
113                .file
114                .cmp(&right.from.file)
115                .then_with(|| left.from.span.start.cmp(&right.from.span.start))
116                .then_with(|| left.to_file.cmp(&right.to_file))
117                .then_with(|| left.to_name.cmp(&right.to_name))
118        });
119        edges
120    }
121}
122
123/// One resolution-backed reference edge: a use site and the definition it
124/// resolved to.
125#[derive(Debug, Clone, PartialEq, Eq)]
126pub struct ReferenceEdge {
127    pub from: RefSite,
128    pub to_file: PathBuf,
129    pub to_name: String,
130}
131
132/// Walk `sources` and record every identifier that [`ModuleGraph::definition_of`]
133/// can resolve.
134///
135/// `unsaved` names files whose text came from an editor buffer. The index
136/// reports that so a consumer knows whether it is looking at the file it
137/// just edited.
138pub fn index_references(
139    graph: &ModuleGraph,
140    sources: &HashMap<PathBuf, ParsedModuleSource>,
141    unsaved: &HashSet<PathBuf>,
142) -> ReferenceIndex {
143    let mut files: Vec<PathBuf> = sources.keys().cloned().collect();
144    files.sort();
145    let has_unsaved_buffers = files.iter().any(|file| unsaved.contains(file));
146    let mut index = ReferenceIndex {
147        files,
148        has_unsaved_buffers,
149        by_def: HashMap::new(),
150        definitions: HashMap::new(),
151        site_defs: Vec::new(),
152    };
153
154    for (file, parsed) in sources {
155        let lexical = resolved_identifier_bindings(&[], &parsed.program);
156        let tokens = Lexer::new(&parsed.source)
157            .tokenize()
158            .expect("a retained parsed source must still lex");
159
160        // A local declaration may not otherwise appear as an identifier AST
161        // node. Seed every binding reached by a use so include-declaration and
162        // a cursor on the declaration resolve to the same stable identity.
163        let mut lexical_defs: HashMap<BindingId, DefSite> = HashMap::new();
164        for binding in lexical.values() {
165            lexical_defs.entry(binding.clone()).or_insert_with(|| {
166                graph
167                    .definition_of(file, &binding.name)
168                    .filter(|def| {
169                        def.file == *file
170                            && def.span.start == binding.declaration_start
171                            && def.span.end == binding.declaration_end
172                    })
173                    .unwrap_or_else(|| DefSite {
174                        name: binding.name.clone(),
175                        file: file.clone(),
176                        kind: DefKind::Variable,
177                        span: Span::with_offsets(
178                            binding.declaration_start,
179                            binding.declaration_end,
180                            1,
181                            1,
182                        ),
183                    })
184            });
185        }
186        for (binding, def) in &lexical_defs {
187            if let Some(span) = identifier_span(
188                &tokens,
189                &binding.name,
190                binding.declaration_start,
191                binding.declaration_end,
192            ) {
193                insert_reference(
194                    &mut index,
195                    def.clone(),
196                    RefSite {
197                        file: file.clone(),
198                        span,
199                        name: binding.name.clone(),
200                    },
201                );
202            }
203        }
204
205        walk_program_interpolated(&parsed.source, &parsed.program, &mut |node| {
206            for (name, broad_span) in name_uses(node) {
207                let def = lexical
208                    .get(&(broad_span.start, broad_span.end))
209                    .and_then(|binding| lexical_defs.get(binding).cloned())
210                    .or_else(|| graph.definition_of(file, name));
211                if let (Some(def), Some(span)) = (
212                    def,
213                    identifier_span(&tokens, name, broad_span.start, broad_span.end),
214                ) {
215                    insert_reference(
216                        &mut index,
217                        def,
218                        RefSite {
219                            file: file.clone(),
220                            span,
221                            name: name.to_string(),
222                        },
223                    );
224                }
225            }
226        });
227    }
228
229    for refs in index.by_def.values_mut() {
230        refs.sort_by(|left, right| {
231            left.file
232                .cmp(&right.file)
233                .then_with(|| left.span.start.cmp(&right.span.start))
234        });
235        refs.dedup_by(|left, right| {
236            left.file == right.file
237                && left.span.start == right.span.start
238                && left.name == right.name
239        });
240    }
241    index.site_defs.sort_by(|(left, _), (right, _)| {
242        left.file
243            .cmp(&right.file)
244            .then_with(|| left.span.start.cmp(&right.span.start))
245            .then_with(|| left.name.cmp(&right.name))
246    });
247    index
248        .site_defs
249        .dedup_by(|(left, left_key), (right, right_key)| {
250            left.file == right.file
251                && left.span == right.span
252                && left.name == right.name
253                && left_key == right_key
254        });
255
256    index
257}
258
259fn insert_reference(index: &mut ReferenceIndex, def: DefSite, site: RefSite) {
260    let key = DefKey::from_def(&def);
261    index.definitions.entry(key.clone()).or_insert(def);
262    index
263        .by_def
264        .entry(key.clone())
265        .or_default()
266        .push(site.clone());
267    index.site_defs.push((site, key));
268}
269
270fn identifier_span(tokens: &[Token], name: &str, start: usize, end: usize) -> Option<Span> {
271    tokens.iter().find_map(|token| match &token.kind {
272        TokenKind::Identifier(found)
273            if found == name && token.span.start >= start && token.span.end <= end =>
274        {
275            Some(token.span)
276        }
277        _ => None,
278    })
279}
280
281fn name_uses(node: &harn_parser::SNode) -> Vec<(&str, Span)> {
282    match &node.node {
283        Node::Identifier(name) => vec![(name.as_str(), node.span)],
284        Node::FunctionCall { name, .. } => vec![(name.as_str(), node.span)],
285        Node::FnDecl { name, .. }
286        | Node::Pipeline { name, .. }
287        | Node::ToolDecl { name, .. }
288        | Node::SkillDecl { name, .. }
289        | Node::StructDecl { name, .. }
290        | Node::EnumDecl { name, .. }
291        | Node::InterfaceDecl { name, .. }
292        | Node::TypeDecl { name, .. }
293        | Node::OverrideDecl { name, .. } => vec![(name.as_str(), node.span)],
294        Node::EvalPackDecl { binding_name, .. } => vec![(binding_name.as_str(), node.span)],
295        _ => Vec::new(),
296    }
297}