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