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