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::{bytes_to_pos, cache_ids, pos_to_bytes, src_to_location, ExternalRefs, NodeInfo};
6
7pub fn all_references(nodes: &HashMap<String, HashMap<u64, NodeInfo>>) -> HashMap<u64, Vec<u64>> {
8    let mut all_refs: HashMap<u64, Vec<u64>> = HashMap::new();
9    for file_nodes in nodes.values() {
10        for (id, node_info) in file_nodes {
11            if let Some(ref_id) = node_info.referenced_declaration {
12                all_refs.entry(ref_id).or_default().push(*id);
13                all_refs.entry(*id).or_default().push(ref_id);
14            }
15        }
16    }
17    all_refs
18}
19
20/// Check if cursor byte position falls on a Yul external reference in the given file.
21/// Returns the Solidity declaration id if so.
22pub fn byte_to_decl_via_external_refs(
23    external_refs: &ExternalRefs,
24    id_to_path: &HashMap<String, String>,
25    abs_path: &str,
26    byte_position: usize,
27) -> Option<u64> {
28    // Build reverse map: file_path -> file_id
29    let path_to_file_id: HashMap<&str, &str> = id_to_path
30        .iter()
31        .map(|(id, p)| (p.as_str(), id.as_str()))
32        .collect();
33    let current_file_id = path_to_file_id.get(abs_path)?;
34
35    for (src_str, decl_id) in external_refs {
36        let parts: Vec<&str> = src_str.split(':').collect();
37        if parts.len() != 3 {
38            continue;
39        }
40        // Only consider refs in the current file
41        if parts[2] != *current_file_id {
42            continue;
43        }
44        if let (Ok(start), Ok(length)) = (parts[0].parse::<usize>(), parts[1].parse::<usize>()) {
45            if start <= byte_position && byte_position < start + length {
46                return Some(*decl_id);
47            }
48        }
49    }
50    None
51}
52
53pub fn byte_to_id(
54    nodes: &HashMap<String, HashMap<u64, NodeInfo>>,
55    abs_path: &str,
56    byte_position: usize,
57) -> Option<u64> {
58    let file_nodes = nodes.get(abs_path)?;
59    let mut refs: HashMap<usize, u64> = HashMap::new();
60    for (id, node_info) in file_nodes {
61        let src_parts: Vec<&str> = node_info.src.split(':').collect();
62        if src_parts.len() != 3 {
63            continue;
64        }
65        let start: usize = src_parts[0].parse().ok()?;
66        let length: usize = src_parts[1].parse().ok()?;
67        let end = start + length;
68
69        if start <= byte_position && byte_position < end {
70            let diff = end - start;
71            refs.entry(diff).or_insert(*id);
72        }
73    }
74    refs.keys().min().map(|min_diff| refs[min_diff])
75}
76
77pub fn id_to_location(
78    nodes: &HashMap<String, HashMap<u64, NodeInfo>>,
79    id_to_path: &HashMap<String, String>,
80    node_id: u64,
81) -> Option<Location> {
82    id_to_location_with_index(nodes, id_to_path, node_id, None)
83}
84
85pub fn id_to_location_with_index(
86    nodes: &HashMap<String, HashMap<u64, NodeInfo>>,
87    id_to_path: &HashMap<String, String>,
88    node_id: u64,
89    name_location_index: Option<usize>,
90) -> Option<Location> {
91    let mut target_node: Option<&NodeInfo> = None;
92    for file_nodes in nodes.values() {
93        if let Some(node) = file_nodes.get(&node_id) {
94            target_node = Some(node);
95            break;
96        }
97    }
98    let node = target_node?;
99
100    let (byte_str, length_str, file_id) = if let Some(index) = name_location_index {
101        if let Some(name_loc) = node.name_locations.get(index) {
102            let parts: Vec<&str> = name_loc.split(':').collect();
103            if parts.len() == 3 {
104                (parts[0], parts[1], parts[2])
105            } else {
106                return None;
107            }
108        } else {
109            return None;
110        }
111    } else if let Some(name_location) = &node.name_location {
112        let parts: Vec<&str> = name_location.split(':').collect();
113        if parts.len() == 3 {
114            (parts[0], parts[1], parts[2])
115        } else {
116            return None;
117        }
118    } else {
119        // Fallback to src location for nodes without nameLocation
120        let parts: Vec<&str> = node.src.split(':').collect();
121        if parts.len() == 3 {
122            (parts[0], parts[1], parts[2])
123        } else {
124            return None;
125        }
126    };
127
128    let byte_offset: usize = byte_str.parse().ok()?;
129    let length: usize = length_str.parse().ok()?;
130    let file_path = id_to_path.get(file_id)?;
131
132    let absolute_path = if std::path::Path::new(file_path).is_absolute() {
133        std::path::PathBuf::from(file_path)
134    } else {
135        std::env::current_dir().ok()?.join(file_path)
136    };
137    let source_bytes = std::fs::read(&absolute_path).ok()?;
138    let start_pos = bytes_to_pos(&source_bytes, byte_offset)?;
139    let end_pos = bytes_to_pos(&source_bytes, byte_offset + length)?;
140    let uri = Url::from_file_path(&absolute_path).ok()?;
141
142    Some(Location {
143        uri,
144        range: Range {
145            start: start_pos,
146            end: end_pos,
147        },
148    })
149}
150
151pub fn goto_references(
152    ast_data: &Value,
153    file_uri: &Url,
154    position: Position,
155    source_bytes: &[u8],
156) -> Vec<Location> {
157    goto_references_with_index(ast_data, file_uri, position, source_bytes, None)
158}
159
160pub fn goto_references_with_index(
161    ast_data: &Value,
162    file_uri: &Url,
163    position: Position,
164    source_bytes: &[u8],
165    name_location_index: Option<usize>,
166) -> Vec<Location> {
167    let sources = match ast_data.get("sources") {
168        Some(s) => s,
169        None => return vec![],
170    };
171    let build_infos = match ast_data.get("build_infos").and_then(|v| v.as_array()) {
172        Some(infos) => infos,
173        None => return vec![],
174    };
175    let first_build_info = match build_infos.first() {
176        Some(info) => info,
177        None => return vec![],
178    };
179    let id_to_path = match first_build_info
180        .get("source_id_to_path")
181        .and_then(|v| v.as_object())
182    {
183        Some(map) => map,
184        None => return vec![],
185    };
186    let id_to_path_map: HashMap<String, String> = id_to_path
187        .iter()
188        .map(|(k, v)| (k.clone(), v.as_str().unwrap_or("").to_string()))
189        .collect();
190
191    let (nodes, path_to_abs, external_refs) = cache_ids(sources);
192    let all_refs = all_references(&nodes);
193    let path = match file_uri.to_file_path() {
194        Ok(p) => p,
195        Err(_) => return vec![],
196    };
197    let path_str = match path.to_str() {
198        Some(s) => s,
199        None => return vec![],
200    };
201    let abs_path = match path_to_abs.get(path_str) {
202        Some(ap) => ap,
203        None => return vec![],
204    };
205    let byte_position = pos_to_bytes(source_bytes, position);
206
207    // Check if cursor is on a Yul external reference first
208    let target_node_id = if let Some(decl_id) =
209        byte_to_decl_via_external_refs(&external_refs, &id_to_path_map, abs_path, byte_position)
210    {
211        decl_id
212    } else {
213        let node_id = match byte_to_id(&nodes, abs_path, byte_position) {
214            Some(id) => id,
215            None => return vec![],
216        };
217        let file_nodes = match nodes.get(abs_path) {
218            Some(nodes) => nodes,
219            None => return vec![],
220        };
221        if let Some(node_info) = file_nodes.get(&node_id) {
222            node_info.referenced_declaration.unwrap_or(node_id)
223        } else {
224            node_id
225        }
226    };
227
228    let mut results = HashSet::new();
229    results.insert(target_node_id);
230    if let Some(refs) = all_refs.get(&target_node_id) {
231        results.extend(refs.iter().copied());
232    }
233    let mut locations = Vec::new();
234    for id in results {
235        if let Some(location) =
236            id_to_location_with_index(&nodes, &id_to_path_map, id, name_location_index)
237        {
238            locations.push(location);
239        }
240    }
241
242    // Also add Yul external reference use sites that point to our target declaration
243    for (src_str, decl_id) in &external_refs {
244        if *decl_id == target_node_id {
245            if let Some(location) = src_to_location(src_str, &id_to_path_map) {
246                locations.push(location);
247            }
248        }
249    }
250
251    let mut unique_locations = Vec::new();
252    let mut seen = std::collections::HashSet::new();
253    for location in locations {
254        let key = (
255            location.uri.clone(),
256            location.range.start.line,
257            location.range.start.character,
258            location.range.end.line,
259            location.range.end.character,
260        );
261        if seen.insert(key) {
262            unique_locations.push(location);
263        }
264    }
265    unique_locations
266}