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