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