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