Skip to main content

solidity_language_server/
goto.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::collections::HashMap;
4use tower_lsp::lsp_types::{Location, Position, Range, Url};
5use tree_sitter::{Node, Parser};
6
7use crate::types::{NodeId, SourceLoc};
8use crate::utils::push_if_node_or_array;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct NodeInfo {
12    pub src: String,
13    pub name_location: Option<String>,
14    pub name_locations: Vec<String>,
15    pub referenced_declaration: Option<NodeId>,
16    pub node_type: Option<String>,
17    pub member_location: Option<String>,
18    pub absolute_path: Option<String>,
19}
20
21/// All AST child keys to traverse (Solidity + Yul).
22pub const CHILD_KEYS: &[&str] = &[
23    "AST",
24    "arguments",
25    "baseContracts",
26    "baseExpression",
27    "baseName",
28    "baseType",
29    "block",
30    "body",
31    "components",
32    "condition",
33    "declarations",
34    "endExpression",
35    "errorCall",
36    "eventCall",
37    "expression",
38    "externalCall",
39    "falseBody",
40    "falseExpression",
41    "file",
42    "foreign",
43    "functionName",
44    "indexExpression",
45    "initialValue",
46    "initializationExpression",
47    "keyType",
48    "leftExpression",
49    "leftHandSide",
50    "libraryName",
51    "literals",
52    "loopExpression",
53    "members",
54    "modifierName",
55    "modifiers",
56    "name",
57    "names",
58    "nodes",
59    "options",
60    "overrides",
61    "parameters",
62    "pathNode",
63    "post",
64    "pre",
65    "returnParameters",
66    "rightExpression",
67    "rightHandSide",
68    "startExpression",
69    "statements",
70    "storageLayout",
71    "subExpression",
72    "subdenomination",
73    "symbolAliases",
74    "trueBody",
75    "trueExpression",
76    "typeName",
77    "unitAlias",
78    "value",
79    "valueType",
80    "variableNames",
81    "variables",
82];
83
84/// Maps `"offset:length:fileId"` src strings from Yul externalReferences
85/// to the Solidity declaration node id they refer to.
86pub type ExternalRefs = HashMap<String, NodeId>;
87
88/// Pre-computed AST index. Built once when an AST enters the cache,
89/// then reused on every goto/references/rename/hover request.
90///
91/// All data from the raw solc JSON is consumed during `new()` into
92/// pre-built indexes. The raw JSON is not retained.
93#[derive(Debug, Clone)]
94pub struct CachedBuild {
95    pub nodes: HashMap<String, HashMap<NodeId, NodeInfo>>,
96    pub path_to_abs: HashMap<String, String>,
97    pub external_refs: ExternalRefs,
98    pub id_to_path_map: HashMap<String, String>,
99    /// O(1) typed declaration node lookup by AST node ID.
100    /// Built from the typed AST via visitor. Contains functions, variables,
101    /// contracts, events, errors, structs, enums, modifiers, and UDVTs.
102    pub decl_index: HashMap<i64, crate::solc_ast::DeclNode>,
103    /// O(1) lookup from any declaration/source-unit node ID to its source file path.
104    /// Built from `typed_ast` during construction. Replaces the O(N)
105    /// `find_source_path_for_node()` that walked raw JSON.
106    pub node_id_to_source_path: HashMap<i64, String>,
107    /// Pre-built gas index from contract output. Built once, reused by
108    /// hover, inlay hints, and code lens.
109    pub gas_index: crate::gas::GasIndex,
110    /// Pre-built hint lookup per file. Built once, reused on every
111    /// inlay hint request (avoids O(n²) declaration resolution per request).
112    pub hint_index: crate::inlay_hints::HintIndex,
113    /// Pre-built documentation index from solc userdoc/devdoc.
114    /// Merged and keyed by selector for fast hover lookup.
115    pub doc_index: crate::hover::DocIndex,
116    /// Pre-built completion cache. Built from sources during construction
117    /// before the sources key is stripped.
118    pub completion_cache: std::sync::Arc<crate::completion::CompletionCache>,
119    /// The text_cache version this build was created from.
120    /// Used to detect dirty files (unsaved edits since last build).
121    pub build_version: i32,
122}
123
124impl CachedBuild {
125    /// Build the index from normalized AST output.
126    ///
127    /// Canonical shape:
128    /// - `sources[path] = { id, ast }`
129    /// - `contracts[path][name] = { abi, evm, ... }`
130    /// - `source_id_to_path = { "0": "path", ... }`
131    pub fn new(ast: Value, build_version: i32) -> Self {
132        let (nodes, path_to_abs, external_refs) = if let Some(sources) = ast.get("sources") {
133            cache_ids(sources)
134        } else {
135            (HashMap::new(), HashMap::new(), HashMap::new())
136        };
137
138        let id_to_path_map = ast
139            .get("source_id_to_path")
140            .and_then(|v| v.as_object())
141            .map(|obj| {
142                obj.iter()
143                    .map(|(k, v)| (k.clone(), v.as_str().unwrap_or("").to_string()))
144                    .collect()
145            })
146            .unwrap_or_default();
147
148        let gas_index = crate::gas::build_gas_index(&ast);
149
150        let doc_index = crate::hover::build_doc_index(&ast);
151
152        // Extract declaration nodes directly from the raw sources JSON.
153        // Instead of deserializing the entire typed AST (SourceUnit, all
154        // expressions, statements, Yul blocks), this walks the raw Value
155        // tree and only deserializes nodes whose nodeType matches one of the
156        // 9 declaration types. Heavy fields (body, modifiers, value, etc.)
157        // are stripped before deserialization.
158        let (decl_index, node_id_to_source_path) = if let Some(sources) = ast.get("sources") {
159            match crate::solc_ast::extract_decl_nodes(sources) {
160                Some(extracted) => (extracted.decl_index, extracted.node_id_to_source_path),
161                None => (HashMap::new(), HashMap::new()),
162            }
163        } else {
164            (HashMap::new(), HashMap::new())
165        };
166
167        // Build constructor index and hint index from the typed decl_index.
168        let constructor_index = crate::inlay_hints::build_constructor_index(&decl_index);
169        let hint_index = if let Some(sources) = ast.get("sources") {
170            crate::inlay_hints::build_hint_index(sources, &decl_index, &constructor_index)
171        } else {
172            HashMap::new()
173        };
174
175        // Build completion cache before stripping sources.
176        let completion_cache = {
177            let sources = ast.get("sources");
178            let contracts = ast.get("contracts");
179            let cc = if let Some(s) = sources {
180                crate::completion::build_completion_cache(s, contracts)
181            } else {
182                crate::completion::build_completion_cache(
183                    &serde_json::Value::Object(Default::default()),
184                    contracts,
185                )
186            };
187            std::sync::Arc::new(cc)
188        };
189
190        // The raw AST JSON is fully consumed — all data has been extracted
191        // into the pre-built indexes above. `ast` is dropped here.
192
193        Self {
194            nodes,
195            path_to_abs,
196            external_refs,
197            id_to_path_map,
198            decl_index,
199            node_id_to_source_path,
200            gas_index,
201            hint_index,
202            doc_index,
203            completion_cache,
204            build_version,
205        }
206    }
207
208    /// Construct a minimal cached build from persisted reference/goto indexes.
209    ///
210    /// This is used for fast startup warm-cache restores where we only need
211    /// cross-file node/reference maps (not full gas/doc/hint indexes).
212    pub fn from_reference_index(
213        nodes: HashMap<String, HashMap<NodeId, NodeInfo>>,
214        path_to_abs: HashMap<String, String>,
215        external_refs: ExternalRefs,
216        id_to_path_map: HashMap<String, String>,
217        build_version: i32,
218    ) -> Self {
219        let completion_cache = std::sync::Arc::new(crate::completion::build_completion_cache(
220            &serde_json::Value::Object(Default::default()),
221            None,
222        ));
223
224        Self {
225            nodes,
226            path_to_abs,
227            external_refs,
228            id_to_path_map,
229            decl_index: HashMap::new(),
230            node_id_to_source_path: HashMap::new(),
231            gas_index: HashMap::new(),
232            hint_index: HashMap::new(),
233            doc_index: HashMap::new(),
234            completion_cache,
235            build_version,
236        }
237    }
238}
239
240/// Return type of [`cache_ids`]: `(nodes, path_to_abs, external_refs)`.
241type CachedIds = (
242    HashMap<String, HashMap<NodeId, NodeInfo>>,
243    HashMap<String, String>,
244    ExternalRefs,
245);
246
247pub fn cache_ids(sources: &Value) -> CachedIds {
248    let source_count = sources.as_object().map_or(0, |obj| obj.len());
249
250    // Pre-size top-level maps based on source file count to avoid rehashing.
251    // Typical project: ~200 nodes/file, ~10 external refs/file.
252    let mut nodes: HashMap<String, HashMap<NodeId, NodeInfo>> =
253        HashMap::with_capacity(source_count);
254    let mut path_to_abs: HashMap<String, String> = HashMap::with_capacity(source_count);
255    let mut external_refs: ExternalRefs = HashMap::with_capacity(source_count * 10);
256
257    if let Some(sources_obj) = sources.as_object() {
258        for (path, source_data) in sources_obj {
259            if let Some(ast) = source_data.get("ast") {
260                // Get the absolute path for this file
261                let abs_path = ast
262                    .get("absolutePath")
263                    .and_then(|v| v.as_str())
264                    .unwrap_or(path)
265                    .to_string();
266
267                path_to_abs.insert(path.clone(), abs_path.clone());
268
269                // Initialize the per-file node map with a size hint.
270                // Use the top-level `nodes` array length as a proxy for total
271                // AST node count (actual count is higher due to nesting, but
272                // this avoids the first few rehashes).
273                let size_hint = ast
274                    .get("nodes")
275                    .and_then(|v| v.as_array())
276                    .map_or(64, |arr| arr.len() * 8);
277                if !nodes.contains_key(&abs_path) {
278                    nodes.insert(abs_path.clone(), HashMap::with_capacity(size_hint));
279                }
280
281                if let Some(id) = ast.get("id").and_then(|v| v.as_u64())
282                    && let Some(src) = ast.get("src").and_then(|v| v.as_str())
283                {
284                    nodes.get_mut(&abs_path).unwrap().insert(
285                        NodeId(id),
286                        NodeInfo {
287                            src: src.to_string(),
288                            name_location: None,
289                            name_locations: vec![],
290                            referenced_declaration: None,
291                            node_type: ast
292                                .get("nodeType")
293                                .and_then(|v| v.as_str())
294                                .map(|s| s.to_string()),
295                            member_location: None,
296                            absolute_path: ast
297                                .get("absolutePath")
298                                .and_then(|v| v.as_str())
299                                .map(|s| s.to_string()),
300                        },
301                    );
302                }
303
304                let mut stack = vec![ast];
305
306                while let Some(tree) = stack.pop() {
307                    if let Some(raw_id) = tree.get("id").and_then(|v| v.as_u64())
308                        && let Some(src) = tree.get("src").and_then(|v| v.as_str())
309                    {
310                        let id = NodeId(raw_id);
311                        // Check for nameLocation first
312                        let mut name_location = tree
313                            .get("nameLocation")
314                            .and_then(|v| v.as_str())
315                            .map(|s| s.to_string());
316
317                        // Check for nameLocations array and use appropriate element
318                        // For IdentifierPath (qualified names like D.State), use the last element (the actual identifier)
319                        // For other nodes, use the first element
320                        if name_location.is_none()
321                            && let Some(name_locations) = tree.get("nameLocations")
322                            && let Some(locations_array) = name_locations.as_array()
323                            && !locations_array.is_empty()
324                        {
325                            let node_type = tree.get("nodeType").and_then(|v| v.as_str());
326                            if node_type == Some("IdentifierPath") {
327                                name_location = locations_array
328                                    .last()
329                                    .and_then(|v| v.as_str())
330                                    .map(|s| s.to_string());
331                            } else {
332                                name_location = locations_array[0].as_str().map(|s| s.to_string());
333                            }
334                        }
335
336                        let name_locations = if let Some(name_locations) = tree.get("nameLocations")
337                            && let Some(locations_array) = name_locations.as_array()
338                        {
339                            locations_array
340                                .iter()
341                                .filter_map(|v| v.as_str().map(|s| s.to_string()))
342                                .collect()
343                        } else {
344                            vec![]
345                        };
346
347                        let mut final_name_location = name_location;
348                        if final_name_location.is_none()
349                            && let Some(member_loc) =
350                                tree.get("memberLocation").and_then(|v| v.as_str())
351                        {
352                            final_name_location = Some(member_loc.to_string());
353                        }
354
355                        let node_info = NodeInfo {
356                            src: src.to_string(),
357                            name_location: final_name_location,
358                            name_locations,
359                            referenced_declaration: tree
360                                .get("referencedDeclaration")
361                                .and_then(|v| v.as_u64())
362                                .map(NodeId),
363                            node_type: tree
364                                .get("nodeType")
365                                .and_then(|v| v.as_str())
366                                .map(|s| s.to_string()),
367                            member_location: tree
368                                .get("memberLocation")
369                                .and_then(|v| v.as_str())
370                                .map(|s| s.to_string()),
371                            absolute_path: tree
372                                .get("absolutePath")
373                                .and_then(|v| v.as_str())
374                                .map(|s| s.to_string()),
375                        };
376
377                        nodes.get_mut(&abs_path).unwrap().insert(id, node_info);
378
379                        // Collect externalReferences from InlineAssembly nodes
380                        if tree.get("nodeType").and_then(|v| v.as_str()) == Some("InlineAssembly")
381                            && let Some(ext_refs) =
382                                tree.get("externalReferences").and_then(|v| v.as_array())
383                        {
384                            for ext_ref in ext_refs {
385                                if let Some(src_str) = ext_ref.get("src").and_then(|v| v.as_str())
386                                    && let Some(decl_id) =
387                                        ext_ref.get("declaration").and_then(|v| v.as_u64())
388                                {
389                                    external_refs.insert(src_str.to_string(), NodeId(decl_id));
390                                }
391                            }
392                        }
393                    }
394
395                    for key in CHILD_KEYS {
396                        push_if_node_or_array(tree, key, &mut stack);
397                    }
398                }
399            }
400        }
401    }
402
403    (nodes, path_to_abs, external_refs)
404}
405
406pub fn pos_to_bytes(source_bytes: &[u8], position: Position) -> usize {
407    let text = String::from_utf8_lossy(source_bytes);
408    crate::utils::position_to_byte_offset(&text, position)
409}
410
411pub fn bytes_to_pos(source_bytes: &[u8], byte_offset: usize) -> Option<Position> {
412    let text = String::from_utf8_lossy(source_bytes);
413    let pos = crate::utils::byte_offset_to_position(&text, byte_offset);
414    Some(pos)
415}
416
417/// Convert a `"offset:length:fileId"` src string to an LSP Location.
418pub fn src_to_location(src: &str, id_to_path: &HashMap<String, String>) -> Option<Location> {
419    let loc = SourceLoc::parse(src)?;
420    let file_path = id_to_path.get(&loc.file_id_str())?;
421
422    let absolute_path = if std::path::Path::new(file_path).is_absolute() {
423        std::path::PathBuf::from(file_path)
424    } else {
425        std::env::current_dir().ok()?.join(file_path)
426    };
427
428    let source_bytes = std::fs::read(&absolute_path).ok()?;
429    let start_pos = bytes_to_pos(&source_bytes, loc.offset)?;
430    let end_pos = bytes_to_pos(&source_bytes, loc.end())?;
431    let uri = Url::from_file_path(&absolute_path).ok()?;
432
433    Some(Location {
434        uri,
435        range: Range {
436            start: start_pos,
437            end: end_pos,
438        },
439    })
440}
441
442pub fn goto_bytes(
443    nodes: &HashMap<String, HashMap<NodeId, NodeInfo>>,
444    path_to_abs: &HashMap<String, String>,
445    id_to_path: &HashMap<String, String>,
446    external_refs: &ExternalRefs,
447    uri: &str,
448    position: usize,
449) -> Option<(String, usize, usize)> {
450    let path = match uri.starts_with("file://") {
451        true => &uri[7..],
452        false => uri,
453    };
454
455    // Get absolute path for this file
456    let abs_path = path_to_abs.get(path)?;
457
458    // Get nodes for the current file only
459    let current_file_nodes = nodes.get(abs_path)?;
460
461    // Build reverse map: file_path -> file_id for filtering external refs by current file
462    let path_to_file_id: HashMap<&str, &str> = id_to_path
463        .iter()
464        .map(|(id, p)| (p.as_str(), id.as_str()))
465        .collect();
466
467    // Determine the file id for the current file
468    // path_to_abs maps filesystem path -> absolutePath (e.g. "src/libraries/SwapMath.sol")
469    // id_to_path maps file_id -> relative path (e.g. "34" -> "src/libraries/SwapMath.sol")
470    let current_file_id = path_to_file_id.get(abs_path.as_str());
471
472    // Check if cursor is on a Yul external reference first
473    for (src_str, decl_id) in external_refs {
474        let Some(src_loc) = SourceLoc::parse(src_str) else {
475            continue;
476        };
477
478        // Only consider external refs in the current file
479        if let Some(file_id) = current_file_id {
480            if src_loc.file_id_str() != *file_id {
481                continue;
482            }
483        } else {
484            continue;
485        }
486
487        if src_loc.offset <= position && position < src_loc.end() {
488            // Found a Yul external reference — resolve to the declaration target
489            let mut target_node: Option<&NodeInfo> = None;
490            for file_nodes in nodes.values() {
491                if let Some(node) = file_nodes.get(decl_id) {
492                    target_node = Some(node);
493                    break;
494                }
495            }
496            let node = target_node?;
497            let loc_str = node.name_location.as_deref().unwrap_or(&node.src);
498            let loc = SourceLoc::parse(loc_str)?;
499            let file_path = id_to_path.get(&loc.file_id_str())?.clone();
500            return Some((file_path, loc.offset, loc.length));
501        }
502    }
503
504    let mut refs = HashMap::new();
505
506    // Only consider nodes from the current file that have references
507    for (id, content) in current_file_nodes {
508        if content.referenced_declaration.is_none() {
509            continue;
510        }
511
512        let Some(src_loc) = SourceLoc::parse(&content.src) else {
513            continue;
514        };
515
516        if src_loc.offset <= position && position < src_loc.end() {
517            let diff = src_loc.length;
518            if !refs.contains_key(&diff) || refs[&diff] <= *id {
519                refs.insert(diff, *id);
520            }
521        }
522    }
523
524    if refs.is_empty() {
525        // Check if we're on the string part of an import statement
526        // ImportDirective nodes have absolutePath pointing to the imported file
527        let tmp = current_file_nodes.iter();
528        for (_id, content) in tmp {
529            if content.node_type == Some("ImportDirective".to_string()) {
530                let Some(src_loc) = SourceLoc::parse(&content.src) else {
531                    continue;
532                };
533
534                if src_loc.offset <= position
535                    && position < src_loc.end()
536                    && let Some(import_path) = &content.absolute_path
537                {
538                    return Some((import_path.clone(), 0, 0));
539                }
540            }
541        }
542        return None;
543    }
544
545    // Find the reference with minimum diff (most specific)
546    let min_diff = *refs.keys().min()?;
547    let chosen_id = refs[&min_diff];
548    let ref_id = current_file_nodes[&chosen_id].referenced_declaration?;
549
550    // Search for the referenced declaration across all files
551    let mut target_node: Option<&NodeInfo> = None;
552    for file_nodes in nodes.values() {
553        if let Some(node) = file_nodes.get(&ref_id) {
554            target_node = Some(node);
555            break;
556        }
557    }
558
559    let node = target_node?;
560
561    // Get location from nameLocation or src
562    let loc_str = node.name_location.as_deref().unwrap_or(&node.src);
563    let loc = SourceLoc::parse(loc_str)?;
564    let file_path = id_to_path.get(&loc.file_id_str())?.clone();
565
566    Some((file_path, loc.offset, loc.length))
567}
568
569/// Go-to-declaration using pre-built `CachedBuild` indices.
570/// Avoids redundant O(N) AST traversal by reusing cached node maps.
571pub fn goto_declaration_cached(
572    build: &CachedBuild,
573    file_uri: &Url,
574    position: Position,
575    source_bytes: &[u8],
576) -> Option<Location> {
577    let byte_position = pos_to_bytes(source_bytes, position);
578
579    if let Some((file_path, location_bytes, length)) = goto_bytes(
580        &build.nodes,
581        &build.path_to_abs,
582        &build.id_to_path_map,
583        &build.external_refs,
584        file_uri.as_ref(),
585        byte_position,
586    ) {
587        let target_file_path = std::path::Path::new(&file_path);
588        let absolute_path = if target_file_path.is_absolute() {
589            target_file_path.to_path_buf()
590        } else {
591            // Resolve relative paths against the current file's directory,
592            // not CWD. This handles solc standard-json output where
593            // absolutePath is relative (e.g. "A.sol") and the server's CWD
594            // differs from the project root.
595            let base = file_uri
596                .to_file_path()
597                .ok()
598                .and_then(|p| p.parent().map(|d| d.to_path_buf()))
599                .or_else(|| std::env::current_dir().ok())
600                .unwrap_or_default();
601            base.join(target_file_path)
602        };
603
604        if let Ok(target_source_bytes) = std::fs::read(&absolute_path)
605            && let Some(start_pos) = bytes_to_pos(&target_source_bytes, location_bytes)
606            && let Some(end_pos) = bytes_to_pos(&target_source_bytes, location_bytes + length)
607            && let Ok(target_uri) = Url::from_file_path(&absolute_path)
608        {
609            return Some(Location {
610                uri: target_uri,
611                range: Range {
612                    start: start_pos,
613                    end: end_pos,
614                },
615            });
616        }
617    };
618
619    None
620}
621
622/// Name-based AST goto — resolves by searching cached AST nodes for identifiers
623/// matching `name` in the current file, then following `referencedDeclaration`.
624///
625/// Unlike `goto_declaration_cached` which matches by byte offset (breaks on dirty files),
626/// this reads the identifier text from the **built source** (on disk) at each node's
627/// `src` range and compares it to the cursor name. Works on dirty files because the
628/// AST node relationships (referencedDeclaration) are still valid — only the byte
629/// offsets in the current buffer are stale.
630/// `byte_hint` is the cursor's byte offset in the dirty buffer, used to pick
631/// the closest matching node when multiple nodes share the same name (overloads).
632pub fn goto_declaration_by_name(
633    cached_build: &CachedBuild,
634    file_uri: &Url,
635    name: &str,
636    byte_hint: usize,
637) -> Option<Location> {
638    let path = match file_uri.as_ref().starts_with("file://") {
639        true => &file_uri.as_ref()[7..],
640        false => file_uri.as_ref(),
641    };
642    let abs_path = cached_build.path_to_abs.get(path)?;
643    // Read the built source from disk to extract identifier text at src ranges
644    let built_source = std::fs::read_to_string(abs_path).ok()?;
645
646    // Collect all matching nodes: (distance_to_hint, span_size, ref_id)
647    let mut candidates: Vec<(usize, usize, NodeId)> = Vec::new();
648
649    let tmp = {
650        let this = cached_build.nodes.get(abs_path)?;
651        this.iter()
652    };
653    for (_id, node) in tmp {
654        let ref_id = match node.referenced_declaration {
655            Some(id) => id,
656            None => continue,
657        };
658
659        // Parse the node's src to get the byte range in the built source
660        let Some(src_loc) = SourceLoc::parse(&node.src) else {
661            continue;
662        };
663        let start = src_loc.offset;
664        let length = src_loc.length;
665
666        if start + length > built_source.len() {
667            continue;
668        }
669
670        let node_text = &built_source[start..start + length];
671
672        // Check if this node's text matches the name we're looking for.
673        // For simple identifiers, the text equals the name directly.
674        // For member access (e.g. `x.toInt128()`), check if the text contains
675        // `.name(` or ends with `.name`.
676        let matches = node_text == name
677            || node_text.contains(&format!(".{name}("))
678            || node_text.ends_with(&format!(".{name}"));
679
680        if matches {
681            // Distance from the byte_hint (cursor in dirty buffer) to the
682            // node's src range. The closest node is most likely the one the
683            // cursor is on, even if byte offsets shifted slightly.
684            let distance = if byte_hint >= start && byte_hint < start + length {
685                0 // cursor is inside this node's range
686            } else if byte_hint < start {
687                start - byte_hint
688            } else {
689                byte_hint - (start + length)
690            };
691            candidates.push((distance, length, ref_id));
692        }
693    }
694
695    // Sort by distance (closest to cursor hint), then by span size (narrowest)
696    candidates.sort_by_key(|&(dist, span, _)| (dist, span));
697    let ref_id = candidates.first()?.2;
698
699    // Find the declaration node across all files
700    let mut target_node: Option<&NodeInfo> = None;
701    for file_nodes in cached_build.nodes.values() {
702        if let Some(node) = file_nodes.get(&ref_id) {
703            target_node = Some(node);
704            break;
705        }
706    }
707
708    let node = target_node?;
709
710    // Parse the target's nameLocation or src
711    let loc_str = node.name_location.as_deref().unwrap_or(&node.src);
712    let loc = SourceLoc::parse(loc_str)?;
713
714    let file_path = cached_build.id_to_path_map.get(&loc.file_id_str())?;
715    let location_bytes = loc.offset;
716    let length = loc.length;
717
718    let target_file_path = std::path::Path::new(file_path);
719    let absolute_path = if target_file_path.is_absolute() {
720        target_file_path.to_path_buf()
721    } else {
722        let base = file_uri
723            .to_file_path()
724            .ok()
725            .and_then(|p| p.parent().map(|d| d.to_path_buf()))
726            .or_else(|| std::env::current_dir().ok())
727            .unwrap_or_default();
728        base.join(target_file_path)
729    };
730
731    let target_source_bytes = std::fs::read(&absolute_path).ok()?;
732    let start_pos = bytes_to_pos(&target_source_bytes, location_bytes)?;
733    let end_pos = bytes_to_pos(&target_source_bytes, location_bytes + length)?;
734    let target_uri = Url::from_file_path(&absolute_path).ok()?;
735
736    Some(Location {
737        uri: target_uri,
738        range: Range {
739            start: start_pos,
740            end: end_pos,
741        },
742    })
743}
744
745// ── Tree-sitter enhanced goto ──────────────────────────────────────────────
746
747/// Context extracted from the cursor position via tree-sitter.
748#[derive(Debug, Clone)]
749pub struct CursorContext {
750    /// The identifier text under the cursor.
751    pub name: String,
752    /// Enclosing function name (if any).
753    pub function: Option<String>,
754    /// Enclosing contract/interface/library name (if any).
755    pub contract: Option<String>,
756    /// Object in a member access expression (e.g. `SqrtPriceMath` in
757    /// `SqrtPriceMath.getAmount0Delta`). Set when the cursor is on the
758    /// property side of a dot expression.
759    pub object: Option<String>,
760    /// Number of arguments at the call site (for overload disambiguation).
761    /// Set when the cursor is on a function name inside a `call_expression`.
762    pub arg_count: Option<usize>,
763    /// Inferred argument types at the call site (e.g. `["uint160", "uint160", "int128"]`).
764    /// `None` entries mean the type couldn't be inferred for that argument.
765    pub arg_types: Vec<Option<String>>,
766}
767
768/// Parse Solidity source with tree-sitter.
769fn ts_parse(source: &str) -> Option<tree_sitter::Tree> {
770    let mut parser = Parser::new();
771    parser
772        .set_language(&tree_sitter_solidity::LANGUAGE.into())
773        .expect("failed to load Solidity grammar");
774    parser.parse(source, None)
775}
776
777/// Validate that the text at a goto target location matches the expected name.
778///
779/// Used to reject tree-sitter results that land on the wrong identifier.
780/// AST results are NOT validated because the AST can legitimately resolve
781/// to a different name (e.g. `.selector` → error declaration).
782pub fn validate_goto_target(target_source: &str, location: &Location, expected_name: &str) -> bool {
783    let line = location.range.start.line as usize;
784    let start_col = location.range.start.character as usize;
785    let end_col = location.range.end.character as usize;
786
787    if let Some(line_text) = target_source.lines().nth(line)
788        && end_col <= line_text.len()
789    {
790        return &line_text[start_col..end_col] == expected_name;
791    }
792    // Can't read target — assume valid
793    true
794}
795
796/// Find the deepest named node at the given byte offset.
797fn ts_node_at_byte(node: Node, byte: usize) -> Option<Node> {
798    if byte < node.start_byte() || byte >= node.end_byte() {
799        return None;
800    }
801    let mut cursor = node.walk();
802    for child in node.children(&mut cursor) {
803        if child.start_byte() <= byte
804            && byte < child.end_byte()
805            && let Some(deeper) = ts_node_at_byte(child, byte)
806        {
807            return Some(deeper);
808        }
809    }
810    Some(node)
811}
812
813/// Get the identifier name from a node (first `identifier` child or the node itself).
814fn ts_child_id_text<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
815    let mut cursor = node.walk();
816    node.children(&mut cursor)
817        .find(|c| c.kind() == "identifier" && c.is_named())
818        .map(|c| &source[c.byte_range()])
819}
820
821/// Infer the type of an expression node using tree-sitter.
822///
823/// For identifiers, walks up to find the variable declaration and extracts its type.
824/// For literals, infers the type from the literal kind.
825/// For function calls, returns None (would need return type resolution).
826fn infer_argument_type<'a>(arg_node: Node<'a>, source: &'a str) -> Option<String> {
827    // Unwrap call_argument → get inner expression
828    let expr = if arg_node.kind() == "call_argument" {
829        let mut c = arg_node.walk();
830        arg_node.children(&mut c).find(|ch| ch.is_named())?
831    } else {
832        arg_node
833    };
834
835    match expr.kind() {
836        "identifier" => {
837            let var_name = &source[expr.byte_range()];
838            // Walk up scopes to find the variable declaration
839            find_variable_type(expr, source, var_name)
840        }
841        "number_literal" | "decimal_number" | "hex_number" => Some("uint256".into()),
842        "boolean_literal" => Some("bool".into()),
843        "string_literal" | "hex_string_literal" => Some("string".into()),
844        _ => None,
845    }
846}
847
848/// Find the type of a variable by searching upward through enclosing scopes.
849///
850/// Looks for `parameter`, `variable_declaration`, and `state_variable_declaration`
851/// nodes whose identifier matches the variable name.
852fn find_variable_type(from: Node, source: &str, var_name: &str) -> Option<String> {
853    let mut scope = from.parent();
854    while let Some(node) = scope {
855        match node.kind() {
856            "function_definition" | "modifier_definition" | "constructor_definition" => {
857                // Check parameters
858                let mut c = node.walk();
859                for child in node.children(&mut c) {
860                    if child.kind() == "parameter"
861                        && let Some(id) = ts_child_id_text(child, source)
862                        && id == var_name
863                    {
864                        // Extract the type from this parameter
865                        let mut pc = child.walk();
866                        return child
867                            .children(&mut pc)
868                            .find(|c| {
869                                matches!(
870                                    c.kind(),
871                                    "type_name"
872                                        | "primitive_type"
873                                        | "user_defined_type"
874                                        | "mapping"
875                                )
876                            })
877                            .map(|t| source[t.byte_range()].trim().to_string());
878                    }
879                }
880            }
881            "function_body" | "block_statement" | "unchecked_block" => {
882                // Check local variable declarations
883                let mut c = node.walk();
884                for child in node.children(&mut c) {
885                    if (child.kind() == "variable_declaration_statement"
886                        || child.kind() == "variable_declaration")
887                        && let Some(id) = ts_child_id_text(child, source)
888                        && id == var_name
889                    {
890                        let mut pc = child.walk();
891                        return child
892                            .children(&mut pc)
893                            .find(|c| {
894                                matches!(
895                                    c.kind(),
896                                    "type_name"
897                                        | "primitive_type"
898                                        | "user_defined_type"
899                                        | "mapping"
900                                )
901                            })
902                            .map(|t| source[t.byte_range()].trim().to_string());
903                    }
904                }
905            }
906            "contract_declaration" | "library_declaration" | "interface_declaration" => {
907                // Check state variables
908                if let Some(body) = ts_find_child(node, "contract_body") {
909                    let mut c = body.walk();
910                    for child in body.children(&mut c) {
911                        if child.kind() == "state_variable_declaration"
912                            && let Some(id) = ts_child_id_text(child, source)
913                            && id == var_name
914                        {
915                            let mut pc = child.walk();
916                            return child
917                                .children(&mut pc)
918                                .find(|c| {
919                                    matches!(
920                                        c.kind(),
921                                        "type_name"
922                                            | "primitive_type"
923                                            | "user_defined_type"
924                                            | "mapping"
925                                    )
926                                })
927                                .map(|t| source[t.byte_range()].trim().to_string());
928                        }
929                    }
930                }
931            }
932            _ => {}
933        }
934        scope = node.parent();
935    }
936    None
937}
938
939/// Infer argument types at a call site by examining each `call_argument` child.
940fn infer_call_arg_types(call_node: Node, source: &str) -> Vec<Option<String>> {
941    let mut cursor = call_node.walk();
942    call_node
943        .children(&mut cursor)
944        .filter(|c| c.kind() == "call_argument")
945        .map(|arg| infer_argument_type(arg, source))
946        .collect()
947}
948
949/// Pick the best overload from multiple declarations based on argument types.
950///
951/// Strategy:
952/// 1. If only one declaration, return it.
953/// 2. Filter by argument count first.
954/// 3. Among count-matched declarations, score by how many argument types match.
955/// 4. Return the highest-scoring declaration.
956fn best_overload<'a>(
957    decls: &'a [TsDeclaration],
958    arg_count: Option<usize>,
959    arg_types: &[Option<String>],
960) -> Option<&'a TsDeclaration> {
961    if decls.len() == 1 {
962        return decls.first();
963    }
964    if decls.is_empty() {
965        return None;
966    }
967
968    // Filter to only function declarations (skip parameters, variables, etc.)
969    let func_decls: Vec<&TsDeclaration> =
970        decls.iter().filter(|d| d.param_count.is_some()).collect();
971
972    if func_decls.is_empty() {
973        return decls.first();
974    }
975
976    // If we have arg_count, filter by it
977    let count_matched: Vec<&&TsDeclaration> = if let Some(ac) = arg_count {
978        let matched: Vec<_> = func_decls
979            .iter()
980            .filter(|d| d.param_count == Some(ac))
981            .collect();
982        if matched.len() == 1 {
983            return Some(matched[0]);
984        }
985        if matched.is_empty() {
986            // No count match — fall back to all
987            func_decls.iter().collect()
988        } else {
989            matched
990        }
991    } else {
992        func_decls.iter().collect()
993    };
994
995    // Score each candidate by how many argument types match parameter types
996    if !arg_types.is_empty() {
997        let mut best: Option<(&TsDeclaration, usize)> = None;
998        for &&decl in &count_matched {
999            let score = arg_types
1000                .iter()
1001                .zip(decl.param_types.iter())
1002                .filter(|(arg_ty, param_ty)| {
1003                    if let Some(at) = arg_ty {
1004                        at == param_ty.as_str()
1005                    } else {
1006                        false
1007                    }
1008                })
1009                .count();
1010            if best.is_none() || score > best.unwrap().1 {
1011                best = Some((decl, score));
1012            }
1013        }
1014        if let Some((decl, _)) = best {
1015            return Some(decl);
1016        }
1017    }
1018
1019    // Fallback: return first count-matched or first overall
1020    count_matched.first().map(|d| **d).or(decls.first())
1021}
1022
1023/// Extract cursor context: the identifier under the cursor and its ancestor names.
1024///
1025/// Walks up the tree-sitter parse tree to find the enclosing function and contract.
1026pub fn cursor_context(source: &str, position: Position) -> Option<CursorContext> {
1027    let tree = ts_parse(source)?;
1028    let byte = pos_to_bytes(source.as_bytes(), position);
1029    let leaf = ts_node_at_byte(tree.root_node(), byte)?;
1030
1031    // The leaf should be an identifier (or we find the nearest identifier)
1032    let id_node = if leaf.kind() == "identifier" {
1033        leaf
1034    } else {
1035        // Check parent — cursor might be just inside a node that contains an identifier
1036        let parent = leaf.parent()?;
1037        if parent.kind() == "identifier" {
1038            parent
1039        } else {
1040            return None;
1041        }
1042    };
1043
1044    let name = source[id_node.byte_range()].to_string();
1045    let mut function = None;
1046    let mut contract = None;
1047
1048    // Detect member access: if the identifier is the `property` side of a
1049    // member_expression (e.g. `SqrtPriceMath.getAmount0Delta`), extract
1050    // the object name so the caller can resolve cross-file.
1051    let object = id_node.parent().and_then(|parent| {
1052        if parent.kind() == "member_expression" {
1053            let prop = parent.child_by_field_name("property")?;
1054            // Only set object when cursor is on the property, not the object side
1055            if prop.id() == id_node.id() {
1056                let obj = parent.child_by_field_name("object")?;
1057                Some(source[obj.byte_range()].to_string())
1058            } else {
1059                None
1060            }
1061        } else {
1062            None
1063        }
1064    });
1065
1066    // Count arguments and infer types at the call site for overload disambiguation.
1067    // Walk up from the identifier to find an enclosing `call_expression`,
1068    // then count its `call_argument` children and infer their types.
1069    let (arg_count, arg_types) = {
1070        let mut node = id_node.parent();
1071        let mut result = (None, vec![]);
1072        while let Some(n) = node {
1073            if n.kind() == "call_expression" {
1074                let types = infer_call_arg_types(n, source);
1075                result = (Some(types.len()), types);
1076                break;
1077            }
1078            node = n.parent();
1079        }
1080        result
1081    };
1082
1083    // Walk ancestors
1084    let mut current = id_node.parent();
1085    while let Some(node) = current {
1086        match node.kind() {
1087            "function_definition" | "modifier_definition" if function.is_none() => {
1088                function = ts_child_id_text(node, source).map(String::from);
1089            }
1090            "constructor_definition" if function.is_none() => {
1091                function = Some("constructor".into());
1092            }
1093            "contract_declaration" | "interface_declaration" | "library_declaration"
1094                if contract.is_none() =>
1095            {
1096                contract = ts_child_id_text(node, source).map(String::from);
1097            }
1098            _ => {}
1099        }
1100        current = node.parent();
1101    }
1102
1103    Some(CursorContext {
1104        name,
1105        function,
1106        contract,
1107        object,
1108        arg_count,
1109        arg_types,
1110    })
1111}
1112
1113/// Information about a declaration found by tree-sitter.
1114#[derive(Debug, Clone)]
1115pub struct TsDeclaration {
1116    /// Position range of the declaration identifier.
1117    pub range: Range,
1118    /// What kind of declaration (contract, function, state_variable, etc.).
1119    pub kind: &'static str,
1120    /// Container name (contract/struct that owns this declaration).
1121    pub container: Option<String>,
1122    /// Number of parameters (for function/modifier declarations).
1123    pub param_count: Option<usize>,
1124    /// Parameter type signature (e.g. `["uint160", "uint160", "int128"]`).
1125    /// Used for overload disambiguation.
1126    pub param_types: Vec<String>,
1127}
1128
1129/// Find all declarations of a name in a source file using tree-sitter.
1130///
1131/// Scans the parse tree for declaration nodes (state variables, functions, events,
1132/// errors, structs, enums, contracts, etc.) whose identifier matches `name`.
1133pub fn find_declarations_by_name(source: &str, name: &str) -> Vec<TsDeclaration> {
1134    let tree = match ts_parse(source) {
1135        Some(t) => t,
1136        None => return vec![],
1137    };
1138    let mut results = Vec::new();
1139    collect_declarations(tree.root_node(), source, name, None, &mut results);
1140    results
1141}
1142
1143fn collect_declarations(
1144    node: Node,
1145    source: &str,
1146    name: &str,
1147    container: Option<&str>,
1148    out: &mut Vec<TsDeclaration>,
1149) {
1150    let mut cursor = node.walk();
1151    for child in node.children(&mut cursor) {
1152        if !child.is_named() {
1153            continue;
1154        }
1155        match child.kind() {
1156            "contract_declaration" | "interface_declaration" | "library_declaration" => {
1157                if let Some(id_name) = ts_child_id_text(child, source) {
1158                    if id_name == name {
1159                        out.push(TsDeclaration {
1160                            range: id_range(child),
1161                            kind: child.kind(),
1162                            container: container.map(String::from),
1163                            param_count: None,
1164                            param_types: vec![],
1165                        });
1166                    }
1167                    // Recurse into contract body
1168                    if let Some(body) = ts_find_child(child, "contract_body") {
1169                        collect_declarations(body, source, name, Some(id_name), out);
1170                    }
1171                }
1172            }
1173            "function_definition" | "modifier_definition" => {
1174                if let Some(id_name) = ts_child_id_text(child, source) {
1175                    if id_name == name {
1176                        let types = parameter_type_signature(child, source);
1177                        out.push(TsDeclaration {
1178                            range: id_range(child),
1179                            kind: child.kind(),
1180                            container: container.map(String::from),
1181                            param_count: Some(types.len()),
1182                            param_types: types.into_iter().map(String::from).collect(),
1183                        });
1184                    }
1185                    // Check function parameters
1186                    collect_parameters(child, source, name, container, out);
1187                    // Recurse into function body for local variables
1188                    if let Some(body) = ts_find_child(child, "function_body") {
1189                        collect_declarations(body, source, name, container, out);
1190                    }
1191                }
1192            }
1193            "constructor_definition" => {
1194                if name == "constructor" {
1195                    let types = parameter_type_signature(child, source);
1196                    out.push(TsDeclaration {
1197                        range: ts_range(child),
1198                        kind: "constructor_definition",
1199                        container: container.map(String::from),
1200                        param_count: Some(types.len()),
1201                        param_types: types.into_iter().map(String::from).collect(),
1202                    });
1203                }
1204                // Check constructor parameters
1205                collect_parameters(child, source, name, container, out);
1206                if let Some(body) = ts_find_child(child, "function_body") {
1207                    collect_declarations(body, source, name, container, out);
1208                }
1209            }
1210            "state_variable_declaration" | "variable_declaration" => {
1211                if let Some(id_name) = ts_child_id_text(child, source)
1212                    && id_name == name
1213                {
1214                    out.push(TsDeclaration {
1215                        range: id_range(child),
1216                        kind: child.kind(),
1217                        container: container.map(String::from),
1218                        param_count: None,
1219                        param_types: vec![],
1220                    });
1221                }
1222            }
1223            "struct_declaration" => {
1224                if let Some(id_name) = ts_child_id_text(child, source) {
1225                    if id_name == name {
1226                        out.push(TsDeclaration {
1227                            range: id_range(child),
1228                            kind: "struct_declaration",
1229                            container: container.map(String::from),
1230                            param_count: None,
1231                            param_types: vec![],
1232                        });
1233                    }
1234                    if let Some(body) = ts_find_child(child, "struct_body") {
1235                        collect_declarations(body, source, name, Some(id_name), out);
1236                    }
1237                }
1238            }
1239            "enum_declaration" => {
1240                if let Some(id_name) = ts_child_id_text(child, source) {
1241                    if id_name == name {
1242                        out.push(TsDeclaration {
1243                            range: id_range(child),
1244                            kind: "enum_declaration",
1245                            container: container.map(String::from),
1246                            param_count: None,
1247                            param_types: vec![],
1248                        });
1249                    }
1250                    // Check enum values
1251                    if let Some(body) = ts_find_child(child, "enum_body") {
1252                        let mut ecur = body.walk();
1253                        for val in body.children(&mut ecur) {
1254                            if val.kind() == "enum_value" && &source[val.byte_range()] == name {
1255                                out.push(TsDeclaration {
1256                                    range: ts_range(val),
1257                                    kind: "enum_value",
1258                                    container: Some(id_name.to_string()),
1259                                    param_count: None,
1260                                    param_types: vec![],
1261                                });
1262                            }
1263                        }
1264                    }
1265                }
1266            }
1267            "event_definition" | "error_declaration" => {
1268                if let Some(id_name) = ts_child_id_text(child, source)
1269                    && id_name == name
1270                {
1271                    out.push(TsDeclaration {
1272                        range: id_range(child),
1273                        kind: child.kind(),
1274                        container: container.map(String::from),
1275                        param_count: None,
1276                        param_types: vec![],
1277                    });
1278                }
1279            }
1280            "user_defined_type_definition" => {
1281                if let Some(id_name) = ts_child_id_text(child, source)
1282                    && id_name == name
1283                {
1284                    out.push(TsDeclaration {
1285                        range: id_range(child),
1286                        kind: "user_defined_type_definition",
1287                        container: container.map(String::from),
1288                        param_count: None,
1289                        param_types: vec![],
1290                    });
1291                }
1292            }
1293            // Recurse into blocks, if-else, loops, etc.
1294            _ => {
1295                collect_declarations(child, source, name, container, out);
1296            }
1297        }
1298    }
1299}
1300
1301/// Extract the type signature from a function's parameters.
1302///
1303/// Returns a list of type strings, e.g. `["uint160", "uint160", "int128"]`.
1304/// For complex types (mappings, arrays, user-defined), returns the full
1305/// text of the type node.
1306fn parameter_type_signature<'a>(node: Node<'a>, source: &'a str) -> Vec<&'a str> {
1307    let mut cursor = node.walk();
1308    node.children(&mut cursor)
1309        .filter(|c| c.kind() == "parameter")
1310        .filter_map(|param| {
1311            let mut pc = param.walk();
1312            param
1313                .children(&mut pc)
1314                .find(|c| {
1315                    matches!(
1316                        c.kind(),
1317                        "type_name" | "primitive_type" | "user_defined_type" | "mapping"
1318                    )
1319                })
1320                .map(|t| source[t.byte_range()].trim())
1321        })
1322        .collect()
1323}
1324
1325/// Collect parameter declarations from a function/constructor node.
1326fn collect_parameters(
1327    node: Node,
1328    source: &str,
1329    name: &str,
1330    container: Option<&str>,
1331    out: &mut Vec<TsDeclaration>,
1332) {
1333    let mut cursor = node.walk();
1334    for child in node.children(&mut cursor) {
1335        if child.kind() == "parameter"
1336            && let Some(id_name) = ts_child_id_text(child, source)
1337            && id_name == name
1338        {
1339            out.push(TsDeclaration {
1340                range: id_range(child),
1341                kind: "parameter",
1342                container: container.map(String::from),
1343                param_count: None,
1344                param_types: vec![],
1345            });
1346        }
1347    }
1348}
1349
1350/// Tree-sitter range helper.
1351fn ts_range(node: Node) -> Range {
1352    let s = node.start_position();
1353    let e = node.end_position();
1354    Range {
1355        start: Position::new(s.row as u32, s.column as u32),
1356        end: Position::new(e.row as u32, e.column as u32),
1357    }
1358}
1359
1360/// Get the range of the identifier child within a declaration node.
1361fn id_range(node: Node) -> Range {
1362    let mut cursor = node.walk();
1363    node.children(&mut cursor)
1364        .find(|c| c.kind() == "identifier" && c.is_named())
1365        .map(|c| ts_range(c))
1366        .unwrap_or_else(|| ts_range(node))
1367}
1368
1369fn ts_find_child<'a>(node: Node<'a>, kind: &str) -> Option<Node<'a>> {
1370    let mut cursor = node.walk();
1371    node.children(&mut cursor).find(|c| c.kind() == kind)
1372}
1373
1374/// Tree-sitter enhanced goto definition.
1375///
1376/// Uses tree-sitter to find the identifier under the cursor and its scope,
1377/// then resolves via the CompletionCache (for cross-file/semantic resolution),
1378/// and finally uses tree-sitter to find the declaration position in the target file.
1379///
1380/// Falls back to None if resolution fails — caller should try the existing AST-based path.
1381pub fn goto_definition_ts(
1382    source: &str,
1383    position: Position,
1384    file_uri: &Url,
1385    completion_cache: &crate::completion::CompletionCache,
1386    text_cache: &HashMap<String, (i32, String)>,
1387) -> Option<Location> {
1388    let ctx = cursor_context(source, position)?;
1389
1390    // Member access: cursor is on `getAmount0Delta` in `SqrtPriceMath.getAmount0Delta`.
1391    // Look up the object (SqrtPriceMath) in the completion cache to find its file,
1392    // then search that file for the member declaration.
1393    // When multiple overloads exist, disambiguate by argument count and types.
1394    if let Some(obj_name) = &ctx.object {
1395        if let Some(path) = find_file_for_contract(completion_cache, obj_name, file_uri) {
1396            let target_source = read_target_source(&path, text_cache)?;
1397            let target_uri = Url::from_file_path(&path).ok()?;
1398            let decls = find_declarations_by_name(&target_source, &ctx.name);
1399            if let Some(d) = best_overload(&decls, ctx.arg_count, &ctx.arg_types) {
1400                return Some(Location {
1401                    uri: target_uri,
1402                    range: d.range,
1403                });
1404            }
1405        }
1406        // Object might be in the same file (e.g. a struct or contract in this file)
1407        let decls = find_declarations_by_name(source, &ctx.name);
1408        if let Some(d) = best_overload(&decls, ctx.arg_count, &ctx.arg_types) {
1409            return Some(Location {
1410                uri: file_uri.clone(),
1411                range: d.range,
1412            });
1413        }
1414    }
1415
1416    // Step 1: Try to resolve via CompletionCache to find which file + name the declaration is in.
1417    // Use the scope chain by names: find the contract scope, then resolve the name.
1418    let resolved = resolve_via_cache(&ctx, file_uri, completion_cache);
1419
1420    match resolved {
1421        Some(ResolvedTarget::SameFile) => {
1422            // Declaration is in the same file — find it with tree-sitter
1423            find_best_declaration(source, &ctx, file_uri)
1424        }
1425        Some(ResolvedTarget::OtherFile { path, name }) => {
1426            // Declaration is in another file — read target source and find by name
1427            let target_source = read_target_source(&path, text_cache);
1428            let target_source = target_source?;
1429            let target_uri = Url::from_file_path(&path).ok()?;
1430            let decls = find_declarations_by_name(&target_source, &name);
1431            decls.first().map(|d| Location {
1432                uri: target_uri,
1433                range: d.range,
1434            })
1435        }
1436        None => {
1437            // CompletionCache couldn't resolve — try same-file tree-sitter lookup as fallback
1438            find_best_declaration(source, &ctx, file_uri)
1439        }
1440    }
1441}
1442
1443#[derive(Debug)]
1444enum ResolvedTarget {
1445    /// Declaration is in the same file as the usage.
1446    SameFile,
1447    /// Declaration is in a different file.
1448    OtherFile { path: String, name: String },
1449}
1450
1451/// Try to resolve an identifier using the CompletionCache.
1452///
1453/// Finds the scope by matching ancestor names (contract, function) against
1454/// the cache's scope data, then resolves the name to a type and traces
1455/// back to the declaring file.
1456fn resolve_via_cache(
1457    ctx: &CursorContext,
1458    file_uri: &Url,
1459    cache: &crate::completion::CompletionCache,
1460) -> Option<ResolvedTarget> {
1461    // Find the contract scope node_id by name
1462    let contract_scope = ctx
1463        .contract
1464        .as_ref()
1465        .and_then(|name| cache.name_to_node_id.get(name.as_str()))
1466        .copied();
1467
1468    // Try scope-based resolution: look in the contract's scope_declarations
1469    if let Some(contract_id) = contract_scope {
1470        // Check function scope if we're inside one
1471        if let Some(func_name) = &ctx.function {
1472            // Find the function scope: look for a scope whose parent is this contract
1473            // and which has a declaration for this function name
1474            if let Some(func_scope_id) = find_function_scope(cache, contract_id, func_name) {
1475                // Check declarations in this function scope first
1476                if let Some(decls) = cache.scope_declarations.get(&func_scope_id)
1477                    && decls.iter().any(|d| d.name == ctx.name)
1478                {
1479                    return Some(ResolvedTarget::SameFile);
1480                }
1481            }
1482        }
1483
1484        // Check contract scope declarations (state variables, functions)
1485        if let Some(decls) = cache.scope_declarations.get(&contract_id)
1486            && decls.iter().any(|d| d.name == ctx.name)
1487        {
1488            return Some(ResolvedTarget::SameFile);
1489        }
1490
1491        // Check inherited contracts (C3 linearization)
1492        if let Some(bases) = cache.linearized_base_contracts.get(&contract_id) {
1493            for &base_id in bases.iter().skip(1) {
1494                if let Some(decls) = cache.scope_declarations.get(&base_id)
1495                    && decls.iter().any(|d| d.name == ctx.name)
1496                {
1497                    // Found in a base contract — find which file it's in
1498                    // Reverse lookup: base_id → contract name → file
1499                    let base_name = cache
1500                        .name_to_node_id
1501                        .iter()
1502                        .find(|&(_, &id)| id == base_id)
1503                        .map(|(name, _)| name.clone());
1504
1505                    if let Some(base_name) = base_name
1506                        && let Some(path) = find_file_for_contract(cache, &base_name, file_uri)
1507                    {
1508                        return Some(ResolvedTarget::OtherFile {
1509                            path,
1510                            name: ctx.name.clone(),
1511                        });
1512                    }
1513                    // Base contract might be in the same file
1514                    return Some(ResolvedTarget::SameFile);
1515                }
1516            }
1517        }
1518    }
1519
1520    // Check if the name is a contract/library/interface name
1521    if cache.name_to_node_id.contains_key(&ctx.name) {
1522        // Could be same file or different file — check if it's in the current file
1523        if let Some(path) = find_file_for_contract(cache, &ctx.name, file_uri) {
1524            let current_path = file_uri.to_file_path().ok()?;
1525            let current_str = current_path.to_str()?;
1526            if path == current_str || path.ends_with(current_str) || current_str.ends_with(&path) {
1527                return Some(ResolvedTarget::SameFile);
1528            }
1529            return Some(ResolvedTarget::OtherFile {
1530                path,
1531                name: ctx.name.clone(),
1532            });
1533        }
1534        return Some(ResolvedTarget::SameFile);
1535    }
1536
1537    // Flat fallback — name_to_type knows about it but we can't determine the file
1538    if cache.name_to_type.contains_key(&ctx.name) {
1539        return Some(ResolvedTarget::SameFile);
1540    }
1541
1542    None
1543}
1544
1545/// Find the scope node_id for a function within a contract.
1546fn find_function_scope(
1547    cache: &crate::completion::CompletionCache,
1548    contract_id: NodeId,
1549    func_name: &str,
1550) -> Option<NodeId> {
1551    // Look for a scope whose parent is the contract and which is a function scope.
1552    // The function name should appear as a declaration in the contract scope,
1553    // and the function's own scope is the one whose parent is the contract.
1554    for (&scope_id, &parent_id) in &cache.scope_parent {
1555        if parent_id == contract_id {
1556            // This scope's parent is our contract — it might be a function scope.
1557            // Check if this scope has declarations (functions/blocks do).
1558            // We also check if the contract declares a function with this name.
1559            if let Some(contract_decls) = cache.scope_declarations.get(&contract_id)
1560                && contract_decls.iter().any(|d| d.name == func_name)
1561            {
1562                // Found a child scope of the contract — could be the function.
1563                // Check if this scope_id has child scopes or declarations
1564                // that match what we'd expect for a function body.
1565                if cache.scope_declarations.contains_key(&scope_id)
1566                    || cache.scope_parent.values().any(|&p| p == scope_id)
1567                {
1568                    return Some(scope_id);
1569                }
1570            }
1571        }
1572    }
1573    None
1574}
1575
1576/// Find the file path for a contract by searching the CompletionCache's path_to_file_id.
1577fn find_file_for_contract(
1578    cache: &crate::completion::CompletionCache,
1579    contract_name: &str,
1580    _file_uri: &Url,
1581) -> Option<String> {
1582    // The completion cache doesn't directly map contract → file.
1583    // But scope_ranges + path_to_file_id can help.
1584    // For now, check if the contract's node_id appears in any scope_range,
1585    // then map file_id back to path.
1586    let node_id = cache.name_to_node_id.get(contract_name)?;
1587    let scope_range = cache.scope_ranges.iter().find(|r| r.node_id == *node_id)?;
1588    let file_id = scope_range.file_id;
1589
1590    // Reverse lookup: file_id → path
1591    cache
1592        .path_to_file_id
1593        .iter()
1594        .find(|&(_, &fid)| fid == file_id)
1595        .map(|(path, _)| path.clone())
1596}
1597
1598/// Read source for a target file — prefer text_cache (open buffers), fallback to disk.
1599fn read_target_source(path: &str, text_cache: &HashMap<String, (i32, String)>) -> Option<String> {
1600    // Try text_cache by URI
1601    let uri = Url::from_file_path(path).ok()?;
1602    if let Some((_, content)) = text_cache.get(&uri.to_string()) {
1603        return Some(content.clone());
1604    }
1605    // Fallback to disk
1606    std::fs::read_to_string(path).ok()
1607}
1608
1609/// Find the best matching declaration in the same file.
1610fn find_best_declaration(source: &str, ctx: &CursorContext, file_uri: &Url) -> Option<Location> {
1611    let decls = find_declarations_by_name(source, &ctx.name);
1612    if decls.is_empty() {
1613        return None;
1614    }
1615
1616    // If there's only one declaration, use it
1617    if decls.len() == 1 {
1618        return Some(Location {
1619            uri: file_uri.clone(),
1620            range: decls[0].range,
1621        });
1622    }
1623
1624    // Multiple declarations — prefer the one in the same contract
1625    if let Some(contract_name) = &ctx.contract
1626        && let Some(d) = decls
1627            .iter()
1628            .find(|d| d.container.as_deref() == Some(contract_name))
1629    {
1630        return Some(Location {
1631            uri: file_uri.clone(),
1632            range: d.range,
1633        });
1634    }
1635
1636    // Fallback: return first declaration
1637    Some(Location {
1638        uri: file_uri.clone(),
1639        range: decls[0].range,
1640    })
1641}
1642
1643#[cfg(test)]
1644mod ts_tests {
1645    use super::*;
1646
1647    #[test]
1648    fn test_cursor_context_state_var() {
1649        let source = r#"
1650contract Token {
1651    uint256 public totalSupply;
1652    function mint(uint256 amount) public {
1653        totalSupply += amount;
1654    }
1655}
1656"#;
1657        // Cursor on `totalSupply` inside mint (line 4, col 8)
1658        let ctx = cursor_context(source, Position::new(4, 8)).unwrap();
1659        assert_eq!(ctx.name, "totalSupply");
1660        assert_eq!(ctx.function.as_deref(), Some("mint"));
1661        assert_eq!(ctx.contract.as_deref(), Some("Token"));
1662    }
1663
1664    #[test]
1665    fn test_cursor_context_top_level() {
1666        let source = r#"
1667contract Foo {}
1668contract Bar {}
1669"#;
1670        // Cursor on `Foo` (line 1, col 9) — the identifier of the contract declaration
1671        let ctx = cursor_context(source, Position::new(1, 9)).unwrap();
1672        assert_eq!(ctx.name, "Foo");
1673        assert!(ctx.function.is_none());
1674        // The identifier `Foo` is a child of contract_declaration, so contract is set
1675        assert_eq!(ctx.contract.as_deref(), Some("Foo"));
1676    }
1677
1678    #[test]
1679    fn test_find_declarations() {
1680        let source = r#"
1681contract Token {
1682    uint256 public totalSupply;
1683    function mint(uint256 amount) public {
1684        totalSupply += amount;
1685    }
1686}
1687"#;
1688        let decls = find_declarations_by_name(source, "totalSupply");
1689        assert_eq!(decls.len(), 1);
1690        assert_eq!(decls[0].kind, "state_variable_declaration");
1691        assert_eq!(decls[0].container.as_deref(), Some("Token"));
1692    }
1693
1694    #[test]
1695    fn test_find_declarations_multiple_contracts() {
1696        let source = r#"
1697contract A {
1698    uint256 public value;
1699}
1700contract B {
1701    uint256 public value;
1702}
1703"#;
1704        let decls = find_declarations_by_name(source, "value");
1705        assert_eq!(decls.len(), 2);
1706        assert_eq!(decls[0].container.as_deref(), Some("A"));
1707        assert_eq!(decls[1].container.as_deref(), Some("B"));
1708    }
1709
1710    #[test]
1711    fn test_find_declarations_enum_value() {
1712        let source = "contract Foo { enum Status { Active, Paused } }";
1713        let decls = find_declarations_by_name(source, "Active");
1714        assert_eq!(decls.len(), 1);
1715        assert_eq!(decls[0].kind, "enum_value");
1716        assert_eq!(decls[0].container.as_deref(), Some("Status"));
1717    }
1718
1719    #[test]
1720    fn test_cursor_context_short_param() {
1721        let source = r#"
1722contract Shop {
1723    uint256 public TAX;
1724    constructor(uint256 price, uint16 tax, uint16 taxBase) {
1725        TAX = tax;
1726    }
1727}
1728"#;
1729        // Cursor on `tax` usage at line 4, col 14 (TAX = tax;)
1730        let ctx = cursor_context(source, Position::new(4, 14)).unwrap();
1731        assert_eq!(ctx.name, "tax");
1732        assert_eq!(ctx.contract.as_deref(), Some("Shop"));
1733
1734        // Cursor on `TAX` at line 4, col 8
1735        let ctx2 = cursor_context(source, Position::new(4, 8)).unwrap();
1736        assert_eq!(ctx2.name, "TAX");
1737
1738        // Parameters are found as declarations
1739        let decls = find_declarations_by_name(source, "tax");
1740        assert_eq!(decls.len(), 1);
1741        assert_eq!(decls[0].kind, "parameter");
1742
1743        let decls_tax_base = find_declarations_by_name(source, "taxBase");
1744        assert_eq!(decls_tax_base.len(), 1);
1745        assert_eq!(decls_tax_base[0].kind, "parameter");
1746
1747        let decls_price = find_declarations_by_name(source, "price");
1748        assert_eq!(decls_price.len(), 1);
1749        assert_eq!(decls_price[0].kind, "parameter");
1750
1751        // State variable is also found
1752        let decls_tax_upper = find_declarations_by_name(source, "TAX");
1753        assert_eq!(decls_tax_upper.len(), 1);
1754        assert_eq!(decls_tax_upper[0].kind, "state_variable_declaration");
1755    }
1756
1757    #[test]
1758    fn test_find_best_declaration_same_contract() {
1759        let source = r#"
1760contract A { uint256 public x; }
1761contract B { uint256 public x; }
1762"#;
1763        let ctx = CursorContext {
1764            name: "x".into(),
1765            function: None,
1766            contract: Some("B".into()),
1767            object: None,
1768            arg_count: None,
1769            arg_types: vec![],
1770        };
1771        let uri = Url::parse("file:///test.sol").unwrap();
1772        let loc = find_best_declaration(source, &ctx, &uri).unwrap();
1773        // Should pick B's x (line 2), not A's x (line 1)
1774        assert_eq!(loc.range.start.line, 2);
1775    }
1776}