Skip to main content

solidity_language_server/
references.rs

1use std::collections::{HashMap, HashSet};
2use tower_lsp::lsp_types::{Location, Position, Range, Url};
3
4use crate::goto::{
5    CachedBuild, ExternalRefs, NodeInfo, bytes_to_pos, pos_to_bytes, src_to_location,
6};
7use crate::types::{NodeId, SourceLoc};
8
9pub fn all_references(
10    nodes: &HashMap<String, HashMap<NodeId, NodeInfo>>,
11) -> HashMap<NodeId, Vec<NodeId>> {
12    let mut all_refs: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
13    for file_nodes in nodes.values() {
14        for (id, node_info) in file_nodes {
15            if let Some(ref_id) = node_info.referenced_declaration {
16                all_refs.entry(ref_id).or_default().push(*id);
17                all_refs.entry(*id).or_default().push(ref_id);
18            }
19        }
20    }
21    all_refs
22}
23
24/// Check if cursor byte position falls on a Yul external reference in the given file.
25/// Returns the Solidity declaration id if so.
26pub fn byte_to_decl_via_external_refs(
27    external_refs: &ExternalRefs,
28    id_to_path: &HashMap<String, String>,
29    abs_path: &str,
30    byte_position: usize,
31) -> Option<NodeId> {
32    // Build reverse map: file_path -> file_id
33    let path_to_file_id: HashMap<&str, &str> = id_to_path
34        .iter()
35        .map(|(id, p)| (p.as_str(), id.as_str()))
36        .collect();
37    let current_file_id = path_to_file_id.get(abs_path)?;
38
39    for (src_str, decl_id) in external_refs {
40        let Some(src_loc) = SourceLoc::parse(src_str) else {
41            continue;
42        };
43        // Only consider refs in the current file
44        if src_loc.file_id_str() != *current_file_id {
45            continue;
46        }
47        if src_loc.offset <= byte_position && byte_position < src_loc.end() {
48            return Some(*decl_id);
49        }
50    }
51    None
52}
53
54pub fn byte_to_id(
55    nodes: &HashMap<String, HashMap<NodeId, NodeInfo>>,
56    abs_path: &str,
57    byte_position: usize,
58) -> Option<NodeId> {
59    let file_nodes = nodes.get(abs_path)?;
60    let mut refs: HashMap<usize, (NodeId, bool)> = HashMap::new();
61    for (id, node_info) in file_nodes {
62        let Some(src_loc) = SourceLoc::parse(&node_info.src) else {
63            continue;
64        };
65
66        if src_loc.offset <= byte_position && byte_position < src_loc.end() {
67            let diff = src_loc.length;
68            let has_ref = node_info.referenced_declaration.is_some();
69            match refs.entry(diff) {
70                std::collections::hash_map::Entry::Vacant(e) => {
71                    e.insert((*id, has_ref));
72                }
73                std::collections::hash_map::Entry::Occupied(mut e) => {
74                    // When two nodes share the same span length, prefer the one
75                    // with referencedDeclaration set. This resolves ambiguity
76                    // between InheritanceSpecifier and its child baseName
77                    // IdentifierPath — both have identical src ranges but only
78                    // the IdentifierPath carries referencedDeclaration.
79                    if has_ref && !e.get().1 {
80                        e.insert((*id, has_ref));
81                    }
82                }
83            }
84        }
85    }
86    refs.keys().min().map(|min_diff| refs[min_diff].0)
87}
88
89pub fn id_to_location(
90    nodes: &HashMap<String, HashMap<NodeId, NodeInfo>>,
91    id_to_path: &HashMap<String, String>,
92    node_id: NodeId,
93) -> Option<Location> {
94    id_to_location_with_index(nodes, id_to_path, node_id, None)
95}
96
97pub fn id_to_location_with_index(
98    nodes: &HashMap<String, HashMap<NodeId, NodeInfo>>,
99    id_to_path: &HashMap<String, String>,
100    node_id: NodeId,
101    name_location_index: Option<usize>,
102) -> Option<Location> {
103    let mut target_node: Option<&NodeInfo> = None;
104    for file_nodes in nodes.values() {
105        if let Some(node) = file_nodes.get(&node_id) {
106            target_node = Some(node);
107            break;
108        }
109    }
110    let node = target_node?;
111
112    let loc_str = if let Some(index) = name_location_index
113        && let Some(name_loc) = node.name_locations.get(index)
114    {
115        name_loc.as_str()
116    } else if let Some(name_location) = &node.name_location {
117        name_location.as_str()
118    } else {
119        // Fallback to src location for nodes without nameLocation
120        node.src.as_str()
121    };
122
123    let loc = SourceLoc::parse(loc_str)?;
124    let file_path = id_to_path.get(&loc.file_id_str())?;
125
126    let absolute_path = if std::path::Path::new(file_path).is_absolute() {
127        std::path::PathBuf::from(file_path)
128    } else {
129        std::env::current_dir().ok()?.join(file_path)
130    };
131    let source_bytes = std::fs::read(&absolute_path).ok()?;
132    let start_pos = bytes_to_pos(&source_bytes, loc.offset)?;
133    let end_pos = bytes_to_pos(&source_bytes, loc.end())?;
134    let uri = Url::from_file_path(&absolute_path).ok()?;
135
136    Some(Location {
137        uri,
138        range: Range {
139            start: start_pos,
140            end: end_pos,
141        },
142    })
143}
144
145/// Find all references using pre-built `CachedBuild` indices.
146/// Avoids redundant O(N) AST traversal by reusing cached node maps.
147pub fn goto_references_cached(
148    build: &CachedBuild,
149    file_uri: &Url,
150    position: Position,
151    source_bytes: &[u8],
152    name_location_index: Option<usize>,
153    include_declaration: bool,
154) -> Vec<Location> {
155    let all_refs = all_references(&build.nodes);
156    let path = match file_uri.to_file_path() {
157        Ok(p) => p,
158        Err(_) => return vec![],
159    };
160    let path_str = match path.to_str() {
161        Some(s) => s,
162        None => return vec![],
163    };
164    let abs_path = match build.path_to_abs.get(path_str) {
165        Some(ap) => ap,
166        None => return vec![],
167    };
168    let byte_position = pos_to_bytes(source_bytes, position);
169
170    // Check if cursor is on a Yul external reference first
171    let target_node_id = if let Some(decl_id) = byte_to_decl_via_external_refs(
172        &build.external_refs,
173        &build.id_to_path_map,
174        abs_path,
175        byte_position,
176    ) {
177        decl_id
178    } else {
179        let node_id = match byte_to_id(&build.nodes, abs_path, byte_position) {
180            Some(id) => id,
181            None => return vec![],
182        };
183        let file_nodes = match build.nodes.get(abs_path) {
184            Some(nodes) => nodes,
185            None => return vec![],
186        };
187        if let Some(node_info) = file_nodes.get(&node_id) {
188            node_info.referenced_declaration.unwrap_or(node_id)
189        } else {
190            node_id
191        }
192    };
193
194    let mut results: HashSet<NodeId> = HashSet::new();
195    if include_declaration {
196        results.insert(target_node_id);
197    }
198    if let Some(refs) = all_refs.get(&target_node_id) {
199        results.extend(refs.iter().copied());
200    }
201    let mut locations = Vec::new();
202    for id in results {
203        if let Some(location) =
204            id_to_location_with_index(&build.nodes, &build.id_to_path_map, id, name_location_index)
205        {
206            locations.push(location);
207        }
208    }
209
210    // Also add Yul external reference use sites
211    for (src_str, decl_id) in &build.external_refs {
212        if *decl_id == target_node_id
213            && let Some(location) = src_to_location(src_str, &build.id_to_path_map)
214        {
215            locations.push(location);
216        }
217    }
218
219    let mut unique_locations = Vec::new();
220    let mut seen = std::collections::HashSet::new();
221    for location in locations {
222        let key = (
223            location.uri.clone(),
224            location.range.start.line,
225            location.range.start.character,
226            location.range.end.line,
227            location.range.end.character,
228        );
229        if seen.insert(key) {
230            unique_locations.push(location);
231        }
232    }
233    unique_locations
234}
235
236/// Resolve cursor position to the target definition's location (abs_path + byte offset).
237/// Node IDs are not stable across builds, but byte offsets within a file are.
238/// Returns (abs_path, byte_offset) of the definition node, usable with byte_to_id
239/// in any other build that includes that file.
240pub fn resolve_target_location(
241    build: &CachedBuild,
242    file_uri: &Url,
243    position: Position,
244    source_bytes: &[u8],
245) -> Option<(String, usize)> {
246    let path = file_uri.to_file_path().ok()?;
247    let path_str = path.to_str()?;
248    let abs_path = build.path_to_abs.get(path_str)?;
249    let byte_position = pos_to_bytes(source_bytes, position);
250
251    // Check Yul external references first
252    let target_node_id = if let Some(decl_id) = byte_to_decl_via_external_refs(
253        &build.external_refs,
254        &build.id_to_path_map,
255        abs_path,
256        byte_position,
257    ) {
258        decl_id
259    } else {
260        let node_id = byte_to_id(&build.nodes, abs_path, byte_position)?;
261        let file_nodes = build.nodes.get(abs_path)?;
262        if let Some(node_info) = file_nodes.get(&node_id) {
263            node_info.referenced_declaration.unwrap_or(node_id)
264        } else {
265            node_id
266        }
267    };
268
269    // Find the definition node and extract its file + byte offset.
270    // Prefer `nameLocation` over `src` — for declarations like
271    // `IPoolManager manager;`, `src` spans the entire declaration
272    // (starting at `IPoolManager`) while `nameLocation` points at
273    // `manager`. Using `src.offset` would cause `byte_to_id` in other
274    // builds to land on the type name node instead of the variable,
275    // contaminating cross-file references with the type's references.
276    for (file_abs_path, file_nodes) in &build.nodes {
277        if let Some(node_info) = file_nodes.get(&target_node_id) {
278            let loc_str = node_info.name_location.as_deref().unwrap_or(&node_info.src);
279            if let Some(src_loc) = SourceLoc::parse(loc_str) {
280                return Some((file_abs_path.clone(), src_loc.offset));
281            }
282        }
283    }
284    None
285}
286
287/// Find all references to a definition in a single AST build, identified by
288/// the definition's file path + byte offset (stable across builds).
289/// Uses byte_to_id to find this build's node ID for the same definition,
290/// then scans for referenced_declaration matches.
291pub fn goto_references_for_target(
292    build: &CachedBuild,
293    def_abs_path: &str,
294    def_byte_offset: usize,
295    name_location_index: Option<usize>,
296    include_declaration: bool,
297) -> Vec<Location> {
298    // Find this build's node ID for the definition using byte offset
299    let target_node_id = match byte_to_id(&build.nodes, def_abs_path, def_byte_offset) {
300        Some(id) => {
301            // If it's a reference, follow to the definition
302            if let Some(file_nodes) = build.nodes.get(def_abs_path) {
303                if let Some(node_info) = file_nodes.get(&id) {
304                    node_info.referenced_declaration.unwrap_or(id)
305                } else {
306                    id
307                }
308            } else {
309                id
310            }
311        }
312        None => return vec![],
313    };
314
315    // Collect the definition node + all nodes whose referenced_declaration matches
316    let mut results: HashSet<NodeId> = HashSet::new();
317    if include_declaration {
318        results.insert(target_node_id);
319    }
320    for file_nodes in build.nodes.values() {
321        for (id, node_info) in file_nodes {
322            if node_info.referenced_declaration == Some(target_node_id) {
323                results.insert(*id);
324            }
325        }
326    }
327
328    let mut locations = Vec::new();
329    for id in results {
330        if let Some(location) =
331            id_to_location_with_index(&build.nodes, &build.id_to_path_map, id, name_location_index)
332        {
333            locations.push(location);
334        }
335    }
336
337    // Yul external reference use sites
338    for (src_str, decl_id) in &build.external_refs {
339        if *decl_id == target_node_id
340            && let Some(location) = src_to_location(src_str, &build.id_to_path_map)
341        {
342            locations.push(location);
343        }
344    }
345
346    locations
347}