Skip to main content

solidity_language_server/
references.rs

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