Skip to main content

solidity_language_server/
goto.rs

1use serde_json::Value;
2use std::collections::HashMap;
3use tower_lsp::lsp_types::{Location, Position, Range, Url};
4
5#[derive(Debug, Clone)]
6pub struct NodeInfo {
7    pub src: String,
8    pub name_location: Option<String>,
9    pub name_locations: Vec<String>,
10    pub referenced_declaration: Option<u64>,
11    pub node_type: Option<String>,
12    pub member_location: Option<String>,
13    pub absolute_path: Option<String>,
14}
15
16/// All AST child keys to traverse (Solidity + Yul).
17pub const CHILD_KEYS: &[&str] = &[
18    "AST",
19    "arguments",
20    "baseContracts",
21    "baseExpression",
22    "baseName",
23    "baseType",
24    "block",
25    "body",
26    "components",
27    "condition",
28    "declarations",
29    "endExpression",
30    "errorCall",
31    "eventCall",
32    "expression",
33    "externalCall",
34    "falseBody",
35    "falseExpression",
36    "file",
37    "foreign",
38    "functionName",
39    "indexExpression",
40    "initialValue",
41    "initializationExpression",
42    "keyType",
43    "leftExpression",
44    "leftHandSide",
45    "libraryName",
46    "literals",
47    "loopExpression",
48    "members",
49    "modifierName",
50    "modifiers",
51    "name",
52    "names",
53    "nodes",
54    "options",
55    "overrides",
56    "parameters",
57    "pathNode",
58    "post",
59    "pre",
60    "returnParameters",
61    "rightExpression",
62    "rightHandSide",
63    "startExpression",
64    "statements",
65    "storageLayout",
66    "subExpression",
67    "subdenomination",
68    "symbolAliases",
69    "trueBody",
70    "trueExpression",
71    "typeName",
72    "unitAlias",
73    "value",
74    "valueType",
75    "variableNames",
76    "variables",
77];
78
79fn push_if_node_or_array<'a>(tree: &'a Value, key: &str, stack: &mut Vec<&'a Value>) {
80    if let Some(value) = tree.get(key) {
81        match value {
82            Value::Array(arr) => {
83                stack.extend(arr);
84            }
85            Value::Object(_) => {
86                stack.push(value);
87            }
88            _ => {}
89        }
90    }
91}
92
93/// Maps `"offset:length:fileId"` src strings from Yul externalReferences
94/// to the Solidity declaration node id they refer to.
95pub type ExternalRefs = HashMap<String, u64>;
96
97pub fn cache_ids(
98    sources: &Value,
99) -> (
100    HashMap<String, HashMap<u64, NodeInfo>>,
101    HashMap<String, String>,
102    ExternalRefs,
103) {
104    let mut nodes: HashMap<String, HashMap<u64, NodeInfo>> = HashMap::new();
105    let mut path_to_abs: HashMap<String, String> = HashMap::new();
106    let mut external_refs: ExternalRefs = HashMap::new();
107
108    if let Some(sources_obj) = sources.as_object() {
109        for (path, contents) in sources_obj {
110            if let Some(contents_array) = contents.as_array()
111                && let Some(first_content) = contents_array.first()
112                && let Some(source_file) = first_content.get("source_file")
113                && let Some(ast) = source_file.get("ast")
114            {
115                // Get the absolute path for this file
116                let abs_path = ast
117                    .get("absolutePath")
118                    .and_then(|v| v.as_str())
119                    .unwrap_or(path)
120                    .to_string();
121
122                path_to_abs.insert(path.clone(), abs_path.clone());
123
124                // Initialize the nodes map for this file
125                if !nodes.contains_key(&abs_path) {
126                    nodes.insert(abs_path.clone(), HashMap::new());
127                }
128
129                if let Some(id) = ast.get("id").and_then(|v| v.as_u64())
130                    && let Some(src) = ast.get("src").and_then(|v| v.as_str())
131                {
132                    nodes.get_mut(&abs_path).unwrap().insert(
133                        id,
134                        NodeInfo {
135                            src: src.to_string(),
136                            name_location: None,
137                            name_locations: vec![],
138                            referenced_declaration: None,
139                            node_type: ast
140                                .get("nodeType")
141                                .and_then(|v| v.as_str())
142                                .map(|s| s.to_string()),
143                            member_location: None,
144                            absolute_path: ast
145                                .get("absolutePath")
146                                .and_then(|v| v.as_str())
147                                .map(|s| s.to_string()),
148                        },
149                    );
150                }
151
152                let mut stack = vec![ast];
153
154                while let Some(tree) = stack.pop() {
155                    if let Some(id) = tree.get("id").and_then(|v| v.as_u64())
156                        && let Some(src) = tree.get("src").and_then(|v| v.as_str())
157                    {
158                        // Check for nameLocation first
159                        let mut name_location = tree
160                            .get("nameLocation")
161                            .and_then(|v| v.as_str())
162                            .map(|s| s.to_string());
163
164                        // Check for nameLocations array and use appropriate element
165                        // For IdentifierPath (qualified names like D.State), use the last element (the actual identifier)
166                        // For other nodes, use the first element
167                        if name_location.is_none()
168                            && let Some(name_locations) = tree.get("nameLocations")
169                            && let Some(locations_array) = name_locations.as_array()
170                            && !locations_array.is_empty()
171                        {
172                            let node_type = tree.get("nodeType").and_then(|v| v.as_str());
173                            if node_type == Some("IdentifierPath") {
174                                name_location = locations_array
175                                    .last()
176                                    .and_then(|v| v.as_str())
177                                    .map(|s| s.to_string());
178                            } else {
179                                name_location = locations_array[0].as_str().map(|s| s.to_string());
180                            }
181                        }
182
183                        let name_locations = if let Some(name_locations) = tree.get("nameLocations")
184                            && let Some(locations_array) = name_locations.as_array()
185                        {
186                            locations_array
187                                .iter()
188                                .filter_map(|v| v.as_str().map(|s| s.to_string()))
189                                .collect()
190                        } else {
191                            vec![]
192                        };
193
194                        let mut final_name_location = name_location;
195                        if final_name_location.is_none()
196                            && let Some(member_loc) =
197                                tree.get("memberLocation").and_then(|v| v.as_str())
198                        {
199                            final_name_location = Some(member_loc.to_string());
200                        }
201
202                        let node_info = NodeInfo {
203                            src: src.to_string(),
204                            name_location: final_name_location,
205                            name_locations,
206                            referenced_declaration: tree
207                                .get("referencedDeclaration")
208                                .and_then(|v| v.as_u64()),
209                            node_type: tree
210                                .get("nodeType")
211                                .and_then(|v| v.as_str())
212                                .map(|s| s.to_string()),
213                            member_location: tree
214                                .get("memberLocation")
215                                .and_then(|v| v.as_str())
216                                .map(|s| s.to_string()),
217                            absolute_path: tree
218                                .get("absolutePath")
219                                .and_then(|v| v.as_str())
220                                .map(|s| s.to_string()),
221                        };
222
223                        nodes.get_mut(&abs_path).unwrap().insert(id, node_info);
224
225                        // Collect externalReferences from InlineAssembly nodes
226                        if tree.get("nodeType").and_then(|v| v.as_str())
227                            == Some("InlineAssembly")
228                        {
229                            if let Some(ext_refs) =
230                                tree.get("externalReferences").and_then(|v| v.as_array())
231                            {
232                                for ext_ref in ext_refs {
233                                    if let Some(src_str) =
234                                        ext_ref.get("src").and_then(|v| v.as_str())
235                                    {
236                                        if let Some(decl_id) =
237                                            ext_ref.get("declaration").and_then(|v| v.as_u64())
238                                        {
239                                            external_refs
240                                                .insert(src_str.to_string(), decl_id);
241                                        }
242                                    }
243                                }
244                            }
245                        }
246                    }
247
248                    for key in CHILD_KEYS {
249                        push_if_node_or_array(tree, key, &mut stack);
250                    }
251                }
252            }
253        }
254    }
255
256    (nodes, path_to_abs, external_refs)
257}
258
259pub fn pos_to_bytes(source_bytes: &[u8], position: Position) -> usize {
260    let text = String::from_utf8_lossy(source_bytes);
261    let lines: Vec<&str> = text.lines().collect();
262
263    let mut byte_offset = 0;
264
265    for (line_num, line_text) in lines.iter().enumerate() {
266        if line_num < position.line as usize {
267            byte_offset += line_text.len() + 1; // +1 for newline
268        } else if line_num == position.line as usize {
269            let char_offset = std::cmp::min(position.character as usize, line_text.len());
270            byte_offset += char_offset;
271            break;
272        }
273    }
274
275    byte_offset
276}
277
278pub fn bytes_to_pos(source_bytes: &[u8], byte_offset: usize) -> Option<Position> {
279    let text = String::from_utf8_lossy(source_bytes);
280    let mut curr_offset = 0;
281
282    for (line_num, line_text) in text.lines().enumerate() {
283        let line_bytes = line_text.len() + 1; // +1 for newline
284        if curr_offset + line_bytes > byte_offset {
285            let col = byte_offset - curr_offset;
286            return Some(Position::new(line_num as u32, col as u32));
287        }
288        curr_offset += line_bytes;
289    }
290
291    None
292}
293
294/// Convert a `"offset:length:fileId"` src string to an LSP Location.
295pub fn src_to_location(
296    src: &str,
297    id_to_path: &HashMap<String, String>,
298) -> Option<Location> {
299    let parts: Vec<&str> = src.split(':').collect();
300    if parts.len() != 3 {
301        return None;
302    }
303    let byte_offset: usize = parts[0].parse().ok()?;
304    let length: usize = parts[1].parse().ok()?;
305    let file_id = parts[2];
306    let file_path = id_to_path.get(file_id)?;
307
308    let absolute_path = if std::path::Path::new(file_path).is_absolute() {
309        std::path::PathBuf::from(file_path)
310    } else {
311        std::env::current_dir().ok()?.join(file_path)
312    };
313
314    let source_bytes = std::fs::read(&absolute_path).ok()?;
315    let start_pos = bytes_to_pos(&source_bytes, byte_offset)?;
316    let end_pos = bytes_to_pos(&source_bytes, byte_offset + length)?;
317    let uri = Url::from_file_path(&absolute_path).ok()?;
318
319    Some(Location {
320        uri,
321        range: Range {
322            start: start_pos,
323            end: end_pos,
324        },
325    })
326}
327
328pub fn goto_bytes(
329    nodes: &HashMap<String, HashMap<u64, NodeInfo>>,
330    path_to_abs: &HashMap<String, String>,
331    id_to_path: &HashMap<String, String>,
332    external_refs: &ExternalRefs,
333    uri: &str,
334    position: usize,
335) -> Option<(String, usize)> {
336    let path = match uri.starts_with("file://") {
337        true => &uri[7..],
338        false => uri,
339    };
340
341    // Get absolute path for this file
342    let abs_path = path_to_abs.get(path)?;
343
344    // Get nodes for the current file only
345    let current_file_nodes = nodes.get(abs_path)?;
346
347    // Build reverse map: file_path -> file_id for filtering external refs by current file
348    let path_to_file_id: HashMap<&str, &str> = id_to_path
349        .iter()
350        .map(|(id, p)| (p.as_str(), id.as_str()))
351        .collect();
352
353    // Determine the file id for the current file
354    // path_to_abs maps filesystem path -> absolutePath (e.g. "src/libraries/SwapMath.sol")
355    // id_to_path maps file_id -> relative path (e.g. "34" -> "src/libraries/SwapMath.sol")
356    let current_file_id = path_to_file_id.get(abs_path.as_str());
357
358    // Check if cursor is on a Yul external reference first
359    for (src_str, decl_id) in external_refs {
360        let src_parts: Vec<&str> = src_str.split(':').collect();
361        if src_parts.len() != 3 {
362            continue;
363        }
364
365        // Only consider external refs in the current file
366        if let Some(file_id) = current_file_id {
367            if src_parts[2] != *file_id {
368                continue;
369            }
370        } else {
371            continue;
372        }
373
374        let start_b: usize = src_parts[0].parse().ok()?;
375        let length: usize = src_parts[1].parse().ok()?;
376        let end_b = start_b + length;
377
378        if start_b <= position && position < end_b {
379            // Found a Yul external reference — resolve to the declaration target
380            let mut target_node: Option<&NodeInfo> = None;
381            for file_nodes in nodes.values() {
382                if let Some(node) = file_nodes.get(decl_id) {
383                    target_node = Some(node);
384                    break;
385                }
386            }
387            let node = target_node?;
388            let (location_str, file_id) = if let Some(name_location) = &node.name_location {
389                let parts: Vec<&str> = name_location.split(':').collect();
390                if parts.len() == 3 {
391                    (parts[0], parts[2])
392                } else {
393                    return None;
394                }
395            } else {
396                let parts: Vec<&str> = node.src.split(':').collect();
397                if parts.len() == 3 {
398                    (parts[0], parts[2])
399                } else {
400                    return None;
401                }
402            };
403            let location: usize = location_str.parse().ok()?;
404            let file_path = id_to_path.get(file_id)?.clone();
405            return Some((file_path, location));
406        }
407    }
408
409    let mut refs = HashMap::new();
410
411    // Only consider nodes from the current file that have references
412    for (id, content) in current_file_nodes {
413        if content.referenced_declaration.is_none() {
414            continue;
415        }
416
417        let src_parts: Vec<&str> = content.src.split(':').collect();
418        if src_parts.len() != 3 {
419            continue;
420        }
421
422        let start_b: usize = src_parts[0].parse().ok()?;
423        let length: usize = src_parts[1].parse().ok()?;
424        let end_b = start_b + length;
425
426        if start_b <= position && position < end_b {
427            let diff = end_b - start_b;
428            if !refs.contains_key(&diff) || refs[&diff] <= *id {
429                refs.insert(diff, *id);
430            }
431        }
432    }
433
434    if refs.is_empty() {
435        // Check if we're on the string part of an import statement
436        // ImportDirective nodes have absolutePath pointing to the imported file
437        let tmp = current_file_nodes.iter();
438        for (_id, content) in tmp {
439            if content.node_type == Some("ImportDirective".to_string()) {
440                let src_parts: Vec<&str> = content.src.split(':').collect();
441                if src_parts.len() != 3 {
442                    continue;
443                }
444
445                let start_b: usize = src_parts[0].parse().ok()?;
446                let length: usize = src_parts[1].parse().ok()?;
447                let end_b = start_b + length;
448
449                if start_b <= position
450                    && position < end_b
451                    && let Some(import_path) = &content.absolute_path
452                {
453                    return Some((import_path.clone(), 0));
454                }
455            }
456        }
457        return None;
458    }
459
460    // Find the reference with minimum diff (most specific)
461    let min_diff = *refs.keys().min()?;
462    let chosen_id = refs[&min_diff];
463    let ref_id = current_file_nodes[&chosen_id].referenced_declaration?;
464
465    // Search for the referenced declaration across all files
466    let mut target_node: Option<&NodeInfo> = None;
467    for file_nodes in nodes.values() {
468        if let Some(node) = file_nodes.get(&ref_id) {
469            target_node = Some(node);
470            break;
471        }
472    }
473
474    let node = target_node?;
475
476    // Get location from nameLocation or src
477    let (location_str, file_id) = if let Some(name_location) = &node.name_location {
478        let parts: Vec<&str> = name_location.split(':').collect();
479        if parts.len() == 3 {
480            (parts[0], parts[2])
481        } else {
482            return None;
483        }
484    } else {
485        let parts: Vec<&str> = node.src.split(':').collect();
486        if parts.len() == 3 {
487            (parts[0], parts[2])
488        } else {
489            return None;
490        }
491    };
492
493    let location: usize = location_str.parse().ok()?;
494    let file_path = id_to_path.get(file_id)?.clone();
495
496    Some((file_path, location))
497}
498
499pub fn goto_declaration(
500    ast_data: &Value,
501    file_uri: &Url,
502    position: Position,
503    source_bytes: &[u8]
504) -> Option<Location> {
505    let sources = ast_data.get("sources")?;
506    let build_infos = ast_data.get("build_infos")?.as_array()?;
507    let first_build_info = build_infos.first()?;
508    let id_to_path = first_build_info.get("source_id_to_path")?.as_object()?;
509
510    let id_to_path_map: HashMap<String, String> = id_to_path
511        .iter()
512        .map(|(k,v)| (k.clone(), v.as_str().unwrap_or("").to_string()))
513        .collect();
514
515    let (nodes, path_to_abs, external_refs) = cache_ids(sources);
516    let byte_position = pos_to_bytes(source_bytes, position);
517
518    if let Some((file_path, location_bytes)) = goto_bytes(
519        &nodes,
520        &path_to_abs,
521        &id_to_path_map,
522        &external_refs,
523        file_uri.as_ref(),
524        byte_position,
525    ) {
526        let target_file_path = std::path::Path::new(&file_path);
527        let absolute_path = if target_file_path.is_absolute() {
528            target_file_path.to_path_buf()
529        } else {
530            std::env::current_dir().ok()?.join(target_file_path)
531        };
532
533        if let Ok(target_source_bytes) = std::fs::read(&absolute_path)
534            && let Some(target_position) = bytes_to_pos(&target_source_bytes, location_bytes)
535            && let Ok(target_uri) = Url::from_file_path(&absolute_path)
536        {
537            return Some(Location {
538                uri: target_uri,
539                range: Range {
540                    start: target_position,
541                    end: target_position,
542                }
543            });
544        }
545
546    };
547
548    None
549
550
551}