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, TextEdit, Url};
5use tree_sitter::{Node, Parser};
6
7use crate::types::{
8    AbsPath, FileId, NodeId, PathInterner, RelPath, SolcFileId, SourceLoc, SrcLocation,
9};
10use crate::utils::push_if_node_or_array;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct NodeInfo {
14    pub src: SrcLocation,
15    pub name_location: Option<String>,
16    pub name_locations: Vec<String>,
17    pub referenced_declaration: Option<NodeId>,
18    pub node_type: Option<String>,
19    pub member_location: Option<String>,
20    pub absolute_path: Option<String>,
21    /// The AST `scope` field — the node ID of the containing declaration
22    /// (contract, library, interface, function, etc.). Used to resolve the
23    /// qualifier in qualified type paths like `Pool.State` where `scope`
24    /// on the `State` struct points to the `Pool` library.
25    #[serde(default)]
26    pub scope: Option<NodeId>,
27    /// The AST `baseFunctions` field — node IDs of interface/parent
28    /// function declarations that this function implements or overrides.
29    /// Present on `FunctionDefinition`, `ModifierDefinition`, and
30    /// `VariableDeclaration` nodes. Used to build the bidirectional
31    /// `base_function_implementation` index for interface ↔ implementation
32    /// equivalence in references and call hierarchy.
33    #[serde(default)]
34    pub base_functions: Vec<NodeId>,
35}
36
37/// All AST child keys to traverse (Solidity + Yul).
38pub const CHILD_KEYS: &[&str] = &[
39    "AST",
40    "arguments",
41    "baseContracts",
42    "baseExpression",
43    "baseName",
44    "baseType",
45    "block",
46    "body",
47    "components",
48    "condition",
49    "declarations",
50    "endExpression",
51    "errorCall",
52    "eventCall",
53    "expression",
54    "externalCall",
55    "falseBody",
56    "falseExpression",
57    "file",
58    "foreign",
59    "functionName",
60    "indexExpression",
61    "initialValue",
62    "initializationExpression",
63    "keyType",
64    "leftExpression",
65    "leftHandSide",
66    "libraryName",
67    "literals",
68    "loopExpression",
69    "members",
70    "modifierName",
71    "modifiers",
72    "name",
73    "names",
74    "nodes",
75    "options",
76    "overrides",
77    "parameters",
78    "pathNode",
79    "post",
80    "pre",
81    "returnParameters",
82    "rightExpression",
83    "rightHandSide",
84    "startExpression",
85    "statements",
86    "storageLayout",
87    "subExpression",
88    "subdenomination",
89    "symbolAliases",
90    "trueBody",
91    "trueExpression",
92    "typeName",
93    "unitAlias",
94    "value",
95    "valueType",
96    "variableNames",
97    "variables",
98];
99
100/// Maps `"offset:length:fileId"` src strings from Yul externalReferences
101/// to the Solidity declaration node id they refer to.
102pub type ExternalRefs = HashMap<SrcLocation, NodeId>;
103
104/// Pre-computed AST index. Built once when an AST enters the cache,
105/// then reused on every goto/references/rename/hover request.
106///
107/// All data from the raw solc JSON is consumed during `new()` into
108/// pre-built indexes. The raw JSON is not retained.
109#[derive(Debug, Clone)]
110pub struct CachedBuild {
111    pub nodes: HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
112    pub path_to_abs: HashMap<RelPath, AbsPath>,
113    pub external_refs: ExternalRefs,
114    pub id_to_path_map: HashMap<crate::types::SolcFileId, String>,
115    /// O(1) typed declaration node lookup by AST node ID.
116    /// Built from the typed AST via visitor. Contains functions, variables,
117    /// contracts, events, errors, structs, enums, modifiers, and UDVTs.
118    pub decl_index: HashMap<NodeId, crate::solc_ast::DeclNode>,
119    /// O(1) lookup from any declaration/source-unit node ID to its source file path.
120    /// Built from `typed_ast` during construction. Replaces the O(N)
121    /// `find_source_path_for_node()` that walked raw JSON.
122    pub node_id_to_source_path: HashMap<NodeId, AbsPath>,
123    /// Pre-built hint lookup per file. Built once, reused on every
124    /// inlay hint request (avoids O(n²) declaration resolution per request).
125    pub hint_index: crate::inlay_hints::HintIndex,
126    /// Pre-built documentation index from solc userdoc/devdoc.
127    /// Merged and keyed by selector for fast hover lookup.
128    pub doc_index: crate::hover::DocIndex,
129    /// Pre-built completion cache. Built from sources during construction
130    /// before the sources key is stripped.
131    pub completion_cache: std::sync::Arc<crate::completion::CompletionCache>,
132    /// The text_cache version this build was created from.
133    /// Used to detect dirty files (unsaved edits since last build).
134    pub build_version: i32,
135    /// Qualifier reference index: maps a container declaration ID
136    /// (contract/library/interface) to `IdentifierPath` node IDs that use
137    /// it as a qualifier prefix in qualified type paths (e.g., `Pool.State`).
138    ///
139    /// Built at cache time by following `referencedDeclaration` on multi-segment
140    /// `IdentifierPath` nodes to their declaration, then reading the declaration's
141    /// `scope` field to find the container.
142    pub qualifier_refs: HashMap<NodeId, Vec<NodeId>>,
143    /// Bidirectional implementation index built from `baseFunctions`/`baseModifiers`.
144    ///
145    /// Maps each declaration ID to the set of IDs that are semantically
146    /// equivalent (interface ↔ implementation). For example, if
147    /// `PoolManager.swap` (616) has `baseFunctions: [2036]` (IPoolManager.swap),
148    /// this will contain `616 → [2036]` and `2036 → [616]`.
149    ///
150    /// Used by `textDocument/implementation`, `textDocument/references`, and
151    /// `callHierarchy/incomingCalls` to unify interface and implementation IDs.
152    /// Empty in warm-loaded builds (`from_reference_index`).
153    pub base_function_implementation: HashMap<NodeId, Vec<NodeId>>,
154}
155
156impl CachedBuild {
157    /// Build the index from normalized AST output.
158    ///
159    /// Canonical shape:
160    /// - `sources[path] = { id, ast }`
161    /// - `contracts[path][name] = { evm, devdoc, ... }`
162    /// - `source_id_to_path = { "0": "path", ... }`
163    ///
164    /// When `interner` is provided, solc's per-compilation file IDs in all
165    /// `src` strings are translated into canonical IDs from the project-wide
166    /// [`PathInterner`].  This ensures all `CachedBuild` instances share the
167    /// same file-ID space regardless of which solc invocation produced them.
168    pub fn new(ast: Value, build_version: i32, interner: Option<&mut PathInterner>) -> Self {
169        let (mut nodes, path_to_abs, mut external_refs) = if let Some(sources) = ast.get("sources")
170        {
171            cache_ids(sources)
172        } else {
173            (HashMap::new(), HashMap::new(), HashMap::new())
174        };
175
176        // Parse solc's source_id_to_path from the AST output.
177        let solc_id_to_path: HashMap<SolcFileId, String> = ast
178            .get("source_id_to_path")
179            .and_then(|v| v.as_object())
180            .map(|obj| {
181                obj.iter()
182                    .map(|(k, v)| {
183                        (
184                            SolcFileId::new(k.clone()),
185                            v.as_str().unwrap_or("").to_string(),
186                        )
187                    })
188                    .collect()
189            })
190            .unwrap_or_default();
191
192        // When an interner is available, canonicalize file IDs in all src
193        // strings so that merges across different compilations are safe.
194        // `canonical_remap` is kept around so that `build_completion_cache`
195        // can translate its own file IDs in the same way.
196        let (id_to_path_map, canonical_remap) = if let Some(interner) = interner {
197            let remap = interner.build_remap(&solc_id_to_path);
198
199            // Rewrite all NodeInfo src strings.
200            for file_nodes in nodes.values_mut() {
201                for info in file_nodes.values_mut() {
202                    canonicalize_node_info(info, &remap);
203                }
204            }
205
206            // Rewrite external ref src strings.
207            let old_refs = std::mem::take(&mut external_refs);
208            for (src, decl_id) in old_refs {
209                let new_src = SrcLocation::new(remap_src_canonical(src.as_str(), &remap));
210                external_refs.insert(new_src, decl_id);
211            }
212
213            // Build id_to_path_map from canonical IDs.
214            (interner.to_id_to_path_map(), Some(remap))
215        } else {
216            // No interner — use solc's original mapping (legacy path).
217            (solc_id_to_path, None)
218        };
219
220        let doc_index = crate::hover::build_doc_index(&ast);
221
222        // Extract declaration nodes directly from the raw sources JSON.
223        // Instead of deserializing the entire typed AST (SourceUnit, all
224        // expressions, statements, Yul blocks), this walks the raw Value
225        // tree and only deserializes nodes whose nodeType matches one of the
226        // 9 declaration types. Heavy fields (body, modifiers, value, etc.)
227        // are stripped before deserialization.
228        let (decl_index, node_id_to_source_path) = if let Some(sources) = ast.get("sources") {
229            match crate::solc_ast::extract_decl_nodes(sources) {
230                Some(extracted) => (
231                    extracted
232                        .decl_index
233                        .into_iter()
234                        .map(|(id, decl)| (NodeId(id), decl))
235                        .collect(),
236                    extracted
237                        .node_id_to_source_path
238                        .into_iter()
239                        .map(|(id, path)| (NodeId(id), AbsPath::new(path)))
240                        .collect(),
241                ),
242                None => (HashMap::new(), HashMap::new()),
243            }
244        } else {
245            (HashMap::new(), HashMap::new())
246        };
247
248        // Build constructor index and hint index from the typed decl_index.
249        let constructor_index = crate::inlay_hints::build_constructor_index(&decl_index);
250        let hint_index = if let Some(sources) = ast.get("sources") {
251            crate::inlay_hints::build_hint_index(sources, &decl_index, &constructor_index)
252        } else {
253            HashMap::new()
254        };
255
256        // Build completion cache before stripping sources.
257        let completion_cache = {
258            let sources = ast.get("sources");
259            let contracts = ast.get("contracts");
260            let cc = if let Some(s) = sources {
261                crate::completion::build_completion_cache(s, contracts, canonical_remap.as_ref())
262            } else {
263                crate::completion::build_completion_cache(
264                    &serde_json::Value::Object(Default::default()),
265                    contracts,
266                    canonical_remap.as_ref(),
267                )
268            };
269            std::sync::Arc::new(cc)
270        };
271
272        // Build the qualifier reference index: for each multi-segment
273        // IdentifierPath (e.g., `Pool.State`), follow referencedDeclaration
274        // to the declaration node, then read its `scope` to find the
275        // container (contract/library/interface). Map container_id → [node_id].
276        let qualifier_refs = build_qualifier_refs(&nodes);
277
278        // Build the bidirectional implementation index from base_functions on NodeInfo.
279        // Works uniformly on both fresh and warm-loaded builds since NodeInfo
280        // now persists the base_functions field.
281        let base_function_implementation = build_base_function_implementation(&nodes);
282
283        // The raw AST JSON is fully consumed — all data has been extracted
284        // into the pre-built indexes above. `ast` is dropped here.
285
286        Self {
287            nodes,
288            path_to_abs,
289            external_refs,
290            id_to_path_map,
291            decl_index,
292            node_id_to_source_path,
293            hint_index,
294            doc_index,
295            completion_cache,
296            build_version,
297            qualifier_refs,
298            base_function_implementation,
299        }
300    }
301
302    /// Absorb data from a previous build for files this build doesn't cover.
303    ///
304    /// For each file in `other.nodes` that is **not** already present in
305    /// `self.nodes`, copies the node map, path mapping, and any related
306    /// entries.  This ensures a freshly compiled project index never loses
307    /// coverage compared to the warm-loaded cache it replaces.
308    pub fn merge_missing_from(&mut self, other: &CachedBuild) {
309        for (abs_path, file_nodes) in &other.nodes {
310            if !self.nodes.contains_key(abs_path) {
311                self.nodes.insert(abs_path.clone(), file_nodes.clone());
312            }
313        }
314        for (k, v) in &other.path_to_abs {
315            self.path_to_abs
316                .entry(k.clone())
317                .or_insert_with(|| v.clone());
318        }
319        for (k, v) in &other.external_refs {
320            self.external_refs.entry(k.clone()).or_insert(*v);
321        }
322        for (k, v) in &other.id_to_path_map {
323            self.id_to_path_map
324                .entry(k.clone())
325                .or_insert_with(|| v.clone());
326        }
327        // Merge qualifier_refs: for each container, add any qualifier node
328        // IDs from `other` that aren't already present in `self`.
329        for (container_id, other_qrefs) in &other.qualifier_refs {
330            let entry = self.qualifier_refs.entry(*container_id).or_default();
331            for &qnode_id in other_qrefs {
332                if !entry.contains(&qnode_id) {
333                    entry.push(qnode_id);
334                }
335            }
336        }
337        // Merge base_function_implementation: add any equivalences from `other`
338        // that aren't already present in `self`.
339        for (node_id, other_impls) in &other.base_function_implementation {
340            let entry = self
341                .base_function_implementation
342                .entry(*node_id)
343                .or_default();
344            for &impl_id in other_impls {
345                if !entry.contains(&impl_id) {
346                    entry.push(impl_id);
347                }
348            }
349        }
350    }
351
352    /// Construct a minimal cached build from persisted reference/goto indexes.
353    ///
354    /// This is used for fast startup warm-cache restores where we only need
355    /// cross-file node/reference maps (not full gas/doc/hint indexes).
356    ///
357    /// When `interner` is provided, the `id_to_path_map` entries are
358    /// registered in the interner so that subsequent compilations will
359    /// assign the same canonical IDs to the same paths.
360    pub fn from_reference_index(
361        nodes: HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
362        path_to_abs: HashMap<RelPath, AbsPath>,
363        external_refs: ExternalRefs,
364        id_to_path_map: HashMap<SolcFileId, String>,
365        build_version: i32,
366        interner: Option<&mut PathInterner>,
367    ) -> Self {
368        // Seed the interner with paths from the persisted cache so that
369        // canonical IDs remain consistent across restart.
370        if let Some(interner) = interner {
371            for path in id_to_path_map.values() {
372                interner.intern(path);
373            }
374        }
375
376        let completion_cache = std::sync::Arc::new(crate::completion::build_completion_cache(
377            &serde_json::Value::Object(Default::default()),
378            None,
379            None,
380        ));
381
382        // Build qualifier refs from the warm-loaded nodes.
383        let qualifier_refs = build_qualifier_refs(&nodes);
384
385        // Build base_function_implementation from the warm-loaded nodes.
386        // NodeInfo now persists `base_functions`, so this works on warm loads.
387        let base_function_implementation = build_base_function_implementation(&nodes);
388
389        Self {
390            nodes,
391            path_to_abs,
392            external_refs,
393            id_to_path_map,
394            decl_index: HashMap::new(),
395            node_id_to_source_path: HashMap::new(),
396            hint_index: HashMap::new(),
397            doc_index: HashMap::new(),
398            completion_cache,
399            build_version,
400            qualifier_refs,
401            base_function_implementation,
402        }
403    }
404}
405
406/// Build the qualifier reference index from the cached node maps.
407///
408/// For each `IdentifierPath` node with `nameLocations.len() > 1` (i.e., a
409/// qualified path like `Pool.State`), follows `referencedDeclaration` to the
410/// declaration node, reads its `scope` field (the containing contract /
411/// library / interface), and records `scope_id → [identifierpath_node_id]`.
412///
413/// This allows "Find All References" on a container to include qualified
414/// type references where the container appears as a prefix.
415fn build_qualifier_refs(
416    nodes: &HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
417) -> HashMap<NodeId, Vec<NodeId>> {
418    // First pass: collect all nodes' scope fields into a lookup table.
419    // We need this to look up the scope of a referencedDeclaration target.
420    let mut node_scope: HashMap<NodeId, NodeId> = HashMap::new();
421    for file_nodes in nodes.values() {
422        for (id, info) in file_nodes {
423            if let Some(scope_id) = info.scope {
424                node_scope.insert(*id, scope_id);
425            }
426        }
427    }
428
429    // Second pass: for each multi-segment IdentifierPath, resolve the
430    // container via referencedDeclaration → scope.
431    let mut qualifier_refs: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
432    for file_nodes in nodes.values() {
433        for (id, info) in file_nodes {
434            if info.name_locations.len() > 1 && info.node_type.as_deref() == Some("IdentifierPath")
435            {
436                if let Some(ref_decl) = info.referenced_declaration
437                    && let Some(&scope_id) = node_scope.get(&ref_decl)
438                {
439                    qualifier_refs.entry(scope_id).or_default().push(*id);
440                }
441            }
442        }
443    }
444
445    qualifier_refs
446}
447
448/// Build the bidirectional `base_function_implementation` index from nodes.
449///
450/// Scans all nodes for entries with non-empty `base_functions` (these are
451/// implementing/overriding function declarations). Creates bidirectional
452/// mappings: `impl_id → [base_ids]` (forward) and `base_id → [impl_id]`
453/// (reverse), so lookups work in both directions.
454///
455/// This function works on both fresh and warm-loaded builds because
456/// `base_functions` is persisted as part of `NodeInfo`.
457fn build_base_function_implementation(
458    nodes: &HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
459) -> HashMap<NodeId, Vec<NodeId>> {
460    let mut index: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
461
462    for file_nodes in nodes.values() {
463        for (id, info) in file_nodes {
464            if info.base_functions.is_empty() {
465                continue;
466            }
467            // Forward: impl_id → base_ids
468            for &base_id in &info.base_functions {
469                index.entry(*id).or_default().push(base_id);
470            }
471            // Reverse: base_id → impl_id
472            for &base_id in &info.base_functions {
473                index.entry(base_id).or_default().push(*id);
474            }
475        }
476    }
477
478    // Deduplicate values.
479    for ids in index.values_mut() {
480        ids.sort_unstable();
481        ids.dedup();
482    }
483
484    index
485}
486
487/// Return type of [`cache_ids`]: `(nodes, path_to_abs, external_refs)`.
488type CachedIds = (
489    HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
490    HashMap<RelPath, AbsPath>,
491    ExternalRefs,
492);
493
494/// Rewrite the file-ID component of a `"offset:length:fileId"` string using
495/// a canonical remap table.  Returns the original string unchanged if the
496/// file ID is not in the remap or if the format is invalid.
497pub fn remap_src_canonical(src: &str, remap: &HashMap<u64, FileId>) -> String {
498    let Some(last_colon) = src.rfind(':') else {
499        return src.to_owned();
500    };
501    let old_id_str = &src[last_colon + 1..];
502    let Ok(old_id) = old_id_str.parse::<u64>() else {
503        return src.to_owned();
504    };
505    let Some(canonical) = remap.get(&old_id) else {
506        return src.to_owned();
507    };
508    if canonical.0 == old_id {
509        return src.to_owned();
510    }
511    format!("{}{}", &src[..=last_colon], canonical.0)
512}
513
514/// Rewrite all file-ID references in a [`NodeInfo`] using the canonical
515/// remap table.
516fn canonicalize_node_info(info: &mut NodeInfo, remap: &HashMap<u64, FileId>) {
517    info.src = SrcLocation::new(remap_src_canonical(info.src.as_str(), remap));
518    if let Some(loc) = info.name_location.as_mut() {
519        *loc = remap_src_canonical(loc, remap);
520    }
521    for loc in &mut info.name_locations {
522        *loc = remap_src_canonical(loc, remap);
523    }
524    if let Some(loc) = info.member_location.as_mut() {
525        *loc = remap_src_canonical(loc, remap);
526    }
527}
528
529pub fn cache_ids(sources: &Value) -> CachedIds {
530    let source_count = sources.as_object().map_or(0, |obj| obj.len());
531
532    // Pre-size top-level maps based on source file count to avoid rehashing.
533    // Typical project: ~200 nodes/file, ~10 external refs/file.
534    let mut nodes: HashMap<AbsPath, HashMap<NodeId, NodeInfo>> =
535        HashMap::with_capacity(source_count);
536    let mut path_to_abs: HashMap<RelPath, AbsPath> = HashMap::with_capacity(source_count);
537    let mut external_refs: ExternalRefs = HashMap::with_capacity(source_count * 10);
538
539    if let Some(sources_obj) = sources.as_object() {
540        for (path, source_data) in sources_obj {
541            if let Some(ast) = source_data.get("ast") {
542                // Get the absolute path for this file
543                let abs_path = AbsPath::new(
544                    ast.get("absolutePath")
545                        .and_then(|v| v.as_str())
546                        .unwrap_or(path)
547                        .to_string(),
548                );
549
550                path_to_abs.insert(RelPath::new(path), abs_path.clone());
551
552                // Initialize the per-file node map with a size hint.
553                // Use the top-level `nodes` array length as a proxy for total
554                // AST node count (actual count is higher due to nesting, but
555                // this avoids the first few rehashes).
556                let size_hint = ast
557                    .get("nodes")
558                    .and_then(|v| v.as_array())
559                    .map_or(64, |arr| arr.len() * 8);
560                if !nodes.contains_key(&abs_path) {
561                    nodes.insert(abs_path.clone(), HashMap::with_capacity(size_hint));
562                }
563
564                if let Some(id) = ast.get("id").and_then(|v| v.as_i64())
565                    && let Some(src) = ast.get("src").and_then(|v| v.as_str())
566                {
567                    nodes.get_mut(&abs_path).unwrap().insert(
568                        NodeId(id),
569                        NodeInfo {
570                            src: SrcLocation::new(src),
571                            name_location: None,
572                            name_locations: vec![],
573                            referenced_declaration: None,
574                            node_type: ast
575                                .get("nodeType")
576                                .and_then(|v| v.as_str())
577                                .map(|s| s.to_string()),
578                            member_location: None,
579                            absolute_path: ast
580                                .get("absolutePath")
581                                .and_then(|v| v.as_str())
582                                .map(|s| s.to_string()),
583                            scope: ast.get("scope").and_then(|v| v.as_i64()).map(NodeId),
584                            base_functions: vec![],
585                        },
586                    );
587                }
588
589                let mut stack = vec![ast];
590
591                while let Some(tree) = stack.pop() {
592                    if let Some(raw_id) = tree.get("id").and_then(|v| v.as_i64())
593                        && let Some(src) = tree.get("src").and_then(|v| v.as_str())
594                    {
595                        let id = NodeId(raw_id);
596                        // Check for nameLocation first
597                        let mut name_location = tree
598                            .get("nameLocation")
599                            .and_then(|v| v.as_str())
600                            .map(|s| s.to_string());
601
602                        // Check for nameLocations array and use appropriate element
603                        // For IdentifierPath (qualified names like D.State), use the last element (the actual identifier)
604                        // For other nodes, use the first element
605                        if name_location.is_none()
606                            && let Some(name_locations) = tree.get("nameLocations")
607                            && let Some(locations_array) = name_locations.as_array()
608                            && !locations_array.is_empty()
609                        {
610                            let node_type = tree.get("nodeType").and_then(|v| v.as_str());
611                            if node_type == Some("IdentifierPath") {
612                                name_location = locations_array
613                                    .last()
614                                    .and_then(|v| v.as_str())
615                                    .map(|s| s.to_string());
616                            } else {
617                                name_location = locations_array[0].as_str().map(|s| s.to_string());
618                            }
619                        }
620
621                        let name_locations = if let Some(name_locations) = tree.get("nameLocations")
622                            && let Some(locations_array) = name_locations.as_array()
623                        {
624                            locations_array
625                                .iter()
626                                .filter_map(|v| v.as_str().map(|s| s.to_string()))
627                                .collect()
628                        } else {
629                            vec![]
630                        };
631
632                        let mut final_name_location = name_location;
633                        if final_name_location.is_none()
634                            && let Some(member_loc) =
635                                tree.get("memberLocation").and_then(|v| v.as_str())
636                        {
637                            final_name_location = Some(member_loc.to_string());
638                        }
639
640                        let node_info = NodeInfo {
641                            src: SrcLocation::new(src),
642                            name_location: final_name_location,
643                            name_locations,
644                            referenced_declaration: tree
645                                .get("referencedDeclaration")
646                                .and_then(|v| v.as_i64())
647                                .map(NodeId),
648                            node_type: tree
649                                .get("nodeType")
650                                .and_then(|v| v.as_str())
651                                .map(|s| s.to_string()),
652                            member_location: tree
653                                .get("memberLocation")
654                                .and_then(|v| v.as_str())
655                                .map(|s| s.to_string()),
656                            absolute_path: tree
657                                .get("absolutePath")
658                                .and_then(|v| v.as_str())
659                                .map(|s| s.to_string()),
660                            scope: tree.get("scope").and_then(|v| v.as_i64()).map(NodeId),
661                            base_functions: tree
662                                .get("baseFunctions")
663                                .and_then(|v| v.as_array())
664                                .map(|arr| {
665                                    arr.iter().filter_map(|v| v.as_i64().map(NodeId)).collect()
666                                })
667                                .unwrap_or_default(),
668                        };
669
670                        nodes.get_mut(&abs_path).unwrap().insert(id, node_info);
671
672                        // Collect externalReferences from InlineAssembly nodes
673                        if tree.get("nodeType").and_then(|v| v.as_str()) == Some("InlineAssembly")
674                            && let Some(ext_refs) =
675                                tree.get("externalReferences").and_then(|v| v.as_array())
676                        {
677                            for ext_ref in ext_refs {
678                                if let Some(src_str) = ext_ref.get("src").and_then(|v| v.as_str())
679                                    && let Some(decl_id) =
680                                        ext_ref.get("declaration").and_then(|v| v.as_i64())
681                                {
682                                    external_refs
683                                        .insert(SrcLocation::new(src_str), NodeId(decl_id));
684                                }
685                            }
686                        }
687                    }
688
689                    for key in CHILD_KEYS {
690                        push_if_node_or_array(tree, key, &mut stack);
691                    }
692                }
693            }
694        }
695    }
696
697    (nodes, path_to_abs, external_refs)
698}
699
700pub fn pos_to_bytes(source_bytes: &[u8], position: Position) -> usize {
701    let text = String::from_utf8_lossy(source_bytes);
702    crate::utils::position_to_byte_offset(&text, position)
703}
704
705pub fn bytes_to_pos(source_bytes: &[u8], byte_offset: usize) -> Option<Position> {
706    let text = String::from_utf8_lossy(source_bytes);
707    let pos = crate::utils::byte_offset_to_position(&text, byte_offset);
708    Some(pos)
709}
710
711/// Convert a `"offset:length:fileId"` src string to an LSP Location.
712pub fn src_to_location(
713    src: &str,
714    id_to_path: &HashMap<crate::types::SolcFileId, String>,
715) -> Option<Location> {
716    let loc = SourceLoc::parse(src)?;
717    let file_path = id_to_path.get(&loc.file_id_str())?;
718
719    let absolute_path = if std::path::Path::new(file_path).is_absolute() {
720        std::path::PathBuf::from(file_path)
721    } else {
722        std::env::current_dir().ok()?.join(file_path)
723    };
724
725    let source_bytes = std::fs::read(&absolute_path).ok()?;
726    let start_pos = bytes_to_pos(&source_bytes, loc.offset)?;
727    let end_pos = bytes_to_pos(&source_bytes, loc.end())?;
728    let uri = Url::from_file_path(&absolute_path).ok()?;
729
730    Some(Location {
731        uri,
732        range: Range {
733            start: start_pos,
734            end: end_pos,
735        },
736    })
737}
738
739pub fn goto_bytes(
740    nodes: &HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
741    path_to_abs: &HashMap<RelPath, AbsPath>,
742    id_to_path: &HashMap<crate::types::SolcFileId, String>,
743    external_refs: &ExternalRefs,
744    uri: &str,
745    position: usize,
746) -> Option<(String, usize, usize)> {
747    let path = match uri.starts_with("file://") {
748        true => &uri[7..],
749        false => uri,
750    };
751
752    // Get absolute path for this file
753    let abs_path = path_to_abs.get(path)?;
754
755    // Get nodes for the current file only
756    let current_file_nodes = nodes.get(abs_path)?;
757
758    // Build reverse map: file_path -> file_id for filtering external refs by current file
759    let path_to_file_id: HashMap<&str, &crate::types::SolcFileId> =
760        id_to_path.iter().map(|(id, p)| (p.as_str(), id)).collect();
761
762    // Determine the file id for the current file
763    // path_to_abs maps filesystem path -> absolutePath (e.g. "src/libraries/SwapMath.sol")
764    // id_to_path maps file_id -> relative path (e.g. "34" -> "src/libraries/SwapMath.sol")
765    let current_file_id = path_to_file_id.get(abs_path.as_str());
766
767    // Check if cursor is on a Yul external reference first
768    for (src_str, decl_id) in external_refs {
769        let Some(src_loc) = SourceLoc::parse(src_str.as_str()) else {
770            continue;
771        };
772
773        // Only consider external refs in the current file
774        if let Some(file_id) = current_file_id {
775            if src_loc.file_id_str() != **file_id {
776                continue;
777            }
778        } else {
779            continue;
780        }
781
782        if src_loc.offset <= position && position < src_loc.end() {
783            // Found a Yul external reference — resolve to the declaration target
784            let mut target_node: Option<&NodeInfo> = None;
785            for file_nodes in nodes.values() {
786                if let Some(node) = file_nodes.get(decl_id) {
787                    target_node = Some(node);
788                    break;
789                }
790            }
791            let node = target_node?;
792            let loc_str = node.name_location.as_deref().unwrap_or(node.src.as_str());
793            let loc = SourceLoc::parse(loc_str)?;
794            let file_path = id_to_path.get(&loc.file_id_str())?.clone();
795            return Some((file_path, loc.offset, loc.length));
796        }
797    }
798
799    let mut refs = HashMap::new();
800
801    // Only consider nodes from the current file that have references
802    for (id, content) in current_file_nodes {
803        if content.referenced_declaration.is_none() {
804            continue;
805        }
806
807        let Some(src_loc) = SourceLoc::parse(content.src.as_str()) else {
808            continue;
809        };
810
811        if src_loc.offset <= position && position < src_loc.end() {
812            let diff = src_loc.length;
813            if !refs.contains_key(&diff) || refs[&diff] <= *id {
814                refs.insert(diff, *id);
815            }
816        }
817    }
818
819    if refs.is_empty() {
820        // Check if we're on the string part of an import statement
821        // ImportDirective nodes have absolutePath pointing to the imported file
822        let tmp = current_file_nodes.iter();
823        for (_id, content) in tmp {
824            if content.node_type == Some("ImportDirective".to_string()) {
825                let Some(src_loc) = SourceLoc::parse(content.src.as_str()) else {
826                    continue;
827                };
828
829                if src_loc.offset <= position
830                    && position < src_loc.end()
831                    && let Some(import_path) = &content.absolute_path
832                {
833                    return Some((import_path.clone(), 0, 0));
834                }
835            }
836        }
837        return None;
838    }
839
840    // Find the reference with minimum diff (most specific)
841    let min_diff = *refs.keys().min()?;
842    let chosen_id = refs[&min_diff];
843    let ref_id = current_file_nodes[&chosen_id].referenced_declaration?;
844
845    // Search for the referenced declaration across all files
846    let mut target_node: Option<&NodeInfo> = None;
847    for file_nodes in nodes.values() {
848        if let Some(node) = file_nodes.get(&ref_id) {
849            target_node = Some(node);
850            break;
851        }
852    }
853
854    let node = target_node?;
855
856    // Get location from nameLocation or src
857    let loc_str = node.name_location.as_deref().unwrap_or(node.src.as_str());
858    let loc = SourceLoc::parse(loc_str)?;
859    let file_path = id_to_path.get(&loc.file_id_str())?.clone();
860
861    Some((file_path, loc.offset, loc.length))
862}
863
864/// Check if cursor is on the qualifier segment (first `nameLocations` entry)
865/// of a multi-segment `IdentifierPath` (e.g., `Pool` in `Pool.State`).
866/// If so, resolve the container declaration via `referencedDeclaration → scope`
867/// and return a Location pointing to the container's name.
868fn resolve_qualifier_goto(
869    build: &CachedBuild,
870    file_uri: &Url,
871    byte_position: usize,
872) -> Option<Location> {
873    let path = file_uri.to_file_path().ok()?;
874    let path_str = path.to_str()?;
875    let abs_path = build.path_to_abs.get(path_str)?;
876    let file_nodes = build.nodes.get(abs_path)?;
877
878    // Find the IdentifierPath node under the cursor.
879    let node_id = crate::references::byte_to_id(&build.nodes, abs_path, byte_position)?;
880    let node_info = file_nodes.get(&node_id)?;
881
882    // Must be a multi-segment IdentifierPath.
883    if node_info.node_type.as_deref() != Some("IdentifierPath")
884        || node_info.name_locations.len() <= 1
885    {
886        return None;
887    }
888
889    // Check if cursor is on the first segment (the qualifier).
890    let first_loc = SourceLoc::parse(&node_info.name_locations[0])?;
891    if byte_position < first_loc.offset || byte_position >= first_loc.end() {
892        return None;
893    }
894
895    // Follow referencedDeclaration to the declaration node, then read scope.
896    let ref_decl_id = node_info.referenced_declaration?;
897    // Find the declaration node to get its scope.
898    let decl_node = find_node_info(&build.nodes, ref_decl_id)?;
899    let scope_id = decl_node.scope?;
900
901    // Resolve the container declaration's location.
902    let container_node = find_node_info(&build.nodes, scope_id)?;
903    let loc_str = container_node
904        .name_location
905        .as_deref()
906        .unwrap_or(container_node.src.as_str());
907    let loc = SourceLoc::parse(loc_str)?;
908    let file_path = build.id_to_path_map.get(&loc.file_id_str())?;
909
910    let absolute_path = if std::path::Path::new(file_path).is_absolute() {
911        std::path::PathBuf::from(file_path)
912    } else {
913        std::env::current_dir().ok()?.join(file_path)
914    };
915
916    let target_bytes = std::fs::read(&absolute_path).ok()?;
917    let start_pos = bytes_to_pos(&target_bytes, loc.offset)?;
918    let end_pos = bytes_to_pos(&target_bytes, loc.end())?;
919    let target_uri = Url::from_file_path(&absolute_path).ok()?;
920
921    Some(Location {
922        uri: target_uri,
923        range: Range {
924            start: start_pos,
925            end: end_pos,
926        },
927    })
928}
929
930/// Find a `NodeInfo` by node ID across all files.
931fn find_node_info<'a>(
932    nodes: &'a HashMap<AbsPath, HashMap<NodeId, NodeInfo>>,
933    node_id: NodeId,
934) -> Option<&'a NodeInfo> {
935    for file_nodes in nodes.values() {
936        if let Some(node) = file_nodes.get(&node_id) {
937            return Some(node);
938        }
939    }
940    None
941}
942
943/// Go-to-declaration using pre-built `CachedBuild` indices.
944/// Avoids redundant O(N) AST traversal by reusing cached node maps.
945pub fn goto_declaration_cached(
946    build: &CachedBuild,
947    file_uri: &Url,
948    position: Position,
949    source_bytes: &[u8],
950) -> Option<Location> {
951    let byte_position = pos_to_bytes(source_bytes, position);
952
953    // Check if cursor is on the qualifier segment of a multi-segment
954    // IdentifierPath (e.g., cursor on `Pool` in `Pool.State`).
955    // If so, navigate to the container declaration (via scope) instead
956    // of the struct/enum that referencedDeclaration points to.
957    if let Some(location) = resolve_qualifier_goto(build, file_uri, byte_position) {
958        return Some(location);
959    }
960
961    if let Some((file_path, location_bytes, length)) = goto_bytes(
962        &build.nodes,
963        &build.path_to_abs,
964        &build.id_to_path_map,
965        &build.external_refs,
966        file_uri.as_ref(),
967        byte_position,
968    ) {
969        let target_file_path = std::path::Path::new(&file_path);
970        let absolute_path = if target_file_path.is_absolute() {
971            target_file_path.to_path_buf()
972        } else {
973            // Resolve relative paths against the current file's directory,
974            // not CWD. This handles solc standard-json output where
975            // absolutePath is relative (e.g. "A.sol") and the server's CWD
976            // differs from the project root.
977            let base = file_uri
978                .to_file_path()
979                .ok()
980                .and_then(|p| p.parent().map(|d| d.to_path_buf()))
981                .or_else(|| std::env::current_dir().ok())
982                .unwrap_or_default();
983            base.join(target_file_path)
984        };
985
986        if let Ok(target_source_bytes) = std::fs::read(&absolute_path)
987            && let Some(start_pos) = bytes_to_pos(&target_source_bytes, location_bytes)
988            && let Some(end_pos) = bytes_to_pos(&target_source_bytes, location_bytes + length)
989            && let Ok(target_uri) = Url::from_file_path(&absolute_path)
990        {
991            return Some(Location {
992                uri: target_uri,
993                range: Range {
994                    start: start_pos,
995                    end: end_pos,
996                },
997            });
998        }
999    };
1000
1001    None
1002}
1003
1004/// Name-based AST goto — resolves by searching cached AST nodes for identifiers
1005/// matching `name` in the current file, then following `referencedDeclaration`.
1006///
1007/// Unlike `goto_declaration_cached` which matches by byte offset (breaks on dirty files),
1008/// this reads the identifier text from the **built source** (on disk) at each node's
1009/// `src` range and compares it to the cursor name. Works on dirty files because the
1010/// AST node relationships (referencedDeclaration) are still valid — only the byte
1011/// offsets in the current buffer are stale.
1012/// `byte_hint` is the cursor's byte offset in the dirty buffer, used to pick
1013/// the closest matching node when multiple nodes share the same name (overloads).
1014pub fn goto_declaration_by_name(
1015    cached_build: &CachedBuild,
1016    file_uri: &Url,
1017    name: &str,
1018    byte_hint: usize,
1019) -> Option<Location> {
1020    let path = match file_uri.as_ref().starts_with("file://") {
1021        true => &file_uri.as_ref()[7..],
1022        false => file_uri.as_ref(),
1023    };
1024    let abs_path = cached_build.path_to_abs.get(path)?;
1025    // Read the built source from disk to extract identifier text at src ranges
1026    let built_source = std::fs::read_to_string(abs_path).ok()?;
1027
1028    // Collect all matching nodes: (distance_to_hint, span_size, ref_id)
1029    let mut candidates: Vec<(usize, usize, NodeId)> = Vec::new();
1030
1031    let tmp = {
1032        let this = cached_build.nodes.get(abs_path)?;
1033        this.iter()
1034    };
1035    for (_id, node) in tmp {
1036        let ref_id = match node.referenced_declaration {
1037            Some(id) => id,
1038            None => continue,
1039        };
1040
1041        // Parse the node's src to get the byte range in the built source
1042        let Some(src_loc) = SourceLoc::parse(node.src.as_str()) else {
1043            continue;
1044        };
1045        let start = src_loc.offset;
1046        let length = src_loc.length;
1047
1048        if start + length > built_source.len() {
1049            continue;
1050        }
1051
1052        let node_text = &built_source[start..start + length];
1053
1054        // Check if this node's text matches the name we're looking for.
1055        // For simple identifiers, the text equals the name directly.
1056        // For member access (e.g. `x.toInt128()`), check if the text contains
1057        // `.name(` or ends with `.name`.
1058        let matches = node_text == name
1059            || node_text.contains(&format!(".{name}("))
1060            || node_text.ends_with(&format!(".{name}"));
1061
1062        if matches {
1063            // Distance from the byte_hint (cursor in dirty buffer) to the
1064            // node's src range. The closest node is most likely the one the
1065            // cursor is on, even if byte offsets shifted slightly.
1066            let distance = if byte_hint >= start && byte_hint < start + length {
1067                0 // cursor is inside this node's range
1068            } else if byte_hint < start {
1069                start - byte_hint
1070            } else {
1071                byte_hint - (start + length)
1072            };
1073            candidates.push((distance, length, ref_id));
1074        }
1075    }
1076
1077    // Sort by distance (closest to cursor hint), then by span size (narrowest)
1078    candidates.sort_by_key(|&(dist, span, _)| (dist, span));
1079    let ref_id = candidates.first()?.2;
1080
1081    // Find the declaration node across all files
1082    let mut target_node: Option<&NodeInfo> = None;
1083    for file_nodes in cached_build.nodes.values() {
1084        if let Some(node) = file_nodes.get(&ref_id) {
1085            target_node = Some(node);
1086            break;
1087        }
1088    }
1089
1090    let node = target_node?;
1091
1092    // Parse the target's nameLocation or src
1093    let loc_str = node.name_location.as_deref().unwrap_or(node.src.as_str());
1094    let loc = SourceLoc::parse(loc_str)?;
1095
1096    let file_path = cached_build.id_to_path_map.get(&loc.file_id_str())?;
1097    let location_bytes = loc.offset;
1098    let length = loc.length;
1099
1100    let target_file_path = std::path::Path::new(file_path);
1101    let absolute_path = if target_file_path.is_absolute() {
1102        target_file_path.to_path_buf()
1103    } else {
1104        let base = file_uri
1105            .to_file_path()
1106            .ok()
1107            .and_then(|p| p.parent().map(|d| d.to_path_buf()))
1108            .or_else(|| std::env::current_dir().ok())
1109            .unwrap_or_default();
1110        base.join(target_file_path)
1111    };
1112
1113    let target_source_bytes = std::fs::read(&absolute_path).ok()?;
1114    let start_pos = bytes_to_pos(&target_source_bytes, location_bytes)?;
1115    let end_pos = bytes_to_pos(&target_source_bytes, location_bytes + length)?;
1116    let target_uri = Url::from_file_path(&absolute_path).ok()?;
1117
1118    Some(Location {
1119        uri: target_uri,
1120        range: Range {
1121            start: start_pos,
1122            end: end_pos,
1123        },
1124    })
1125}
1126
1127// ── Tree-sitter enhanced goto ──────────────────────────────────────────────
1128
1129/// Context extracted from the cursor position via tree-sitter.
1130#[derive(Debug, Clone)]
1131pub struct CursorContext {
1132    /// The identifier text under the cursor.
1133    pub name: String,
1134    /// Enclosing function name (if any).
1135    pub function: Option<String>,
1136    /// Enclosing contract/interface/library name (if any).
1137    pub contract: Option<String>,
1138    /// Object in a member access expression (e.g. `SqrtPriceMath` in
1139    /// `SqrtPriceMath.getAmount0Delta`). Set when the cursor is on the
1140    /// property side of a dot expression.
1141    pub object: Option<String>,
1142    /// Number of arguments at the call site (for overload disambiguation).
1143    /// Set when the cursor is on a function name inside a `call_expression`.
1144    pub arg_count: Option<usize>,
1145    /// Inferred argument types at the call site (e.g. `["uint160", "uint160", "int128"]`).
1146    /// `None` entries mean the type couldn't be inferred for that argument.
1147    pub arg_types: Vec<Option<String>>,
1148}
1149
1150/// Parse Solidity source with tree-sitter.
1151fn ts_parse(source: &str) -> Option<tree_sitter::Tree> {
1152    let mut parser = Parser::new();
1153    parser
1154        .set_language(&tree_sitter_solidity::LANGUAGE.into())
1155        .expect("failed to load Solidity grammar");
1156    parser.parse(source, None)
1157}
1158
1159/// Validate that the text at a goto target location matches the expected name.
1160///
1161/// Used to reject tree-sitter results that land on the wrong identifier.
1162/// AST results are NOT validated because the AST can legitimately resolve
1163/// to a different name (e.g. `.selector` → error declaration).
1164pub fn validate_goto_target(target_source: &str, location: &Location, expected_name: &str) -> bool {
1165    let line = location.range.start.line as usize;
1166    let start_col = location.range.start.character as usize;
1167    let end_col = location.range.end.character as usize;
1168
1169    if let Some(line_text) = target_source.lines().nth(line)
1170        && end_col <= line_text.len()
1171    {
1172        return &line_text[start_col..end_col] == expected_name;
1173    }
1174    // Can't read target — assume valid
1175    true
1176}
1177
1178/// Find the deepest named node at the given byte offset.
1179fn ts_node_at_byte(node: Node, byte: usize) -> Option<Node> {
1180    if byte < node.start_byte() || byte >= node.end_byte() {
1181        return None;
1182    }
1183    let mut cursor = node.walk();
1184    for child in node.children(&mut cursor) {
1185        if child.start_byte() <= byte
1186            && byte < child.end_byte()
1187            && let Some(deeper) = ts_node_at_byte(child, byte)
1188        {
1189            return Some(deeper);
1190        }
1191    }
1192    Some(node)
1193}
1194
1195/// Get the identifier name from a node (first `identifier` child or the node itself).
1196fn ts_child_id_text<'a>(node: Node<'a>, source: &'a str) -> Option<&'a str> {
1197    let mut cursor = node.walk();
1198    node.children(&mut cursor)
1199        .find(|c| c.kind() == "identifier" && c.is_named())
1200        .map(|c| &source[c.byte_range()])
1201}
1202
1203/// Infer the type of an expression node using tree-sitter.
1204///
1205/// For identifiers, walks up to find the variable declaration and extracts its type.
1206/// For literals, infers the type from the literal kind.
1207/// For function calls, returns None (would need return type resolution).
1208fn infer_argument_type<'a>(arg_node: Node<'a>, source: &'a str) -> Option<String> {
1209    // Unwrap call_argument → get inner expression
1210    let expr = if arg_node.kind() == "call_argument" {
1211        let mut c = arg_node.walk();
1212        arg_node.children(&mut c).find(|ch| ch.is_named())?
1213    } else {
1214        arg_node
1215    };
1216
1217    match expr.kind() {
1218        "identifier" => {
1219            let var_name = &source[expr.byte_range()];
1220            // Walk up scopes to find the variable declaration
1221            find_variable_type(expr, source, var_name)
1222        }
1223        "number_literal" | "decimal_number" | "hex_number" => Some("uint256".into()),
1224        "boolean_literal" => Some("bool".into()),
1225        "string_literal" | "hex_string_literal" => Some("string".into()),
1226        _ => None,
1227    }
1228}
1229
1230/// Find the type of a variable by searching upward through enclosing scopes.
1231///
1232/// Looks for `parameter`, `variable_declaration`, and `state_variable_declaration`
1233/// nodes whose identifier matches the variable name.
1234fn find_variable_type(from: Node, source: &str, var_name: &str) -> Option<String> {
1235    let mut scope = from.parent();
1236    while let Some(node) = scope {
1237        match node.kind() {
1238            "function_definition" | "modifier_definition" | "constructor_definition" => {
1239                // Check parameters
1240                let mut c = node.walk();
1241                for child in node.children(&mut c) {
1242                    if child.kind() == "parameter"
1243                        && let Some(id) = ts_child_id_text(child, source)
1244                        && id == var_name
1245                    {
1246                        // Extract the type from this parameter
1247                        let mut pc = child.walk();
1248                        return child
1249                            .children(&mut pc)
1250                            .find(|c| {
1251                                matches!(
1252                                    c.kind(),
1253                                    "type_name"
1254                                        | "primitive_type"
1255                                        | "user_defined_type"
1256                                        | "mapping"
1257                                )
1258                            })
1259                            .map(|t| source[t.byte_range()].trim().to_string());
1260                    }
1261                }
1262            }
1263            "function_body" | "block_statement" | "unchecked_block" => {
1264                // Check local variable declarations
1265                let mut c = node.walk();
1266                for child in node.children(&mut c) {
1267                    if (child.kind() == "variable_declaration_statement"
1268                        || child.kind() == "variable_declaration")
1269                        && let Some(id) = ts_child_id_text(child, source)
1270                        && id == var_name
1271                    {
1272                        let mut pc = child.walk();
1273                        return child
1274                            .children(&mut pc)
1275                            .find(|c| {
1276                                matches!(
1277                                    c.kind(),
1278                                    "type_name"
1279                                        | "primitive_type"
1280                                        | "user_defined_type"
1281                                        | "mapping"
1282                                )
1283                            })
1284                            .map(|t| source[t.byte_range()].trim().to_string());
1285                    }
1286                }
1287            }
1288            "contract_declaration" | "library_declaration" | "interface_declaration" => {
1289                // Check state variables
1290                if let Some(body) = ts_find_child(node, "contract_body") {
1291                    let mut c = body.walk();
1292                    for child in body.children(&mut c) {
1293                        if child.kind() == "state_variable_declaration"
1294                            && let Some(id) = ts_child_id_text(child, source)
1295                            && id == var_name
1296                        {
1297                            let mut pc = child.walk();
1298                            return child
1299                                .children(&mut pc)
1300                                .find(|c| {
1301                                    matches!(
1302                                        c.kind(),
1303                                        "type_name"
1304                                            | "primitive_type"
1305                                            | "user_defined_type"
1306                                            | "mapping"
1307                                    )
1308                                })
1309                                .map(|t| source[t.byte_range()].trim().to_string());
1310                        }
1311                    }
1312                }
1313            }
1314            _ => {}
1315        }
1316        scope = node.parent();
1317    }
1318    None
1319}
1320
1321/// Infer argument types at a call site by examining each `call_argument` child.
1322fn infer_call_arg_types(call_node: Node, source: &str) -> Vec<Option<String>> {
1323    let mut cursor = call_node.walk();
1324    call_node
1325        .children(&mut cursor)
1326        .filter(|c| c.kind() == "call_argument")
1327        .map(|arg| infer_argument_type(arg, source))
1328        .collect()
1329}
1330
1331/// Pick the best overload from multiple declarations based on argument types.
1332///
1333/// Strategy:
1334/// 1. If only one declaration, return it.
1335/// 2. Filter by argument count first.
1336/// 3. Among count-matched declarations, score by how many argument types match.
1337/// 4. Return the highest-scoring declaration.
1338fn best_overload<'a>(
1339    decls: &'a [TsDeclaration],
1340    arg_count: Option<usize>,
1341    arg_types: &[Option<String>],
1342) -> Option<&'a TsDeclaration> {
1343    if decls.len() == 1 {
1344        return decls.first();
1345    }
1346    if decls.is_empty() {
1347        return None;
1348    }
1349
1350    // Filter to only function declarations (skip parameters, variables, etc.)
1351    let func_decls: Vec<&TsDeclaration> =
1352        decls.iter().filter(|d| d.param_count.is_some()).collect();
1353
1354    if func_decls.is_empty() {
1355        return decls.first();
1356    }
1357
1358    // If we have arg_count, filter by it
1359    let count_matched: Vec<&&TsDeclaration> = if let Some(ac) = arg_count {
1360        let matched: Vec<_> = func_decls
1361            .iter()
1362            .filter(|d| d.param_count == Some(ac))
1363            .collect();
1364        if matched.len() == 1 {
1365            return Some(matched[0]);
1366        }
1367        if matched.is_empty() {
1368            // No count match — fall back to all
1369            func_decls.iter().collect()
1370        } else {
1371            matched
1372        }
1373    } else {
1374        func_decls.iter().collect()
1375    };
1376
1377    // Score each candidate by how many argument types match parameter types
1378    if !arg_types.is_empty() {
1379        let mut best: Option<(&TsDeclaration, usize)> = None;
1380        for &&decl in &count_matched {
1381            let score = arg_types
1382                .iter()
1383                .zip(decl.param_types.iter())
1384                .filter(|(arg_ty, param_ty)| {
1385                    if let Some(at) = arg_ty {
1386                        at == param_ty.as_str()
1387                    } else {
1388                        false
1389                    }
1390                })
1391                .count();
1392            if best.is_none() || score > best.unwrap().1 {
1393                best = Some((decl, score));
1394            }
1395        }
1396        if let Some((decl, _)) = best {
1397            return Some(decl);
1398        }
1399    }
1400
1401    // Fallback: return first count-matched or first overall
1402    count_matched.first().map(|d| **d).or(decls.first())
1403}
1404
1405/// Extract cursor context: the identifier under the cursor and its ancestor names.
1406///
1407/// Walks up the tree-sitter parse tree to find the enclosing function and contract.
1408pub fn cursor_context(source: &str, position: Position) -> Option<CursorContext> {
1409    let tree = ts_parse(source)?;
1410    let byte = pos_to_bytes(source.as_bytes(), position);
1411    let leaf = ts_node_at_byte(tree.root_node(), byte)?;
1412
1413    // The leaf should be an identifier (or we find the nearest identifier)
1414    let id_node = if leaf.kind() == "identifier" {
1415        leaf
1416    } else {
1417        // Check parent — cursor might be just inside a node that contains an identifier
1418        let parent = leaf.parent()?;
1419        if parent.kind() == "identifier" {
1420            parent
1421        } else {
1422            return None;
1423        }
1424    };
1425
1426    let name = source[id_node.byte_range()].to_string();
1427    let mut function = None;
1428    let mut contract = None;
1429
1430    // Detect member access: if the identifier is the `property` side of a
1431    // member_expression (e.g. `SqrtPriceMath.getAmount0Delta`), extract
1432    // the object name so the caller can resolve cross-file.
1433    let object = id_node.parent().and_then(|parent| {
1434        if parent.kind() == "member_expression" {
1435            let prop = parent.child_by_field_name("property")?;
1436            // Only set object when cursor is on the property, not the object side
1437            if prop.id() == id_node.id() {
1438                let obj = parent.child_by_field_name("object")?;
1439                Some(source[obj.byte_range()].to_string())
1440            } else {
1441                None
1442            }
1443        } else {
1444            None
1445        }
1446    });
1447
1448    // Count arguments and infer types at the call site for overload disambiguation.
1449    // Walk up from the identifier to find an enclosing `call_expression`,
1450    // then count its `call_argument` children and infer their types.
1451    let (arg_count, arg_types) = {
1452        let mut node = id_node.parent();
1453        let mut result = (None, vec![]);
1454        while let Some(n) = node {
1455            if n.kind() == "call_expression" {
1456                let types = infer_call_arg_types(n, source);
1457                result = (Some(types.len()), types);
1458                break;
1459            }
1460            node = n.parent();
1461        }
1462        result
1463    };
1464
1465    // Walk ancestors
1466    let mut current = id_node.parent();
1467    while let Some(node) = current {
1468        match node.kind() {
1469            "function_definition" | "modifier_definition" if function.is_none() => {
1470                function = ts_child_id_text(node, source).map(String::from);
1471            }
1472            "constructor_definition" if function.is_none() => {
1473                function = Some("constructor".into());
1474            }
1475            "contract_declaration" | "interface_declaration" | "library_declaration"
1476                if contract.is_none() =>
1477            {
1478                contract = ts_child_id_text(node, source).map(String::from);
1479            }
1480            _ => {}
1481        }
1482        current = node.parent();
1483    }
1484
1485    Some(CursorContext {
1486        name,
1487        function,
1488        contract,
1489        object,
1490        arg_count,
1491        arg_types,
1492    })
1493}
1494
1495/// Information about a declaration found by tree-sitter.
1496#[derive(Debug, Clone)]
1497pub struct TsDeclaration {
1498    /// Position range of the declaration identifier.
1499    pub range: Range,
1500    /// What kind of declaration (contract, function, state_variable, etc.).
1501    pub kind: &'static str,
1502    /// Container name (contract/struct that owns this declaration).
1503    pub container: Option<String>,
1504    /// Number of parameters (for function/modifier declarations).
1505    pub param_count: Option<usize>,
1506    /// Parameter type signature (e.g. `["uint160", "uint160", "int128"]`).
1507    /// Used for overload disambiguation.
1508    pub param_types: Vec<String>,
1509}
1510
1511/// Find all declarations of a name in a source file using tree-sitter.
1512///
1513/// Scans the parse tree for declaration nodes (state variables, functions, events,
1514/// errors, structs, enums, contracts, etc.) whose identifier matches `name`.
1515pub fn find_declarations_by_name(source: &str, name: &str) -> Vec<TsDeclaration> {
1516    let tree = match ts_parse(source) {
1517        Some(t) => t,
1518        None => return vec![],
1519    };
1520    let mut results = Vec::new();
1521    collect_declarations(tree.root_node(), source, name, None, &mut results);
1522    results
1523}
1524
1525fn collect_declarations(
1526    node: Node,
1527    source: &str,
1528    name: &str,
1529    container: Option<&str>,
1530    out: &mut Vec<TsDeclaration>,
1531) {
1532    let mut cursor = node.walk();
1533    for child in node.children(&mut cursor) {
1534        if !child.is_named() {
1535            continue;
1536        }
1537        match child.kind() {
1538            "contract_declaration" | "interface_declaration" | "library_declaration" => {
1539                if let Some(id_name) = ts_child_id_text(child, source) {
1540                    if id_name == name {
1541                        out.push(TsDeclaration {
1542                            range: id_range(child),
1543                            kind: child.kind(),
1544                            container: container.map(String::from),
1545                            param_count: None,
1546                            param_types: vec![],
1547                        });
1548                    }
1549                    // Recurse into contract body
1550                    if let Some(body) = ts_find_child(child, "contract_body") {
1551                        collect_declarations(body, source, name, Some(id_name), out);
1552                    }
1553                }
1554            }
1555            "function_definition" | "modifier_definition" => {
1556                if let Some(id_name) = ts_child_id_text(child, source) {
1557                    if id_name == name {
1558                        let types = parameter_type_signature(child, source);
1559                        out.push(TsDeclaration {
1560                            range: id_range(child),
1561                            kind: child.kind(),
1562                            container: container.map(String::from),
1563                            param_count: Some(types.len()),
1564                            param_types: types.into_iter().map(String::from).collect(),
1565                        });
1566                    }
1567                    // Check function parameters
1568                    collect_parameters(child, source, name, container, out);
1569                    // Recurse into function body for local variables
1570                    if let Some(body) = ts_find_child(child, "function_body") {
1571                        collect_declarations(body, source, name, container, out);
1572                    }
1573                }
1574            }
1575            "constructor_definition" => {
1576                if name == "constructor" {
1577                    let types = parameter_type_signature(child, source);
1578                    out.push(TsDeclaration {
1579                        range: ts_range(child),
1580                        kind: "constructor_definition",
1581                        container: container.map(String::from),
1582                        param_count: Some(types.len()),
1583                        param_types: types.into_iter().map(String::from).collect(),
1584                    });
1585                }
1586                // Check constructor parameters
1587                collect_parameters(child, source, name, container, out);
1588                if let Some(body) = ts_find_child(child, "function_body") {
1589                    collect_declarations(body, source, name, container, out);
1590                }
1591            }
1592            "state_variable_declaration" | "variable_declaration" => {
1593                if let Some(id_name) = ts_child_id_text(child, source)
1594                    && id_name == name
1595                {
1596                    out.push(TsDeclaration {
1597                        range: id_range(child),
1598                        kind: child.kind(),
1599                        container: container.map(String::from),
1600                        param_count: None,
1601                        param_types: vec![],
1602                    });
1603                }
1604            }
1605            "struct_declaration" => {
1606                if let Some(id_name) = ts_child_id_text(child, source) {
1607                    if id_name == name {
1608                        out.push(TsDeclaration {
1609                            range: id_range(child),
1610                            kind: "struct_declaration",
1611                            container: container.map(String::from),
1612                            param_count: None,
1613                            param_types: vec![],
1614                        });
1615                    }
1616                    if let Some(body) = ts_find_child(child, "struct_body") {
1617                        collect_declarations(body, source, name, Some(id_name), out);
1618                    }
1619                }
1620            }
1621            "enum_declaration" => {
1622                if let Some(id_name) = ts_child_id_text(child, source) {
1623                    if id_name == name {
1624                        out.push(TsDeclaration {
1625                            range: id_range(child),
1626                            kind: "enum_declaration",
1627                            container: container.map(String::from),
1628                            param_count: None,
1629                            param_types: vec![],
1630                        });
1631                    }
1632                    // Check enum values
1633                    if let Some(body) = ts_find_child(child, "enum_body") {
1634                        let mut ecur = body.walk();
1635                        for val in body.children(&mut ecur) {
1636                            if val.kind() == "enum_value" && &source[val.byte_range()] == name {
1637                                out.push(TsDeclaration {
1638                                    range: ts_range(val),
1639                                    kind: "enum_value",
1640                                    container: Some(id_name.to_string()),
1641                                    param_count: None,
1642                                    param_types: vec![],
1643                                });
1644                            }
1645                        }
1646                    }
1647                }
1648            }
1649            "event_definition" | "error_declaration" => {
1650                if let Some(id_name) = ts_child_id_text(child, source)
1651                    && id_name == name
1652                {
1653                    out.push(TsDeclaration {
1654                        range: id_range(child),
1655                        kind: child.kind(),
1656                        container: container.map(String::from),
1657                        param_count: None,
1658                        param_types: vec![],
1659                    });
1660                }
1661            }
1662            "user_defined_type_definition" => {
1663                if let Some(id_name) = ts_child_id_text(child, source)
1664                    && id_name == name
1665                {
1666                    out.push(TsDeclaration {
1667                        range: id_range(child),
1668                        kind: "user_defined_type_definition",
1669                        container: container.map(String::from),
1670                        param_count: None,
1671                        param_types: vec![],
1672                    });
1673                }
1674            }
1675            // Recurse into blocks, if-else, loops, etc.
1676            _ => {
1677                collect_declarations(child, source, name, container, out);
1678            }
1679        }
1680    }
1681}
1682
1683/// Extract the type signature from a function's parameters.
1684///
1685/// Returns a list of type strings, e.g. `["uint160", "uint160", "int128"]`.
1686/// For complex types (mappings, arrays, user-defined), returns the full
1687/// text of the type node.
1688fn parameter_type_signature<'a>(node: Node<'a>, source: &'a str) -> Vec<&'a str> {
1689    let mut cursor = node.walk();
1690    node.children(&mut cursor)
1691        .filter(|c| c.kind() == "parameter")
1692        .filter_map(|param| {
1693            let mut pc = param.walk();
1694            param
1695                .children(&mut pc)
1696                .find(|c| {
1697                    matches!(
1698                        c.kind(),
1699                        "type_name" | "primitive_type" | "user_defined_type" | "mapping"
1700                    )
1701                })
1702                .map(|t| source[t.byte_range()].trim())
1703        })
1704        .collect()
1705}
1706
1707/// Collect parameter declarations from a function/constructor node.
1708fn collect_parameters(
1709    node: Node,
1710    source: &str,
1711    name: &str,
1712    container: Option<&str>,
1713    out: &mut Vec<TsDeclaration>,
1714) {
1715    let mut cursor = node.walk();
1716    for child in node.children(&mut cursor) {
1717        if child.kind() == "parameter"
1718            && let Some(id_name) = ts_child_id_text(child, source)
1719            && id_name == name
1720        {
1721            out.push(TsDeclaration {
1722                range: id_range(child),
1723                kind: "parameter",
1724                container: container.map(String::from),
1725                param_count: None,
1726                param_types: vec![],
1727            });
1728        }
1729    }
1730}
1731
1732/// Tree-sitter range helper.
1733fn ts_range(node: Node) -> Range {
1734    let s = node.start_position();
1735    let e = node.end_position();
1736    Range {
1737        start: Position::new(s.row as u32, s.column as u32),
1738        end: Position::new(e.row as u32, e.column as u32),
1739    }
1740}
1741
1742/// Get the range of the identifier child within a declaration node.
1743fn id_range(node: Node) -> Range {
1744    let mut cursor = node.walk();
1745    node.children(&mut cursor)
1746        .find(|c| c.kind() == "identifier" && c.is_named())
1747        .map(|c| ts_range(c))
1748        .unwrap_or_else(|| ts_range(node))
1749}
1750
1751fn ts_find_child<'a>(node: Node<'a>, kind: &str) -> Option<Node<'a>> {
1752    let mut cursor = node.walk();
1753    node.children(&mut cursor).find(|c| c.kind() == kind)
1754}
1755
1756/// Tree-sitter enhanced goto definition.
1757///
1758/// Uses tree-sitter to find the identifier under the cursor and its scope,
1759/// then resolves via the CompletionCache (for cross-file/semantic resolution),
1760/// and finally uses tree-sitter to find the declaration position in the target file.
1761///
1762/// Falls back to None if resolution fails — caller should try the existing AST-based path.
1763pub fn goto_definition_ts(
1764    source: &str,
1765    position: Position,
1766    file_uri: &Url,
1767    completion_cache: &crate::completion::CompletionCache,
1768    text_cache: &HashMap<crate::types::DocumentUri, (i32, String)>,
1769) -> Option<Location> {
1770    let ctx = cursor_context(source, position)?;
1771
1772    // Member access: cursor is on `getAmount0Delta` in `SqrtPriceMath.getAmount0Delta`.
1773    // Look up the object (SqrtPriceMath) in the completion cache to find its file,
1774    // then search that file for the member declaration.
1775    // When multiple overloads exist, disambiguate by argument count and types.
1776    if let Some(obj_name) = &ctx.object {
1777        if let Some(path) = find_file_for_contract(completion_cache, obj_name, file_uri) {
1778            let target_source = read_target_source(&path, text_cache)?;
1779            let target_uri = Url::from_file_path(&path).ok()?;
1780            let decls = find_declarations_by_name(&target_source, &ctx.name);
1781            if let Some(d) = best_overload(&decls, ctx.arg_count, &ctx.arg_types) {
1782                return Some(Location {
1783                    uri: target_uri,
1784                    range: d.range,
1785                });
1786            }
1787        }
1788        // Object might be in the same file (e.g. a struct or contract in this file)
1789        let decls = find_declarations_by_name(source, &ctx.name);
1790        if let Some(d) = best_overload(&decls, ctx.arg_count, &ctx.arg_types) {
1791            return Some(Location {
1792                uri: file_uri.clone(),
1793                range: d.range,
1794            });
1795        }
1796    }
1797
1798    // Step 1: Try to resolve via CompletionCache to find which file + name the declaration is in.
1799    // Use the scope chain by names: find the contract scope, then resolve the name.
1800    let resolved = resolve_via_cache(&ctx, file_uri, completion_cache);
1801
1802    match resolved {
1803        Some(ResolvedTarget::SameFile) => {
1804            // Declaration is in the same file — find it with tree-sitter
1805            find_best_declaration(source, &ctx, file_uri)
1806        }
1807        Some(ResolvedTarget::OtherFile { path, name }) => {
1808            // Declaration is in another file — read target source and find by name
1809            let target_source = read_target_source(&path, text_cache);
1810            let target_source = target_source?;
1811            let target_uri = Url::from_file_path(&path).ok()?;
1812            let decls = find_declarations_by_name(&target_source, &name);
1813            decls.first().map(|d| Location {
1814                uri: target_uri,
1815                range: d.range,
1816            })
1817        }
1818        None => {
1819            // CompletionCache couldn't resolve — try same-file tree-sitter lookup as fallback
1820            find_best_declaration(source, &ctx, file_uri)
1821        }
1822    }
1823}
1824
1825#[derive(Debug)]
1826enum ResolvedTarget {
1827    /// Declaration is in the same file as the usage.
1828    SameFile,
1829    /// Declaration is in a different file.
1830    OtherFile { path: String, name: String },
1831}
1832
1833/// Try to resolve an identifier using the CompletionCache.
1834///
1835/// Finds the scope by matching ancestor names (contract, function) against
1836/// the cache's scope data, then resolves the name to a type and traces
1837/// back to the declaring file.
1838fn resolve_via_cache(
1839    ctx: &CursorContext,
1840    file_uri: &Url,
1841    cache: &crate::completion::CompletionCache,
1842) -> Option<ResolvedTarget> {
1843    // Find the contract scope node_id by name
1844    let contract_scope = ctx
1845        .contract
1846        .as_ref()
1847        .and_then(|name| cache.name_to_node_id.get(name.as_str()))
1848        .copied();
1849
1850    // Try scope-based resolution: look in the contract's scope_declarations
1851    if let Some(contract_id) = contract_scope {
1852        // Check function scope if we're inside one
1853        if let Some(func_name) = &ctx.function {
1854            // Find the function scope: look for a scope whose parent is this contract
1855            // and which has a declaration for this function name
1856            if let Some(func_scope_id) = find_function_scope(cache, contract_id, func_name) {
1857                // Check declarations in this function scope first
1858                if let Some(decls) = cache.scope_declarations.get(&func_scope_id)
1859                    && decls.iter().any(|d| d.name == ctx.name)
1860                {
1861                    return Some(ResolvedTarget::SameFile);
1862                }
1863            }
1864        }
1865
1866        // Check contract scope declarations (state variables, functions)
1867        if let Some(decls) = cache.scope_declarations.get(&contract_id)
1868            && decls.iter().any(|d| d.name == ctx.name)
1869        {
1870            return Some(ResolvedTarget::SameFile);
1871        }
1872
1873        // Check inherited contracts (C3 linearization)
1874        if let Some(bases) = cache.linearized_base_contracts.get(&contract_id) {
1875            for &base_id in bases.iter().skip(1) {
1876                if let Some(decls) = cache.scope_declarations.get(&base_id)
1877                    && decls.iter().any(|d| d.name == ctx.name)
1878                {
1879                    // Found in a base contract — find which file it's in
1880                    // Reverse lookup: base_id → contract name → file
1881                    let base_name = cache
1882                        .name_to_node_id
1883                        .iter()
1884                        .find(|&(_, &id)| id == base_id)
1885                        .map(|(name, _)| name.clone());
1886
1887                    if let Some(base_name) = base_name
1888                        && let Some(path) =
1889                            find_file_for_contract(cache, base_name.as_str(), file_uri)
1890                    {
1891                        return Some(ResolvedTarget::OtherFile {
1892                            path,
1893                            name: ctx.name.clone(),
1894                        });
1895                    }
1896                    // Base contract might be in the same file
1897                    return Some(ResolvedTarget::SameFile);
1898                }
1899            }
1900        }
1901    }
1902
1903    // Check if the name is a contract/library/interface name
1904    if cache.name_to_node_id.contains_key(ctx.name.as_str()) {
1905        // Could be same file or different file — check if it's in the current file
1906        if let Some(path) = find_file_for_contract(cache, &ctx.name, file_uri) {
1907            let current_path = file_uri.to_file_path().ok()?;
1908            let current_str = current_path.to_str()?;
1909            if path == current_str || path.ends_with(current_str) || current_str.ends_with(&path) {
1910                return Some(ResolvedTarget::SameFile);
1911            }
1912            return Some(ResolvedTarget::OtherFile {
1913                path,
1914                name: ctx.name.clone(),
1915            });
1916        }
1917        return Some(ResolvedTarget::SameFile);
1918    }
1919
1920    // Flat fallback — name_to_type knows about it but we can't determine the file
1921    if cache.name_to_type.contains_key(ctx.name.as_str()) {
1922        return Some(ResolvedTarget::SameFile);
1923    }
1924
1925    None
1926}
1927
1928/// Find the scope node_id for a function within a contract.
1929fn find_function_scope(
1930    cache: &crate::completion::CompletionCache,
1931    contract_id: NodeId,
1932    func_name: &str,
1933) -> Option<NodeId> {
1934    // Look for a scope whose parent is the contract and which is a function scope.
1935    // The function name should appear as a declaration in the contract scope,
1936    // and the function's own scope is the one whose parent is the contract.
1937    for (&scope_id, &parent_id) in &cache.scope_parent {
1938        if parent_id == contract_id {
1939            // This scope's parent is our contract — it might be a function scope.
1940            // Check if this scope has declarations (functions/blocks do).
1941            // We also check if the contract declares a function with this name.
1942            if let Some(contract_decls) = cache.scope_declarations.get(&contract_id)
1943                && contract_decls.iter().any(|d| d.name == func_name)
1944            {
1945                // Found a child scope of the contract — could be the function.
1946                // Check if this scope_id has child scopes or declarations
1947                // that match what we'd expect for a function body.
1948                if cache.scope_declarations.contains_key(&scope_id)
1949                    || cache.scope_parent.values().any(|&p| p == scope_id)
1950                {
1951                    return Some(scope_id);
1952                }
1953            }
1954        }
1955    }
1956    None
1957}
1958
1959/// Find the file path for a contract by searching the CompletionCache's path_to_file_id.
1960fn find_file_for_contract(
1961    cache: &crate::completion::CompletionCache,
1962    contract_name: &str,
1963    _file_uri: &Url,
1964) -> Option<String> {
1965    // The completion cache doesn't directly map contract → file.
1966    // But scope_ranges + path_to_file_id can help.
1967    // For now, check if the contract's node_id appears in any scope_range,
1968    // then map file_id back to path.
1969    let node_id = cache.name_to_node_id.get(contract_name)?;
1970    let scope_range = cache.scope_ranges.iter().find(|r| r.node_id == *node_id)?;
1971    let file_id = scope_range.file_id;
1972
1973    // Reverse lookup: file_id → path
1974    cache
1975        .path_to_file_id
1976        .iter()
1977        .find(|&(_, &fid)| fid == file_id)
1978        .map(|(path, _)| path.to_string())
1979}
1980
1981/// Read source for a target file — prefer text_cache (open buffers), fallback to disk.
1982fn read_target_source(
1983    path: &str,
1984    text_cache: &HashMap<crate::types::DocumentUri, (i32, String)>,
1985) -> Option<String> {
1986    // Try text_cache by URI
1987    let uri = Url::from_file_path(path).ok()?;
1988    if let Some((_, content)) = text_cache.get(&uri.to_string()) {
1989        return Some(content.clone());
1990    }
1991    // Fallback to disk
1992    std::fs::read_to_string(path).ok()
1993}
1994
1995/// Find the best matching declaration in the same file.
1996fn find_best_declaration(source: &str, ctx: &CursorContext, file_uri: &Url) -> Option<Location> {
1997    let decls = find_declarations_by_name(source, &ctx.name);
1998    if decls.is_empty() {
1999        return None;
2000    }
2001
2002    // If there's only one declaration, use it
2003    if decls.len() == 1 {
2004        return Some(Location {
2005            uri: file_uri.clone(),
2006            range: decls[0].range,
2007        });
2008    }
2009
2010    // Multiple declarations — prefer the one in the same contract
2011    if let Some(contract_name) = &ctx.contract
2012        && let Some(d) = decls
2013            .iter()
2014            .find(|d| d.container.as_deref() == Some(contract_name))
2015    {
2016        return Some(Location {
2017            uri: file_uri.clone(),
2018            range: d.range,
2019        });
2020    }
2021
2022    // Fallback: return first declaration
2023    Some(Location {
2024        uri: file_uri.clone(),
2025        range: decls[0].range,
2026    })
2027}
2028
2029// ─────────────────────────────────────────────────────────────────────────────
2030// Code-action helpers (used by lsp.rs `code_action` handler)
2031// ─────────────────────────────────────────────────────────────────────────────
2032
2033/// What kind of edit a code action should produce.
2034#[derive(Debug, Clone, Copy)]
2035pub(crate) enum CodeActionKind<'a> {
2036    /// Insert fixed text at the very start of the file (line 0, col 0).
2037    InsertAtFileStart { text: &'a str },
2038
2039    /// Replace the token at `diag_range.start` with `replacement`.
2040    /// Used for deprecated-builtin fixes (now→block.timestamp, sha3→keccak256, …).
2041    /// When `walk_to` is `Some`, walk up to that ancestor node and replace its
2042    /// full span instead of just the leaf (e.g. `member_expression` for msg.gas).
2043    ReplaceToken {
2044        replacement: &'a str,
2045        walk_to: Option<&'a str>,
2046    },
2047
2048    /// Delete the token whose start byte falls inside `diag_range`
2049    /// (+ one trailing space when present).
2050    DeleteToken,
2051
2052    /// Delete the entire `variable_declaration_statement` containing the
2053    /// identifier at `diag_range.start`, including leading whitespace/newline.
2054    DeleteLocalVar,
2055
2056    /// Walk up the TS tree to the first ancestor whose kind matches `node_kind`,
2057    /// then delete that whole node including its preceding newline+indentation.
2058    /// Used for any "delete this whole statement/declaration" fix (e.g. unused import).
2059    DeleteNodeByKind { node_kind: &'a str },
2060
2061    /// Walk the TS tree up to `walk_to`, then delete the first child whose
2062    /// kind matches any entry in `child_kinds` (tried in order).
2063    DeleteChildNode {
2064        walk_to: &'a str,
2065        child_kinds: &'a [&'a str],
2066    },
2067
2068    /// Walk the TS tree up to `walk_to`, then replace the first child whose
2069    /// kind matches `child_kind` with `replacement`.
2070    ReplaceChildNode {
2071        walk_to: &'a str,
2072        child_kind: &'a str,
2073        replacement: &'a str,
2074    },
2075
2076    /// Walk the TS tree up to `walk_to`, then insert `text` immediately before
2077    /// the first child whose kind matches any entry in `before_child`.
2078    /// Used for 5424 (insert `virtual` before `returns`/`;`).
2079    InsertBeforeNode {
2080        walk_to: &'a str,
2081        before_child: &'a [&'a str],
2082        text: &'a str,
2083    },
2084}
2085
2086/// Compute the `TextEdit` for a code action using tree-sitter for precision.
2087///
2088/// Returns `None` when the tree cannot be parsed or the target node cannot be
2089/// located (caller should fall back to returning no action for that diagnostic).
2090pub(crate) fn code_action_edit(
2091    source: &str,
2092    diag_range: Range,
2093    kind: CodeActionKind<'_>,
2094) -> Option<TextEdit> {
2095    let source_bytes = source.as_bytes();
2096
2097    match kind {
2098        // ── Insert fixed text at the top of the file ──────────────────────────
2099        CodeActionKind::InsertAtFileStart { text } => Some(TextEdit {
2100            range: Range {
2101                start: Position {
2102                    line: 0,
2103                    character: 0,
2104                },
2105                end: Position {
2106                    line: 0,
2107                    character: 0,
2108                },
2109            },
2110            new_text: text.to_string(),
2111        }),
2112
2113        // ── Replace the token at diag_range.start ─────────────────────────────
2114        CodeActionKind::ReplaceToken {
2115            replacement,
2116            walk_to,
2117        } => {
2118            let tree = ts_parse(source)?;
2119            let byte = pos_to_bytes(source_bytes, diag_range.start);
2120            let mut node = ts_node_at_byte(tree.root_node(), byte)?;
2121            // When a walk_to node kind is specified, walk up to that ancestor
2122            // so we replace its full span (e.g. `member_expression` for
2123            // `msg.gas` or `block.blockhash`).
2124            if let Some(target_kind) = walk_to {
2125                loop {
2126                    if node.kind() == target_kind {
2127                        break;
2128                    }
2129                    node = node.parent()?;
2130                }
2131            }
2132            let start_pos = bytes_to_pos(source_bytes, node.start_byte())?;
2133            let end_pos = bytes_to_pos(source_bytes, node.end_byte())?;
2134            Some(TextEdit {
2135                range: Range {
2136                    start: start_pos,
2137                    end: end_pos,
2138                },
2139                new_text: replacement.to_string(),
2140            })
2141        }
2142
2143        // ── Delete the token at diag_range.start (+ optional trailing space) ──
2144        CodeActionKind::DeleteToken => {
2145            let tree = ts_parse(source)?;
2146            let byte = pos_to_bytes(source_bytes, diag_range.start);
2147            let node = ts_node_at_byte(tree.root_node(), byte)?;
2148            let start = node.start_byte();
2149            let end =
2150                if node.end_byte() < source_bytes.len() && source_bytes[node.end_byte()] == b' ' {
2151                    node.end_byte() + 1
2152                } else {
2153                    node.end_byte()
2154                };
2155            let start_pos = bytes_to_pos(source_bytes, start)?;
2156            let end_pos = bytes_to_pos(source_bytes, end)?;
2157            Some(TextEdit {
2158                range: Range {
2159                    start: start_pos,
2160                    end: end_pos,
2161                },
2162                new_text: String::new(),
2163            })
2164        }
2165
2166        // ── Delete the enclosing variable_declaration_statement ───────────────
2167        CodeActionKind::DeleteLocalVar => {
2168            let tree = ts_parse(source)?;
2169            let byte = pos_to_bytes(source_bytes, diag_range.start);
2170            let mut node = ts_node_at_byte(tree.root_node(), byte)?;
2171
2172            loop {
2173                if node.kind() == "variable_declaration_statement" {
2174                    break;
2175                }
2176                node = node.parent()?;
2177            }
2178
2179            // Consume the preceding newline + indentation so no blank line remains.
2180            let stmt_start = node.start_byte();
2181            let delete_from = if stmt_start > 0 {
2182                let mut i = stmt_start - 1;
2183                while i > 0 && (source_bytes[i] == b' ' || source_bytes[i] == b'\t') {
2184                    i -= 1;
2185                }
2186                if source_bytes[i] == b'\n' {
2187                    i
2188                } else {
2189                    stmt_start
2190                }
2191            } else {
2192                stmt_start
2193            };
2194
2195            let start_pos = bytes_to_pos(source_bytes, delete_from)?;
2196            let end_pos = bytes_to_pos(source_bytes, node.end_byte())?;
2197            Some(TextEdit {
2198                range: Range {
2199                    start: start_pos,
2200                    end: end_pos,
2201                },
2202                new_text: String::new(),
2203            })
2204        }
2205
2206        // ── Walk up to `node_kind`, delete that whole node (+ preceding newline) ─
2207        CodeActionKind::DeleteNodeByKind { node_kind } => {
2208            let tree = ts_parse(source)?;
2209            let byte = pos_to_bytes(source_bytes, diag_range.start);
2210            let mut node = ts_node_at_byte(tree.root_node(), byte)?;
2211            loop {
2212                if node.kind() == node_kind {
2213                    break;
2214                }
2215                node = node.parent()?;
2216            }
2217            // Consume the preceding newline + indentation so no blank line remains.
2218            let node_start = node.start_byte();
2219            let delete_from = if node_start > 0 {
2220                let mut i = node_start - 1;
2221                while i > 0 && (source_bytes[i] == b' ' || source_bytes[i] == b'\t') {
2222                    i -= 1;
2223                }
2224                if source_bytes[i] == b'\n' {
2225                    i
2226                } else {
2227                    node_start
2228                }
2229            } else {
2230                node_start
2231            };
2232            let start_pos = bytes_to_pos(source_bytes, delete_from)?;
2233            let end_pos = bytes_to_pos(source_bytes, node.end_byte())?;
2234            Some(TextEdit {
2235                range: Range {
2236                    start: start_pos,
2237                    end: end_pos,
2238                },
2239                new_text: String::new(),
2240            })
2241        }
2242
2243        // ── Walk up to `walk_to`, delete first child of `child_kind` ─────────
2244        //
2245        // Used for 4126 (free function visibility) and payable codes where the
2246        // diagnostic points to the whole function, not the bad token.
2247        CodeActionKind::DeleteChildNode {
2248            walk_to,
2249            child_kinds,
2250        } => {
2251            let tree = ts_parse(source)?;
2252            let byte = pos_to_bytes(source_bytes, diag_range.start);
2253            let mut node = ts_node_at_byte(tree.root_node(), byte)?;
2254            loop {
2255                if node.kind() == walk_to {
2256                    break;
2257                }
2258                node = node.parent()?;
2259            }
2260            let mut cursor = node.walk();
2261            let children: Vec<_> = node.children(&mut cursor).collect();
2262            let child = child_kinds
2263                .iter()
2264                .find_map(|k| children.iter().find(|c| c.kind() == *k))?;
2265            let start = child.start_byte();
2266            let end = if child.end_byte() < source_bytes.len()
2267                && source_bytes[child.end_byte()] == b' '
2268            {
2269                child.end_byte() + 1
2270            } else {
2271                child.end_byte()
2272            };
2273            let start_pos = bytes_to_pos(source_bytes, start)?;
2274            let end_pos = bytes_to_pos(source_bytes, end)?;
2275            Some(TextEdit {
2276                range: Range {
2277                    start: start_pos,
2278                    end: end_pos,
2279                },
2280                new_text: String::new(),
2281            })
2282        }
2283
2284        // ── Walk up to `walk_to`, replace first child of `child_kind` ─────────
2285        //
2286        // Used for 1560/1159/4095: replace wrong visibility with `external`.
2287        CodeActionKind::ReplaceChildNode {
2288            walk_to,
2289            child_kind,
2290            replacement,
2291        } => {
2292            let tree = ts_parse(source)?;
2293            let byte = pos_to_bytes(source_bytes, diag_range.start);
2294            let mut node = ts_node_at_byte(tree.root_node(), byte)?;
2295            loop {
2296                if node.kind() == walk_to {
2297                    break;
2298                }
2299                node = node.parent()?;
2300            }
2301            let mut cursor = node.walk();
2302            let child = node
2303                .children(&mut cursor)
2304                .find(|c| c.kind() == child_kind)?;
2305            let start_pos = bytes_to_pos(source_bytes, child.start_byte())?;
2306            let end_pos = bytes_to_pos(source_bytes, child.end_byte())?;
2307            Some(TextEdit {
2308                range: Range {
2309                    start: start_pos,
2310                    end: end_pos,
2311                },
2312                new_text: replacement.to_string(),
2313            })
2314        }
2315
2316        // ── Walk up to `walk_to`, insert `text` before first matching child ───
2317        //
2318        // Used for 5424 (insert `virtual` before `return_type_definition` / `;`).
2319        //
2320        // `before_child` is tried in order — the first matching child kind wins.
2321        // This lets callers express "prefer returns, fall back to semicolon".
2322        CodeActionKind::InsertBeforeNode {
2323            walk_to,
2324            before_child,
2325            text,
2326        } => {
2327            let tree = ts_parse(source)?;
2328            let byte = pos_to_bytes(source_bytes, diag_range.start);
2329            let mut node = ts_node_at_byte(tree.root_node(), byte)?;
2330
2331            loop {
2332                if node.kind() == walk_to {
2333                    break;
2334                }
2335                node = node.parent()?;
2336            }
2337
2338            let mut cursor = node.walk();
2339            let children: Vec<_> = node.children(&mut cursor).collect();
2340
2341            // Try each `before_child` kind in order.
2342            for target_kind in before_child {
2343                if let Some(child) = children.iter().find(|c| c.kind() == *target_kind) {
2344                    let insert_pos = bytes_to_pos(source_bytes, child.start_byte())?;
2345                    return Some(TextEdit {
2346                        range: Range {
2347                            start: insert_pos,
2348                            end: insert_pos,
2349                        },
2350                        new_text: text.to_string(),
2351                    });
2352                }
2353            }
2354            None
2355        }
2356    }
2357}
2358
2359#[cfg(test)]
2360mod ts_tests {
2361    use super::*;
2362
2363    #[test]
2364    fn test_cursor_context_state_var() {
2365        let source = r#"
2366contract Token {
2367    uint256 public totalSupply;
2368    function mint(uint256 amount) public {
2369        totalSupply += amount;
2370    }
2371}
2372"#;
2373        // Cursor on `totalSupply` inside mint (line 4, col 8)
2374        let ctx = cursor_context(source, Position::new(4, 8)).unwrap();
2375        assert_eq!(ctx.name, "totalSupply");
2376        assert_eq!(ctx.function.as_deref(), Some("mint"));
2377        assert_eq!(ctx.contract.as_deref(), Some("Token"));
2378    }
2379
2380    #[test]
2381    fn test_cursor_context_top_level() {
2382        let source = r#"
2383contract Foo {}
2384contract Bar {}
2385"#;
2386        // Cursor on `Foo` (line 1, col 9) — the identifier of the contract declaration
2387        let ctx = cursor_context(source, Position::new(1, 9)).unwrap();
2388        assert_eq!(ctx.name, "Foo");
2389        assert!(ctx.function.is_none());
2390        // The identifier `Foo` is a child of contract_declaration, so contract is set
2391        assert_eq!(ctx.contract.as_deref(), Some("Foo"));
2392    }
2393
2394    #[test]
2395    fn test_find_declarations() {
2396        let source = r#"
2397contract Token {
2398    uint256 public totalSupply;
2399    function mint(uint256 amount) public {
2400        totalSupply += amount;
2401    }
2402}
2403"#;
2404        let decls = find_declarations_by_name(source, "totalSupply");
2405        assert_eq!(decls.len(), 1);
2406        assert_eq!(decls[0].kind, "state_variable_declaration");
2407        assert_eq!(decls[0].container.as_deref(), Some("Token"));
2408    }
2409
2410    #[test]
2411    fn test_find_declarations_multiple_contracts() {
2412        let source = r#"
2413contract A {
2414    uint256 public value;
2415}
2416contract B {
2417    uint256 public value;
2418}
2419"#;
2420        let decls = find_declarations_by_name(source, "value");
2421        assert_eq!(decls.len(), 2);
2422        assert_eq!(decls[0].container.as_deref(), Some("A"));
2423        assert_eq!(decls[1].container.as_deref(), Some("B"));
2424    }
2425
2426    #[test]
2427    fn test_find_declarations_enum_value() {
2428        let source = "contract Foo { enum Status { Active, Paused } }";
2429        let decls = find_declarations_by_name(source, "Active");
2430        assert_eq!(decls.len(), 1);
2431        assert_eq!(decls[0].kind, "enum_value");
2432        assert_eq!(decls[0].container.as_deref(), Some("Status"));
2433    }
2434
2435    #[test]
2436    fn test_cursor_context_short_param() {
2437        let source = r#"
2438contract Shop {
2439    uint256 public TAX;
2440    constructor(uint256 price, uint16 tax, uint16 taxBase) {
2441        TAX = tax;
2442    }
2443}
2444"#;
2445        // Cursor on `tax` usage at line 4, col 14 (TAX = tax;)
2446        let ctx = cursor_context(source, Position::new(4, 14)).unwrap();
2447        assert_eq!(ctx.name, "tax");
2448        assert_eq!(ctx.contract.as_deref(), Some("Shop"));
2449
2450        // Cursor on `TAX` at line 4, col 8
2451        let ctx2 = cursor_context(source, Position::new(4, 8)).unwrap();
2452        assert_eq!(ctx2.name, "TAX");
2453
2454        // Parameters are found as declarations
2455        let decls = find_declarations_by_name(source, "tax");
2456        assert_eq!(decls.len(), 1);
2457        assert_eq!(decls[0].kind, "parameter");
2458
2459        let decls_tax_base = find_declarations_by_name(source, "taxBase");
2460        assert_eq!(decls_tax_base.len(), 1);
2461        assert_eq!(decls_tax_base[0].kind, "parameter");
2462
2463        let decls_price = find_declarations_by_name(source, "price");
2464        assert_eq!(decls_price.len(), 1);
2465        assert_eq!(decls_price[0].kind, "parameter");
2466
2467        // State variable is also found
2468        let decls_tax_upper = find_declarations_by_name(source, "TAX");
2469        assert_eq!(decls_tax_upper.len(), 1);
2470        assert_eq!(decls_tax_upper[0].kind, "state_variable_declaration");
2471    }
2472
2473    #[test]
2474    fn test_delete_child_node_2462_constructor_public() {
2475        // ConstructorVisibility.sol: `constructor() public {}`
2476        // Diagnostic range: start={line:9, char:4}, end={line:11, char:5}
2477        let source = "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\n// Warning 2462\n\ncontract ConstructorVisibility {\n    uint256 public value;\n\n    constructor() public {\n        value = 1;\n    }\n}\n";
2478        let diag_range = Range {
2479            start: Position {
2480                line: 8,
2481                character: 4,
2482            },
2483            end: Position {
2484                line: 10,
2485                character: 5,
2486            },
2487        };
2488        let source_bytes = source.as_bytes();
2489        let tree = ts_parse(source).expect("parse failed");
2490        let byte = pos_to_bytes(source_bytes, diag_range.start);
2491        eprintln!("2462 byte offset: {byte}");
2492        if let Some(mut n) = ts_node_at_byte(tree.root_node(), byte) {
2493            loop {
2494                eprintln!(
2495                    "  ancestor: kind={} start={} end={}",
2496                    n.kind(),
2497                    n.start_byte(),
2498                    n.end_byte()
2499                );
2500                if n.kind() == "constructor_definition" {
2501                    let mut cursor = n.walk();
2502                    for child in n.children(&mut cursor) {
2503                        eprintln!(
2504                            "    child: kind={:?} text={:?}",
2505                            child.kind(),
2506                            &source[child.start_byte()..child.end_byte()]
2507                        );
2508                    }
2509                    break;
2510                }
2511                match n.parent() {
2512                    Some(p) => n = p,
2513                    None => break,
2514                }
2515            }
2516        }
2517        let ck: Vec<&str> = vec!["public", "modifier_invocation"];
2518        let edit = code_action_edit(
2519            source,
2520            diag_range,
2521            CodeActionKind::DeleteChildNode {
2522                walk_to: "constructor_definition",
2523                child_kinds: &ck,
2524            },
2525        );
2526        assert!(edit.is_some(), "2462 fix returned None");
2527        let edit = edit.unwrap();
2528        assert_eq!(edit.new_text, "");
2529        let lines: Vec<&str> = source.lines().collect();
2530        let deleted = &lines[edit.range.start.line as usize]
2531            [edit.range.start.character as usize..edit.range.end.character as usize];
2532        assert!(deleted.contains("public"), "deleted: {:?}", deleted);
2533    }
2534
2535    #[test]
2536    fn test_delete_child_node_9239_constructor_private() {
2537        // Fixture mirrors example/codeaction/ConstructorInvalidVisibility.sol
2538        // Diagnostic range from server: start={line:9, char:4}, end={line:11, char:5}
2539        let source = "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\n// Error 9239: Constructor cannot have visibility.\n// Fix: remove the visibility specifier (private/external) from the constructor.\n\ncontract ConstructorPrivateVisibility {\n    uint256 public value;\n\n    constructor() private {\n        value = 1;\n    }\n}\n";
2540        let diag_range = Range {
2541            start: Position {
2542                line: 9,
2543                character: 4,
2544            },
2545            end: Position {
2546                line: 11,
2547                character: 5,
2548            },
2549        };
2550        let ck: Vec<&str> = vec!["modifier_invocation"];
2551        let edit = code_action_edit(
2552            source,
2553            diag_range,
2554            CodeActionKind::DeleteChildNode {
2555                walk_to: "constructor_definition",
2556                child_kinds: &ck,
2557            },
2558        );
2559        assert!(edit.is_some(), "expected Some(TextEdit) for 9239, got None");
2560        let edit = edit.unwrap();
2561        // The edit should delete 'private ' — new_text must be empty
2562        assert_eq!(edit.new_text, "", "expected deletion (empty new_text)");
2563        // The deleted text should contain 'private'
2564        let lines: Vec<&str> = source.lines().collect();
2565        let line_text = lines[edit.range.start.line as usize];
2566        let deleted =
2567            &line_text[edit.range.start.character as usize..edit.range.end.character as usize];
2568        assert!(
2569            deleted.contains("private"),
2570            "expected deleted text to contain 'private', got: {:?}",
2571            deleted
2572        );
2573    }
2574
2575    #[test]
2576    fn test_find_best_declaration_same_contract() {
2577        let source = r#"
2578contract A { uint256 public x; }
2579contract B { uint256 public x; }
2580"#;
2581        let ctx = CursorContext {
2582            name: "x".into(),
2583            function: None,
2584            contract: Some("B".into()),
2585            object: None,
2586            arg_count: None,
2587            arg_types: vec![],
2588        };
2589        let uri = Url::parse("file:///test.sol").unwrap();
2590        let loc = find_best_declaration(source, &ctx, &uri).unwrap();
2591        // Should pick B's x (line 2), not A's x (line 1)
2592        assert_eq!(loc.range.start.line, 2);
2593    }
2594}
2595// temp