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
97/// Pre-computed AST index. Built once when an AST enters the cache,
98/// then reused on every goto/references/rename/hover request.
99#[derive(Debug, Clone)]
100pub struct CachedBuild {
101    pub ast: Value,
102    pub nodes: HashMap<String, HashMap<u64, NodeInfo>>,
103    pub path_to_abs: HashMap<String, String>,
104    pub external_refs: ExternalRefs,
105    pub id_to_path_map: HashMap<String, String>,
106}
107
108impl CachedBuild {
109    /// Build the index from raw `forge build --ast` output.
110    pub fn new(ast: Value) -> Self {
111        let (nodes, path_to_abs, external_refs) = if let Some(sources) = ast.get("sources") {
112            cache_ids(sources)
113        } else {
114            (HashMap::new(), HashMap::new(), HashMap::new())
115        };
116
117        let id_to_path_map = ast
118            .get("build_infos")
119            .and_then(|v| v.as_array())
120            .and_then(|arr| arr.first())
121            .and_then(|info| info.get("source_id_to_path"))
122            .and_then(|v| v.as_object())
123            .map(|obj| {
124                obj.iter()
125                    .map(|(k, v)| (k.clone(), v.as_str().unwrap_or("").to_string()))
126                    .collect()
127            })
128            .unwrap_or_default();
129
130        Self {
131            ast,
132            nodes,
133            path_to_abs,
134            external_refs,
135            id_to_path_map,
136        }
137    }
138}
139
140type Type = (
141    HashMap<String, HashMap<u64, NodeInfo>>,
142    HashMap<String, String>,
143    ExternalRefs,
144);
145
146pub fn cache_ids(sources: &Value) -> Type {
147    let mut nodes: HashMap<String, HashMap<u64, NodeInfo>> = HashMap::new();
148    let mut path_to_abs: HashMap<String, String> = HashMap::new();
149    let mut external_refs: ExternalRefs = HashMap::new();
150
151    if let Some(sources_obj) = sources.as_object() {
152        for (path, contents) in sources_obj {
153            if let Some(contents_array) = contents.as_array()
154                && let Some(first_content) = contents_array.first()
155                && let Some(source_file) = first_content.get("source_file")
156                && let Some(ast) = source_file.get("ast")
157            {
158                // Get the absolute path for this file
159                let abs_path = ast
160                    .get("absolutePath")
161                    .and_then(|v| v.as_str())
162                    .unwrap_or(path)
163                    .to_string();
164
165                path_to_abs.insert(path.clone(), abs_path.clone());
166
167                // Initialize the nodes map for this file
168                if !nodes.contains_key(&abs_path) {
169                    nodes.insert(abs_path.clone(), HashMap::new());
170                }
171
172                if let Some(id) = ast.get("id").and_then(|v| v.as_u64())
173                    && let Some(src) = ast.get("src").and_then(|v| v.as_str())
174                {
175                    nodes.get_mut(&abs_path).unwrap().insert(
176                        id,
177                        NodeInfo {
178                            src: src.to_string(),
179                            name_location: None,
180                            name_locations: vec![],
181                            referenced_declaration: None,
182                            node_type: ast
183                                .get("nodeType")
184                                .and_then(|v| v.as_str())
185                                .map(|s| s.to_string()),
186                            member_location: None,
187                            absolute_path: ast
188                                .get("absolutePath")
189                                .and_then(|v| v.as_str())
190                                .map(|s| s.to_string()),
191                        },
192                    );
193                }
194
195                let mut stack = vec![ast];
196
197                while let Some(tree) = stack.pop() {
198                    if let Some(id) = tree.get("id").and_then(|v| v.as_u64())
199                        && let Some(src) = tree.get("src").and_then(|v| v.as_str())
200                    {
201                        // Check for nameLocation first
202                        let mut name_location = tree
203                            .get("nameLocation")
204                            .and_then(|v| v.as_str())
205                            .map(|s| s.to_string());
206
207                        // Check for nameLocations array and use appropriate element
208                        // For IdentifierPath (qualified names like D.State), use the last element (the actual identifier)
209                        // For other nodes, use the first element
210                        if name_location.is_none()
211                            && let Some(name_locations) = tree.get("nameLocations")
212                            && let Some(locations_array) = name_locations.as_array()
213                            && !locations_array.is_empty()
214                        {
215                            let node_type = tree.get("nodeType").and_then(|v| v.as_str());
216                            if node_type == Some("IdentifierPath") {
217                                name_location = locations_array
218                                    .last()
219                                    .and_then(|v| v.as_str())
220                                    .map(|s| s.to_string());
221                            } else {
222                                name_location = locations_array[0].as_str().map(|s| s.to_string());
223                            }
224                        }
225
226                        let name_locations = if let Some(name_locations) = tree.get("nameLocations")
227                            && let Some(locations_array) = name_locations.as_array()
228                        {
229                            locations_array
230                                .iter()
231                                .filter_map(|v| v.as_str().map(|s| s.to_string()))
232                                .collect()
233                        } else {
234                            vec![]
235                        };
236
237                        let mut final_name_location = name_location;
238                        if final_name_location.is_none()
239                            && let Some(member_loc) =
240                                tree.get("memberLocation").and_then(|v| v.as_str())
241                        {
242                            final_name_location = Some(member_loc.to_string());
243                        }
244
245                        let node_info = NodeInfo {
246                            src: src.to_string(),
247                            name_location: final_name_location,
248                            name_locations,
249                            referenced_declaration: tree
250                                .get("referencedDeclaration")
251                                .and_then(|v| v.as_u64()),
252                            node_type: tree
253                                .get("nodeType")
254                                .and_then(|v| v.as_str())
255                                .map(|s| s.to_string()),
256                            member_location: tree
257                                .get("memberLocation")
258                                .and_then(|v| v.as_str())
259                                .map(|s| s.to_string()),
260                            absolute_path: tree
261                                .get("absolutePath")
262                                .and_then(|v| v.as_str())
263                                .map(|s| s.to_string()),
264                        };
265
266                        nodes.get_mut(&abs_path).unwrap().insert(id, node_info);
267
268                        // Collect externalReferences from InlineAssembly nodes
269                        if tree.get("nodeType").and_then(|v| v.as_str()) == Some("InlineAssembly")
270                            && let Some(ext_refs) =
271                                tree.get("externalReferences").and_then(|v| v.as_array())
272                        {
273                            for ext_ref in ext_refs {
274                                if let Some(src_str) = ext_ref.get("src").and_then(|v| v.as_str())
275                                    && let Some(decl_id) =
276                                        ext_ref.get("declaration").and_then(|v| v.as_u64())
277                                {
278                                    external_refs.insert(src_str.to_string(), decl_id);
279                                }
280                            }
281                        }
282                    }
283
284                    for key in CHILD_KEYS {
285                        push_if_node_or_array(tree, key, &mut stack);
286                    }
287                }
288            }
289        }
290    }
291
292    (nodes, path_to_abs, external_refs)
293}
294
295pub fn pos_to_bytes(source_bytes: &[u8], position: Position) -> usize {
296    let text = String::from_utf8_lossy(source_bytes);
297    crate::utils::position_to_byte_offset(&text, position.line, position.character)
298}
299
300pub fn bytes_to_pos(source_bytes: &[u8], byte_offset: usize) -> Option<Position> {
301    let text = String::from_utf8_lossy(source_bytes);
302    let (line, col) = crate::utils::byte_offset_to_position(&text, byte_offset);
303    Some(Position::new(line, col))
304}
305
306/// Convert a `"offset:length:fileId"` src string to an LSP Location.
307pub fn src_to_location(src: &str, id_to_path: &HashMap<String, String>) -> Option<Location> {
308    let parts: Vec<&str> = src.split(':').collect();
309    if parts.len() != 3 {
310        return None;
311    }
312    let byte_offset: usize = parts[0].parse().ok()?;
313    let length: usize = parts[1].parse().ok()?;
314    let file_id = parts[2];
315    let file_path = id_to_path.get(file_id)?;
316
317    let absolute_path = if std::path::Path::new(file_path).is_absolute() {
318        std::path::PathBuf::from(file_path)
319    } else {
320        std::env::current_dir().ok()?.join(file_path)
321    };
322
323    let source_bytes = std::fs::read(&absolute_path).ok()?;
324    let start_pos = bytes_to_pos(&source_bytes, byte_offset)?;
325    let end_pos = bytes_to_pos(&source_bytes, byte_offset + length)?;
326    let uri = Url::from_file_path(&absolute_path).ok()?;
327
328    Some(Location {
329        uri,
330        range: Range {
331            start: start_pos,
332            end: end_pos,
333        },
334    })
335}
336
337pub fn goto_bytes(
338    nodes: &HashMap<String, HashMap<u64, NodeInfo>>,
339    path_to_abs: &HashMap<String, String>,
340    id_to_path: &HashMap<String, String>,
341    external_refs: &ExternalRefs,
342    uri: &str,
343    position: usize,
344) -> Option<(String, usize, usize)> {
345    let path = match uri.starts_with("file://") {
346        true => &uri[7..],
347        false => uri,
348    };
349
350    // Get absolute path for this file
351    let abs_path = path_to_abs.get(path)?;
352
353    // Get nodes for the current file only
354    let current_file_nodes = nodes.get(abs_path)?;
355
356    // Build reverse map: file_path -> file_id for filtering external refs by current file
357    let path_to_file_id: HashMap<&str, &str> = id_to_path
358        .iter()
359        .map(|(id, p)| (p.as_str(), id.as_str()))
360        .collect();
361
362    // Determine the file id for the current file
363    // path_to_abs maps filesystem path -> absolutePath (e.g. "src/libraries/SwapMath.sol")
364    // id_to_path maps file_id -> relative path (e.g. "34" -> "src/libraries/SwapMath.sol")
365    let current_file_id = path_to_file_id.get(abs_path.as_str());
366
367    // Check if cursor is on a Yul external reference first
368    for (src_str, decl_id) in external_refs {
369        let src_parts: Vec<&str> = src_str.split(':').collect();
370        if src_parts.len() != 3 {
371            continue;
372        }
373
374        // Only consider external refs in the current file
375        if let Some(file_id) = current_file_id {
376            if src_parts[2] != *file_id {
377                continue;
378            }
379        } else {
380            continue;
381        }
382
383        let start_b: usize = src_parts[0].parse().ok()?;
384        let length: usize = src_parts[1].parse().ok()?;
385        let end_b = start_b + length;
386
387        if start_b <= position && position < end_b {
388            // Found a Yul external reference — resolve to the declaration target
389            let mut target_node: Option<&NodeInfo> = None;
390            for file_nodes in nodes.values() {
391                if let Some(node) = file_nodes.get(decl_id) {
392                    target_node = Some(node);
393                    break;
394                }
395            }
396            let node = target_node?;
397            let (location_str, length_str, file_id) =
398                if let Some(name_location) = &node.name_location {
399                    let parts: Vec<&str> = name_location.split(':').collect();
400                    if parts.len() == 3 {
401                        (parts[0], parts[1], parts[2])
402                    } else {
403                        return None;
404                    }
405                } else {
406                    let parts: Vec<&str> = node.src.split(':').collect();
407                    if parts.len() == 3 {
408                        (parts[0], parts[1], parts[2])
409                    } else {
410                        return None;
411                    }
412                };
413            let location: usize = location_str.parse().ok()?;
414            let len: usize = length_str.parse().ok()?;
415            let file_path = id_to_path.get(file_id)?.clone();
416            return Some((file_path, location, len));
417        }
418    }
419
420    let mut refs = HashMap::new();
421
422    // Only consider nodes from the current file that have references
423    for (id, content) in current_file_nodes {
424        if content.referenced_declaration.is_none() {
425            continue;
426        }
427
428        let src_parts: Vec<&str> = content.src.split(':').collect();
429        if src_parts.len() != 3 {
430            continue;
431        }
432
433        let start_b: usize = src_parts[0].parse().ok()?;
434        let length: usize = src_parts[1].parse().ok()?;
435        let end_b = start_b + length;
436
437        if start_b <= position && position < end_b {
438            let diff = end_b - start_b;
439            if !refs.contains_key(&diff) || refs[&diff] <= *id {
440                refs.insert(diff, *id);
441            }
442        }
443    }
444
445    if refs.is_empty() {
446        // Check if we're on the string part of an import statement
447        // ImportDirective nodes have absolutePath pointing to the imported file
448        let tmp = current_file_nodes.iter();
449        for (_id, content) in tmp {
450            if content.node_type == Some("ImportDirective".to_string()) {
451                let src_parts: Vec<&str> = content.src.split(':').collect();
452                if src_parts.len() != 3 {
453                    continue;
454                }
455
456                let start_b: usize = src_parts[0].parse().ok()?;
457                let length: usize = src_parts[1].parse().ok()?;
458                let end_b = start_b + length;
459
460                if start_b <= position
461                    && position < end_b
462                    && let Some(import_path) = &content.absolute_path
463                {
464                    return Some((import_path.clone(), 0, 0));
465                }
466            }
467        }
468        return None;
469    }
470
471    // Find the reference with minimum diff (most specific)
472    let min_diff = *refs.keys().min()?;
473    let chosen_id = refs[&min_diff];
474    let ref_id = current_file_nodes[&chosen_id].referenced_declaration?;
475
476    // Search for the referenced declaration across all files
477    let mut target_node: Option<&NodeInfo> = None;
478    for file_nodes in nodes.values() {
479        if let Some(node) = file_nodes.get(&ref_id) {
480            target_node = Some(node);
481            break;
482        }
483    }
484
485    let node = target_node?;
486
487    // Get location from nameLocation or src
488    let (location_str, length_str, file_id) = if let Some(name_location) = &node.name_location {
489        let parts: Vec<&str> = name_location.split(':').collect();
490        if parts.len() == 3 {
491            (parts[0], parts[1], parts[2])
492        } else {
493            return None;
494        }
495    } else {
496        let parts: Vec<&str> = node.src.split(':').collect();
497        if parts.len() == 3 {
498            (parts[0], parts[1], parts[2])
499        } else {
500            return None;
501        }
502    };
503
504    let location: usize = location_str.parse().ok()?;
505    let len: usize = length_str.parse().ok()?;
506    let file_path = id_to_path.get(file_id)?.clone();
507
508    Some((file_path, location, len))
509}
510
511pub fn goto_declaration(
512    ast_data: &Value,
513    file_uri: &Url,
514    position: Position,
515    source_bytes: &[u8],
516) -> Option<Location> {
517    let sources = ast_data.get("sources")?;
518    let build_infos = ast_data.get("build_infos")?.as_array()?;
519    let first_build_info = build_infos.first()?;
520    let id_to_path = first_build_info.get("source_id_to_path")?.as_object()?;
521
522    let id_to_path_map: HashMap<String, String> = id_to_path
523        .iter()
524        .map(|(k, v)| (k.clone(), v.as_str().unwrap_or("").to_string()))
525        .collect();
526
527    let (nodes, path_to_abs, external_refs) = cache_ids(sources);
528    let byte_position = pos_to_bytes(source_bytes, position);
529
530    if let Some((file_path, location_bytes, length)) = goto_bytes(
531        &nodes,
532        &path_to_abs,
533        &id_to_path_map,
534        &external_refs,
535        file_uri.as_ref(),
536        byte_position,
537    ) {
538        let target_file_path = std::path::Path::new(&file_path);
539        let absolute_path = if target_file_path.is_absolute() {
540            target_file_path.to_path_buf()
541        } else {
542            std::env::current_dir().ok()?.join(target_file_path)
543        };
544
545        if let Ok(target_source_bytes) = std::fs::read(&absolute_path)
546            && let Some(start_pos) = bytes_to_pos(&target_source_bytes, location_bytes)
547            && let Some(end_pos) = bytes_to_pos(&target_source_bytes, location_bytes + length)
548            && let Ok(target_uri) = Url::from_file_path(&absolute_path)
549        {
550            return Some(Location {
551                uri: target_uri,
552                range: Range {
553                    start: start_pos,
554                    end: end_pos,
555                },
556            });
557        }
558    };
559
560    None
561}