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};
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
48            && byte_position < start + length
49        {
50            return Some(*decl_id);
51        }
52    }
53    None
54}
55
56pub fn byte_to_id(
57    nodes: &HashMap<String, HashMap<u64, NodeInfo>>,
58    abs_path: &str,
59    byte_position: usize,
60) -> Option<u64> {
61    let file_nodes = nodes.get(abs_path)?;
62    let mut refs: HashMap<usize, u64> = HashMap::new();
63    for (id, node_info) in file_nodes {
64        let src_parts: Vec<&str> = node_info.src.split(':').collect();
65        if src_parts.len() != 3 {
66            continue;
67        }
68        let start: usize = src_parts[0].parse().ok()?;
69        let length: usize = src_parts[1].parse().ok()?;
70        let end = start + length;
71
72        if start <= byte_position && byte_position < end {
73            let diff = end - start;
74            refs.entry(diff).or_insert(*id);
75        }
76    }
77    refs.keys().min().map(|min_diff| refs[min_diff])
78}
79
80pub fn id_to_location(
81    nodes: &HashMap<String, HashMap<u64, NodeInfo>>,
82    id_to_path: &HashMap<String, String>,
83    node_id: u64,
84) -> Option<Location> {
85    id_to_location_with_index(nodes, id_to_path, node_id, None)
86}
87
88pub fn id_to_location_with_index(
89    nodes: &HashMap<String, HashMap<u64, NodeInfo>>,
90    id_to_path: &HashMap<String, String>,
91    node_id: u64,
92    name_location_index: Option<usize>,
93) -> Option<Location> {
94    let mut target_node: Option<&NodeInfo> = None;
95    for file_nodes in nodes.values() {
96        if let Some(node) = file_nodes.get(&node_id) {
97            target_node = Some(node);
98            break;
99        }
100    }
101    let node = target_node?;
102
103    let (byte_str, length_str, file_id) = if let Some(index) = name_location_index
104        && let Some(name_loc) = node.name_locations.get(index)
105    {
106        let parts: Vec<&str> = name_loc.split(':').collect();
107        if parts.len() == 3 {
108            (parts[0], parts[1], parts[2])
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    include_declaration: bool,
158) -> Vec<Location> {
159    goto_references_with_index(
160        ast_data,
161        file_uri,
162        position,
163        source_bytes,
164        None,
165        include_declaration,
166    )
167}
168
169/// Resolve cursor position to the target definition's location (abs_path + byte offset).
170/// Node IDs are not stable across builds, but byte offsets within a file are.
171/// Returns (abs_path, byte_offset) of the definition node, usable with byte_to_id
172/// in any other build that includes that file.
173pub fn resolve_target_location(
174    build: &CachedBuild,
175    file_uri: &Url,
176    position: Position,
177    source_bytes: &[u8],
178) -> Option<(String, usize)> {
179    let path = file_uri.to_file_path().ok()?;
180    let path_str = path.to_str()?;
181    let abs_path = build.path_to_abs.get(path_str)?;
182    let byte_position = pos_to_bytes(source_bytes, position);
183
184    // Check Yul external references first
185    let target_node_id = if let Some(decl_id) = byte_to_decl_via_external_refs(
186        &build.external_refs,
187        &build.id_to_path_map,
188        abs_path,
189        byte_position,
190    ) {
191        decl_id
192    } else {
193        let node_id = byte_to_id(&build.nodes, abs_path, byte_position)?;
194        let file_nodes = build.nodes.get(abs_path)?;
195        if let Some(node_info) = file_nodes.get(&node_id) {
196            node_info.referenced_declaration.unwrap_or(node_id)
197        } else {
198            node_id
199        }
200    };
201
202    // Find the definition node and extract its file + byte offset
203    for (file_abs_path, file_nodes) in &build.nodes {
204        if let Some(node_info) = file_nodes.get(&target_node_id) {
205            let src_parts: Vec<&str> = node_info.src.split(':').collect();
206            if src_parts.len() == 3 {
207                let byte_offset: usize = src_parts[0].parse().ok()?;
208                return Some((file_abs_path.clone(), byte_offset));
209            }
210        }
211    }
212    None
213}
214
215pub fn goto_references_with_index(
216    ast_data: &Value,
217    file_uri: &Url,
218    position: Position,
219    source_bytes: &[u8],
220    name_location_index: Option<usize>,
221    include_declaration: bool,
222) -> Vec<Location> {
223    let sources = match ast_data.get("sources") {
224        Some(s) => s,
225        None => return vec![],
226    };
227    let build_infos = match ast_data.get("build_infos").and_then(|v| v.as_array()) {
228        Some(infos) => infos,
229        None => return vec![],
230    };
231    let first_build_info = match build_infos.first() {
232        Some(info) => info,
233        None => return vec![],
234    };
235    let id_to_path = match first_build_info
236        .get("source_id_to_path")
237        .and_then(|v| v.as_object())
238    {
239        Some(map) => map,
240        None => return vec![],
241    };
242    let id_to_path_map: HashMap<String, String> = id_to_path
243        .iter()
244        .map(|(k, v)| (k.clone(), v.as_str().unwrap_or("").to_string()))
245        .collect();
246
247    let (nodes, path_to_abs, external_refs) = cache_ids(sources);
248    let all_refs = all_references(&nodes);
249    let path = match file_uri.to_file_path() {
250        Ok(p) => p,
251        Err(_) => return vec![],
252    };
253    let path_str = match path.to_str() {
254        Some(s) => s,
255        None => return vec![],
256    };
257    let abs_path = match path_to_abs.get(path_str) {
258        Some(ap) => ap,
259        None => return vec![],
260    };
261    let byte_position = pos_to_bytes(source_bytes, position);
262
263    // Check if cursor is on a Yul external reference first
264    let target_node_id = if let Some(decl_id) =
265        byte_to_decl_via_external_refs(&external_refs, &id_to_path_map, abs_path, byte_position)
266    {
267        decl_id
268    } else {
269        let node_id = match byte_to_id(&nodes, abs_path, byte_position) {
270            Some(id) => id,
271            None => return vec![],
272        };
273        let file_nodes = match nodes.get(abs_path) {
274            Some(nodes) => nodes,
275            None => return vec![],
276        };
277        if let Some(node_info) = file_nodes.get(&node_id) {
278            node_info.referenced_declaration.unwrap_or(node_id)
279        } else {
280            node_id
281        }
282    };
283
284    let mut results = HashSet::new();
285    if include_declaration {
286        results.insert(target_node_id);
287    }
288    if let Some(refs) = all_refs.get(&target_node_id) {
289        results.extend(refs.iter().copied());
290    }
291    let mut locations = Vec::new();
292    for id in results {
293        if let Some(location) =
294            id_to_location_with_index(&nodes, &id_to_path_map, id, name_location_index)
295        {
296            locations.push(location);
297        }
298    }
299
300    // Also add Yul external reference use sites that point to our target declaration
301    for (src_str, decl_id) in &external_refs {
302        if *decl_id == target_node_id
303            && let Some(location) = src_to_location(src_str, &id_to_path_map)
304        {
305            locations.push(location);
306        }
307    }
308
309    let mut unique_locations = Vec::new();
310    let mut seen = std::collections::HashSet::new();
311    for location in locations {
312        let key = (
313            location.uri.clone(),
314            location.range.start.line,
315            location.range.start.character,
316            location.range.end.line,
317            location.range.end.character,
318        );
319        if seen.insert(key) {
320            unique_locations.push(location);
321        }
322    }
323    unique_locations
324}
325
326/// Find all references to a definition in a single AST build, identified by
327/// the definition's file path + byte offset (stable across builds).
328/// Uses byte_to_id to find this build's node ID for the same definition,
329/// then scans for referenced_declaration matches.
330pub fn goto_references_for_target(
331    build: &CachedBuild,
332    def_abs_path: &str,
333    def_byte_offset: usize,
334    name_location_index: Option<usize>,
335    include_declaration: bool,
336) -> Vec<Location> {
337    // Find this build's node ID for the definition using byte offset
338    let target_node_id = match byte_to_id(&build.nodes, def_abs_path, def_byte_offset) {
339        Some(id) => {
340            // If it's a reference, follow to the definition
341            if let Some(file_nodes) = build.nodes.get(def_abs_path) {
342                if let Some(node_info) = file_nodes.get(&id) {
343                    node_info.referenced_declaration.unwrap_or(id)
344                } else {
345                    id
346                }
347            } else {
348                id
349            }
350        }
351        None => return vec![],
352    };
353
354    // Collect the definition node + all nodes whose referenced_declaration matches
355    let mut results = HashSet::new();
356    if include_declaration {
357        results.insert(target_node_id);
358    }
359    for file_nodes in build.nodes.values() {
360        for (id, node_info) in file_nodes {
361            if node_info.referenced_declaration == Some(target_node_id) {
362                results.insert(*id);
363            }
364        }
365    }
366
367    let mut locations = Vec::new();
368    for id in results {
369        if let Some(location) =
370            id_to_location_with_index(&build.nodes, &build.id_to_path_map, id, name_location_index)
371        {
372            locations.push(location);
373        }
374    }
375
376    // Yul external reference use sites
377    for (src_str, decl_id) in &build.external_refs {
378        if *decl_id == target_node_id
379            && let Some(location) = src_to_location(src_str, &build.id_to_path_map)
380        {
381            locations.push(location);
382        }
383    }
384
385    locations
386}