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