Skip to main content

solidity_language_server/
completion.rs

1use serde_json::Value;
2use std::collections::HashMap;
3use std::path::Path;
4use tower_lsp::lsp_types::{
5    CompletionItem, CompletionItemKind, CompletionList, CompletionResponse, Position, Range,
6    TextEdit,
7};
8
9use crate::goto::CHILD_KEYS;
10use crate::hover::build_function_signature;
11use crate::types::{FileId, NodeId, SourceLoc};
12use crate::utils::push_if_node_or_array;
13
14/// A directly-declared top-level symbol that can be imported.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub struct TopLevelImportable {
17    /// Symbol name.
18    pub name: String,
19    /// Absolute source path where the symbol is declared.
20    pub declaring_path: String,
21    /// AST node type for this declaration.
22    pub node_type: String,
23    /// LSP completion kind mapped from the AST node type.
24    pub kind: CompletionItemKind,
25}
26
27/// A declaration found within a specific scope.
28#[derive(Debug, Clone)]
29pub struct ScopedDeclaration {
30    /// Variable/function/type name.
31    pub name: String,
32    /// typeIdentifier from typeDescriptions (e.g. "t_struct$_PoolKey_$8887_memory_ptr").
33    pub type_id: String,
34}
35
36/// A byte range identifying a scope-creating AST node.
37#[derive(Debug, Clone)]
38pub struct ScopeRange {
39    /// AST node id of this scope.
40    pub node_id: NodeId,
41    /// Byte offset where this scope starts (from `src` field).
42    pub start: usize,
43    /// Byte offset where this scope ends (start + length).
44    pub end: usize,
45    /// Source file id (from `src` field).
46    pub file_id: FileId,
47}
48
49/// Completion cache built from the AST.
50#[derive(Debug)]
51pub struct CompletionCache {
52    /// All named identifiers as completion items (flat, unscoped).
53    pub names: Vec<CompletionItem>,
54
55    /// name → typeIdentifier (for dot-completion: look up what type a variable is).
56    pub name_to_type: HashMap<String, String>,
57
58    /// node id → Vec<CompletionItem> (members of structs, contracts, enums, libraries).
59    pub node_members: HashMap<NodeId, Vec<CompletionItem>>,
60
61    /// typeIdentifier → node id (resolve a type string to its definition).
62    pub type_to_node: HashMap<String, NodeId>,
63
64    /// contract/library/interface name → node id (for direct name dot-completion like `FullMath.`).
65    pub name_to_node_id: HashMap<String, NodeId>,
66
67    /// node id → Vec<CompletionItem> from methodIdentifiers in .contracts section.
68    /// Full function signatures with 4-byte selectors for contracts/interfaces.
69    pub method_identifiers: HashMap<NodeId, Vec<CompletionItem>>,
70
71    /// (contract_node_id, fn_name) → return typeIdentifier.
72    /// For resolving `foo().` — look up what `foo` returns.
73    pub function_return_types: HashMap<(NodeId, String), String>,
74
75    /// typeIdentifier → Vec<CompletionItem> from UsingForDirective.
76    /// Library functions available on a type via `using X for Y`.
77    pub using_for: HashMap<String, Vec<CompletionItem>>,
78
79    /// Wildcard using-for: `using X for *` — available on all types.
80    pub using_for_wildcard: Vec<CompletionItem>,
81
82    /// Pre-built general completions (AST names + keywords + globals + units).
83    /// Built once, returned by reference on every non-dot completion request.
84    pub general_completions: Vec<CompletionItem>,
85
86    /// scope node_id → declarations in that scope.
87    /// Each scope (Block, FunctionDefinition, ContractDefinition, SourceUnit)
88    /// has the variables/functions/types declared directly within it.
89    pub scope_declarations: HashMap<NodeId, Vec<ScopedDeclaration>>,
90
91    /// node_id → parent scope node_id.
92    /// Walk this chain upward to widen the search scope.
93    pub scope_parent: HashMap<NodeId, NodeId>,
94
95    /// All scope ranges, for finding which scope a byte position falls in.
96    /// Sorted by span size ascending (smallest first) for efficient innermost-scope lookup.
97    pub scope_ranges: Vec<ScopeRange>,
98
99    /// absolute file path → AST source file id.
100    /// Used to map a URI to the file_id needed for scope resolution.
101    pub path_to_file_id: HashMap<String, FileId>,
102
103    /// contract node_id → linearized base contracts (C3 linearization order).
104    /// First element is the contract itself, followed by parents in resolution order.
105    /// Used to search inherited state variables and functions during scope resolution.
106    pub linearized_base_contracts: HashMap<NodeId, Vec<NodeId>>,
107
108    /// contract/interface/library node_id → contractKind string.
109    /// Values are `"contract"`, `"interface"`, or `"library"`.
110    /// Used to determine which `type(X).` members to offer.
111    pub contract_kinds: HashMap<NodeId, String>,
112
113    /// Directly-declared importable top-level symbols keyed by symbol name.
114    ///
115    /// This intentionally excludes imported aliases/re-exports and excludes
116    /// non-constant variables. It is used for import-on-completion candidate
117    /// lookup without re-scanning the full AST per request.
118    pub top_level_importables_by_name: HashMap<String, Vec<TopLevelImportable>>,
119
120    /// Directly-declared importable top-level symbols keyed by declaring file path.
121    ///
122    /// This enables cheap incremental invalidation/update on file edits/deletes:
123    /// only the changed file's symbols need to be replaced.
124    pub top_level_importables_by_file: HashMap<String, Vec<TopLevelImportable>>,
125}
126
127/// Map AST nodeType to LSP CompletionItemKind.
128fn node_type_to_completion_kind(node_type: &str) -> CompletionItemKind {
129    match node_type {
130        "FunctionDefinition" => CompletionItemKind::FUNCTION,
131        "VariableDeclaration" => CompletionItemKind::VARIABLE,
132        "ContractDefinition" => CompletionItemKind::CLASS,
133        "StructDefinition" => CompletionItemKind::STRUCT,
134        "EnumDefinition" => CompletionItemKind::ENUM,
135        "EnumValue" => CompletionItemKind::ENUM_MEMBER,
136        "EventDefinition" => CompletionItemKind::EVENT,
137        "ErrorDefinition" => CompletionItemKind::EVENT,
138        "ModifierDefinition" => CompletionItemKind::METHOD,
139        "ImportDirective" => CompletionItemKind::MODULE,
140        _ => CompletionItemKind::TEXT,
141    }
142}
143
144/// Parse the `src` field of an AST node: "offset:length:fileId".
145/// Returns the parsed SourceLoc or None if the format is invalid.
146fn parse_src(node: &Value) -> Option<SourceLoc> {
147    let src = node.get("src").and_then(|v| v.as_str())?;
148    SourceLoc::parse(src)
149}
150
151/// Extract the trailing node id from a typeIdentifier string.
152/// e.g. `t_struct$_PoolKey_$8887_storage_ptr` → Some(8887)
153///      `t_contract$_IHooks_$2248` → Some(2248)
154///      `t_uint256` → None
155pub fn extract_node_id_from_type(type_id: &str) -> Option<NodeId> {
156    // Pattern: ..._$<digits>... where digits follow the last _$
157    // We find all _$<digits> groups and take the last one that's part of the type name
158    let mut last_id = None;
159    let mut i = 0;
160    let bytes = type_id.as_bytes();
161    while i < bytes.len() {
162        if i + 1 < bytes.len() && bytes[i] == b'_' && bytes[i + 1] == b'$' {
163            i += 2;
164            let start = i;
165            while i < bytes.len() && bytes[i].is_ascii_digit() {
166                i += 1;
167            }
168            if i > start
169                && let Ok(id) = type_id[start..i].parse::<u64>()
170            {
171                last_id = Some(NodeId(id));
172            }
173        } else {
174            i += 1;
175        }
176    }
177    last_id
178}
179
180/// Extract the deepest value type from a mapping typeIdentifier.
181/// Peels off all `t_mapping$_<key>_$_<value>` layers and returns the innermost value type.
182///
183/// e.g. `t_mapping$_t_address_$_t_uint256_$` → `t_uint256`
184///      `t_mapping$_t_address_$_t_mapping$_t_uint256_$_t_uint256_$_$` → `t_uint256`
185///      `t_mapping$_t_userDefinedValueType$_PoolId_$8841_$_t_struct$_State_$4809_storage_$` → `t_struct$_State_$4809_storage`
186pub fn extract_mapping_value_type(type_id: &str) -> Option<String> {
187    let mut current = type_id;
188
189    loop {
190        if !current.starts_with("t_mapping$_") {
191            // Not a mapping — this is the value type
192            // Strip trailing _$ suffixes (mapping closers)
193            let result = current.trim_end_matches("_$");
194            return if result.is_empty() {
195                None
196            } else {
197                Some(result.to_string())
198            };
199        }
200
201        // Strip "t_mapping$_" prefix to get "<key>_$_<value>_$"
202        let inner = &current["t_mapping$_".len()..];
203
204        // Find the boundary between key and value.
205        // We need to find the _$_ that separates key from value at depth 0.
206        // Each $_ opens a nesting level, each _$ closes one.
207        let mut depth = 0i32;
208        let bytes = inner.as_bytes();
209        let mut split_pos = None;
210
211        let mut i = 0;
212        while i < bytes.len() {
213            if i + 1 < bytes.len() && bytes[i] == b'$' && bytes[i + 1] == b'_' {
214                depth += 1;
215                i += 2;
216            } else if i + 2 < bytes.len()
217                && bytes[i] == b'_'
218                && bytes[i + 1] == b'$'
219                && bytes[i + 2] == b'_'
220                && depth == 0
221            {
222                // This is the _$_ separator at depth 0
223                split_pos = Some(i);
224                break;
225            } else if i + 1 < bytes.len() && bytes[i] == b'_' && bytes[i + 1] == b'$' {
226                depth -= 1;
227                i += 2;
228            } else {
229                i += 1;
230            }
231        }
232
233        if let Some(pos) = split_pos {
234            // Value type starts after "_$_"
235            current = &inner[pos + 3..];
236        } else {
237            return None;
238        }
239    }
240}
241
242/// Count parameters in an ABI method signature like "swap((address,address),uint256,bytes)".
243/// Counts commas at depth 0 (inside the outer parens), handling nested tuples.
244fn count_abi_params(signature: &str) -> usize {
245    // Find the first '(' and work from there
246    let start = match signature.find('(') {
247        Some(i) => i + 1,
248        None => return 0,
249    };
250    let bytes = signature.as_bytes();
251    if start >= bytes.len() {
252        return 0;
253    }
254    // Check for empty params "()"
255    if bytes[start] == b')' {
256        return 0;
257    }
258    let mut count = 1; // at least one param if not empty
259    let mut depth = 0;
260    for &b in &bytes[start..] {
261        match b {
262            b'(' => depth += 1,
263            b')' => {
264                if depth == 0 {
265                    break;
266                }
267                depth -= 1;
268            }
269            b',' if depth == 0 => count += 1,
270            _ => {}
271        }
272    }
273    count
274}
275
276/// Count parameters in an AST-derived signature like "swap(PoolKey key, SwapParams params, bytes hookData)".
277fn count_signature_params(sig: &str) -> usize {
278    count_abi_params(sig)
279}
280
281fn is_top_level_importable_decl(node_type: &str, node: &Value) -> bool {
282    match node_type {
283        "ContractDefinition"
284        | "StructDefinition"
285        | "EnumDefinition"
286        | "UserDefinedValueTypeDefinition"
287        | "FunctionDefinition" => true,
288        "VariableDeclaration" => node.get("constant").and_then(|v| v.as_bool()) == Some(true),
289        _ => false,
290    }
291}
292
293fn build_top_level_importables_by_name(
294    by_file: &HashMap<String, Vec<TopLevelImportable>>,
295) -> HashMap<String, Vec<TopLevelImportable>> {
296    let mut by_name: HashMap<String, Vec<TopLevelImportable>> = HashMap::new();
297    for symbols in by_file.values() {
298        for symbol in symbols {
299            by_name
300                .entry(symbol.name.clone())
301                .or_default()
302                .push(symbol.clone());
303        }
304    }
305    by_name
306}
307
308/// Extract directly-declared importable top-level symbols from a file AST.
309///
310/// - Includes: contract/interface/library/struct/enum/UDVT/top-level free function/top-level constant
311/// - Excludes: imported aliases/re-exports, nested declarations, non-constant variables
312pub fn extract_top_level_importables_for_file(
313    path: &str,
314    ast: &Value,
315) -> Vec<TopLevelImportable> {
316    let mut out: Vec<TopLevelImportable> = Vec::new();
317    let mut stack: Vec<&Value> = vec![ast];
318    let mut source_unit_id: Option<NodeId> = None;
319
320    while let Some(tree) = stack.pop() {
321        let node_type = tree.get("nodeType").and_then(|v| v.as_str()).unwrap_or("");
322        let node_id = tree.get("id").and_then(|v| v.as_u64()).map(NodeId);
323        if node_type == "SourceUnit" {
324            source_unit_id = node_id;
325        }
326        let name = tree.get("name").and_then(|v| v.as_str()).unwrap_or("");
327
328        if !name.is_empty()
329            && is_top_level_importable_decl(node_type, tree)
330            && let Some(src_scope) = source_unit_id
331            && tree.get("scope").and_then(|v| v.as_u64()) == Some(src_scope.0)
332        {
333            out.push(TopLevelImportable {
334                name: name.to_string(),
335                declaring_path: path.to_string(),
336                node_type: node_type.to_string(),
337                kind: node_type_to_completion_kind(node_type),
338            });
339        }
340
341        for key in CHILD_KEYS {
342            push_if_node_or_array(tree, key, &mut stack);
343        }
344    }
345
346    out
347}
348
349impl CompletionCache {
350    /// Replace top-level importables for a file path and rebuild the by-name index.
351    /// Pass an empty `symbols` list when the file is deleted.
352    pub fn replace_top_level_importables_for_path(
353        &mut self,
354        path: String,
355        symbols: Vec<TopLevelImportable>,
356    ) {
357        self.top_level_importables_by_file.insert(path, symbols);
358        self.top_level_importables_by_name =
359            build_top_level_importables_by_name(&self.top_level_importables_by_file);
360    }
361}
362
363/// Build a CompletionCache from AST sources and contracts.
364/// `contracts` is the `.contracts` section of the compiler output (optional).
365pub fn build_completion_cache(sources: &Value, contracts: Option<&Value>) -> CompletionCache {
366    let source_count = sources.as_object().map_or(0, |obj| obj.len());
367    // Pre-size collections based on source count to reduce rehash churn.
368    // Estimates: ~20 names/file, ~5 contracts/file, ~10 functions/file.
369    let est_names = source_count * 20;
370    let est_contracts = source_count * 5;
371
372    let mut names: Vec<CompletionItem> = Vec::with_capacity(est_names);
373    let mut seen_names: HashMap<String, usize> = HashMap::with_capacity(est_names);
374    let mut name_to_type: HashMap<String, String> = HashMap::with_capacity(est_names);
375    let mut node_members: HashMap<NodeId, Vec<CompletionItem>> =
376        HashMap::with_capacity(est_contracts);
377    let mut type_to_node: HashMap<String, NodeId> = HashMap::with_capacity(est_contracts);
378    let mut method_identifiers: HashMap<NodeId, Vec<CompletionItem>> =
379        HashMap::with_capacity(est_contracts);
380    let mut name_to_node_id: HashMap<String, NodeId> = HashMap::with_capacity(est_names);
381    let mut contract_kinds: HashMap<NodeId, String> = HashMap::with_capacity(est_contracts);
382
383    // Collect (path, contract_name, node_id) during AST walk for methodIdentifiers lookup after.
384    let mut contract_locations: Vec<(String, String, NodeId)> = Vec::with_capacity(est_contracts);
385
386    // contract_node_id → fn_name → Vec<signature> (for matching method_identifiers to AST signatures)
387    let mut function_signatures: HashMap<NodeId, HashMap<String, Vec<String>>> =
388        HashMap::with_capacity(est_contracts);
389
390    // (contract_node_id, fn_name) → return typeIdentifier
391    let mut function_return_types: HashMap<(NodeId, String), String> =
392        HashMap::with_capacity(source_count * 10);
393
394    // typeIdentifier → Vec<CompletionItem> from UsingForDirective
395    let mut using_for: HashMap<String, Vec<CompletionItem>> = HashMap::with_capacity(source_count);
396    let mut using_for_wildcard: Vec<CompletionItem> = Vec::new();
397
398    // Temp: (library_node_id, target_type_id_or_none) for resolving after walk
399    let mut using_for_directives: Vec<(NodeId, Option<String>)> = Vec::new();
400
401    // Scope-aware completion data
402    let mut scope_declarations: HashMap<NodeId, Vec<ScopedDeclaration>> =
403        HashMap::with_capacity(est_contracts);
404    let mut scope_parent: HashMap<NodeId, NodeId> = HashMap::with_capacity(est_contracts);
405    let mut scope_ranges: Vec<ScopeRange> = Vec::with_capacity(est_contracts);
406    let mut path_to_file_id: HashMap<String, FileId> = HashMap::with_capacity(source_count);
407    let mut linearized_base_contracts: HashMap<NodeId, Vec<NodeId>> =
408        HashMap::with_capacity(est_contracts);
409    let mut top_level_importables_by_file: HashMap<String, Vec<TopLevelImportable>> =
410        HashMap::with_capacity(est_names);
411
412    if let Some(sources_obj) = sources.as_object() {
413        for (path, source_data) in sources_obj {
414            if let Some(ast) = source_data.get("ast") {
415                // Map file path → source file id for scope resolution
416                if let Some(fid) = source_data.get("id").and_then(|v| v.as_u64()) {
417                    path_to_file_id.insert(path.clone(), FileId(fid));
418                }
419                let file_importables = extract_top_level_importables_for_file(path, ast);
420                if !file_importables.is_empty() {
421                    top_level_importables_by_file.insert(path.clone(), file_importables);
422                }
423                let mut stack: Vec<&Value> = vec![ast];
424
425                while let Some(tree) = stack.pop() {
426                    let node_type = tree.get("nodeType").and_then(|v| v.as_str()).unwrap_or("");
427                    let name = tree.get("name").and_then(|v| v.as_str()).unwrap_or("");
428                    let node_id = tree.get("id").and_then(|v| v.as_u64()).map(NodeId);
429
430                    // --- Scope-aware data collection ---
431
432                    // Record scope-creating nodes (SourceUnit, ContractDefinition,
433                    // FunctionDefinition, ModifierDefinition, Block) and their byte ranges.
434                    let is_scope_node = matches!(
435                        node_type,
436                        "SourceUnit"
437                            | "ContractDefinition"
438                            | "FunctionDefinition"
439                            | "ModifierDefinition"
440                            | "Block"
441                            | "UncheckedBlock"
442                    );
443                    if is_scope_node && let Some(nid) = node_id {
444                        if let Some(src_loc) = parse_src(tree) {
445                            scope_ranges.push(ScopeRange {
446                                node_id: nid,
447                                start: src_loc.offset,
448                                end: src_loc.end(),
449                                file_id: src_loc.file_id,
450                            });
451                        }
452                        // Record parent link: this node's scope → its parent
453                        if let Some(parent_id) = tree.get("scope").and_then(|v| v.as_u64()) {
454                            scope_parent.insert(nid, NodeId(parent_id));
455                        }
456                    }
457
458                    // For ContractDefinitions, record linearizedBaseContracts
459                    if node_type == "ContractDefinition"
460                        && let Some(nid) = node_id
461                        && let Some(bases) = tree
462                            .get("linearizedBaseContracts")
463                            .and_then(|v| v.as_array())
464                    {
465                        let base_ids: Vec<NodeId> = bases
466                            .iter()
467                            .filter_map(|b| b.as_u64())
468                            .map(NodeId)
469                            .collect();
470                        if !base_ids.is_empty() {
471                            linearized_base_contracts.insert(nid, base_ids);
472                        }
473                    }
474
475                    // For VariableDeclarations, record the declaration in its scope
476                    if node_type == "VariableDeclaration"
477                        && !name.is_empty()
478                        && let Some(scope_raw) = tree.get("scope").and_then(|v| v.as_u64())
479                        && let Some(tid) = tree
480                            .get("typeDescriptions")
481                            .and_then(|td| td.get("typeIdentifier"))
482                            .and_then(|v| v.as_str())
483                    {
484                        scope_declarations
485                            .entry(NodeId(scope_raw))
486                            .or_default()
487                            .push(ScopedDeclaration {
488                                name: name.to_string(),
489                                type_id: tid.to_string(),
490                            });
491                    }
492
493                    // For FunctionDefinitions, record them in their parent scope (the contract)
494                    if node_type == "FunctionDefinition"
495                        && !name.is_empty()
496                        && let Some(scope_raw) = tree.get("scope").and_then(|v| v.as_u64())
497                        && let Some(tid) = tree
498                            .get("typeDescriptions")
499                            .and_then(|td| td.get("typeIdentifier"))
500                            .and_then(|v| v.as_str())
501                    {
502                        scope_declarations
503                            .entry(NodeId(scope_raw))
504                            .or_default()
505                            .push(ScopedDeclaration {
506                                name: name.to_string(),
507                                type_id: tid.to_string(),
508                            });
509                    }
510
511                    // Collect named nodes as completion items
512                    if !name.is_empty() && !seen_names.contains_key(name) {
513                        let type_string = tree
514                            .get("typeDescriptions")
515                            .and_then(|td| td.get("typeString"))
516                            .and_then(|v| v.as_str())
517                            .map(|s| s.to_string());
518
519                        let type_id = tree
520                            .get("typeDescriptions")
521                            .and_then(|td| td.get("typeIdentifier"))
522                            .and_then(|v| v.as_str());
523
524                        let kind = node_type_to_completion_kind(node_type);
525
526                        let item = CompletionItem {
527                            label: name.to_string(),
528                            kind: Some(kind),
529                            detail: type_string,
530                            ..Default::default()
531                        };
532
533                        let idx = names.len();
534                        names.push(item);
535                        seen_names.insert(name.to_string(), idx);
536
537                        // Store name → typeIdentifier mapping
538                        if let Some(tid) = type_id {
539                            name_to_type.insert(name.to_string(), tid.to_string());
540                        }
541                    }
542
543                    // Collect struct members
544                    if node_type == "StructDefinition"
545                        && let Some(id) = node_id
546                    {
547                        let mut members = Vec::new();
548                        if let Some(member_array) = tree.get("members").and_then(|v| v.as_array()) {
549                            for member in member_array {
550                                let member_name =
551                                    member.get("name").and_then(|v| v.as_str()).unwrap_or("");
552                                if member_name.is_empty() {
553                                    continue;
554                                }
555                                let member_type = member
556                                    .get("typeDescriptions")
557                                    .and_then(|td| td.get("typeString"))
558                                    .and_then(|v| v.as_str())
559                                    .map(|s| s.to_string());
560
561                                members.push(CompletionItem {
562                                    label: member_name.to_string(),
563                                    kind: Some(CompletionItemKind::FIELD),
564                                    detail: member_type,
565                                    ..Default::default()
566                                });
567                            }
568                        }
569                        if !members.is_empty() {
570                            node_members.insert(id, members);
571                        }
572
573                        // Map typeIdentifier → node id
574                        if let Some(tid) = tree
575                            .get("typeDescriptions")
576                            .and_then(|td| td.get("typeIdentifier"))
577                            .and_then(|v| v.as_str())
578                        {
579                            type_to_node.insert(tid.to_string(), id);
580                        }
581                    }
582
583                    // Collect contract/library members (functions, state variables, events, etc.)
584                    if node_type == "ContractDefinition"
585                        && let Some(id) = node_id
586                    {
587                        let mut members = Vec::new();
588                        let mut fn_sigs: HashMap<String, Vec<String>> = HashMap::new();
589                        if let Some(nodes_array) = tree.get("nodes").and_then(|v| v.as_array()) {
590                            for member in nodes_array {
591                                let member_type = member
592                                    .get("nodeType")
593                                    .and_then(|v| v.as_str())
594                                    .unwrap_or("");
595                                let member_name =
596                                    member.get("name").and_then(|v| v.as_str()).unwrap_or("");
597                                if member_name.is_empty() {
598                                    continue;
599                                }
600
601                                // Build function signature and collect return types for FunctionDefinitions
602                                let (member_detail, label_details) =
603                                    if member_type == "FunctionDefinition" {
604                                        // Collect return type for chain resolution.
605                                        // Only single-return functions can be dot-chained
606                                        // (tuples require destructuring).
607                                        if let Some(ret_params) = member
608                                            .get("returnParameters")
609                                            .and_then(|rp| rp.get("parameters"))
610                                            .and_then(|v| v.as_array())
611                                            && ret_params.len() == 1
612                                            && let Some(ret_tid) = ret_params[0]
613                                                .get("typeDescriptions")
614                                                .and_then(|td| td.get("typeIdentifier"))
615                                                .and_then(|v| v.as_str())
616                                        {
617                                            function_return_types.insert(
618                                                (id, member_name.to_string()),
619                                                ret_tid.to_string(),
620                                            );
621                                        }
622
623                                        if let Some(sig) = build_function_signature(member) {
624                                            fn_sigs
625                                                .entry(member_name.to_string())
626                                                .or_default()
627                                                .push(sig.clone());
628                                            (Some(sig), None)
629                                        } else {
630                                            (
631                                                member
632                                                    .get("typeDescriptions")
633                                                    .and_then(|td| td.get("typeString"))
634                                                    .and_then(|v| v.as_str())
635                                                    .map(|s| s.to_string()),
636                                                None,
637                                            )
638                                        }
639                                    } else {
640                                        (
641                                            member
642                                                .get("typeDescriptions")
643                                                .and_then(|td| td.get("typeString"))
644                                                .and_then(|v| v.as_str())
645                                                .map(|s| s.to_string()),
646                                            None,
647                                        )
648                                    };
649
650                                let kind = node_type_to_completion_kind(member_type);
651                                members.push(CompletionItem {
652                                    label: member_name.to_string(),
653                                    kind: Some(kind),
654                                    detail: member_detail,
655                                    label_details,
656                                    ..Default::default()
657                                });
658                            }
659                        }
660                        if !members.is_empty() {
661                            node_members.insert(id, members);
662                        }
663                        if !fn_sigs.is_empty() {
664                            function_signatures.insert(id, fn_sigs);
665                        }
666
667                        if let Some(tid) = tree
668                            .get("typeDescriptions")
669                            .and_then(|td| td.get("typeIdentifier"))
670                            .and_then(|v| v.as_str())
671                        {
672                            type_to_node.insert(tid.to_string(), id);
673                        }
674
675                        // Record for methodIdentifiers lookup after traversal
676                        if !name.is_empty() {
677                            contract_locations.push((path.clone(), name.to_string(), id));
678                            name_to_node_id.insert(name.to_string(), id);
679                        }
680
681                        // Record contractKind (contract, interface, library) for type(X). completions
682                        if let Some(ck) = tree.get("contractKind").and_then(|v| v.as_str()) {
683                            contract_kinds.insert(id, ck.to_string());
684                        }
685                    }
686
687                    // Collect enum members
688                    if node_type == "EnumDefinition"
689                        && let Some(id) = node_id
690                    {
691                        let mut members = Vec::new();
692                        if let Some(member_array) = tree.get("members").and_then(|v| v.as_array()) {
693                            for member in member_array {
694                                let member_name =
695                                    member.get("name").and_then(|v| v.as_str()).unwrap_or("");
696                                if member_name.is_empty() {
697                                    continue;
698                                }
699                                members.push(CompletionItem {
700                                    label: member_name.to_string(),
701                                    kind: Some(CompletionItemKind::ENUM_MEMBER),
702                                    detail: None,
703                                    ..Default::default()
704                                });
705                            }
706                        }
707                        if !members.is_empty() {
708                            node_members.insert(id, members);
709                        }
710
711                        if let Some(tid) = tree
712                            .get("typeDescriptions")
713                            .and_then(|td| td.get("typeIdentifier"))
714                            .and_then(|v| v.as_str())
715                        {
716                            type_to_node.insert(tid.to_string(), id);
717                        }
718                    }
719
720                    // Collect UsingForDirective: using Library for Type
721                    if node_type == "UsingForDirective" {
722                        // Get target type (None = wildcard `for *`)
723                        let target_type = tree.get("typeName").and_then(|tn| {
724                            tn.get("typeDescriptions")
725                                .and_then(|td| td.get("typeIdentifier"))
726                                .and_then(|v| v.as_str())
727                                .map(|s| s.to_string())
728                        });
729
730                        // Form 1: library name object with referencedDeclaration
731                        if let Some(lib) = tree.get("libraryName") {
732                            if let Some(lib_id) =
733                                lib.get("referencedDeclaration").and_then(|v| v.as_u64())
734                            {
735                                using_for_directives.push((NodeId(lib_id), target_type));
736                            }
737                        }
738                        // Form 2: functionList array — individual function references
739                        // These are typically operator overloads (not dot-callable),
740                        // but collect non-operator ones just in case
741                        else if let Some(func_list) =
742                            tree.get("functionList").and_then(|v| v.as_array())
743                        {
744                            for entry in func_list {
745                                // Skip operator overloads
746                                if entry.get("operator").is_some() {
747                                    continue;
748                                }
749                                if let Some(def) = entry.get("definition") {
750                                    let fn_name =
751                                        def.get("name").and_then(|v| v.as_str()).unwrap_or("");
752                                    if !fn_name.is_empty() {
753                                        let items = if let Some(ref tid) = target_type {
754                                            using_for.entry(tid.clone()).or_default()
755                                        } else {
756                                            &mut using_for_wildcard
757                                        };
758                                        items.push(CompletionItem {
759                                            label: fn_name.to_string(),
760                                            kind: Some(CompletionItemKind::FUNCTION),
761                                            detail: None,
762                                            ..Default::default()
763                                        });
764                                    }
765                                }
766                            }
767                        }
768                    }
769
770                    // Traverse children
771                    for key in CHILD_KEYS {
772                        push_if_node_or_array(tree, key, &mut stack);
773                    }
774                }
775            }
776        }
777    }
778
779    // Resolve UsingForDirective library references (Form 1)
780    // Now that node_members is populated, look up each library's functions
781    for (lib_id, target_type) in &using_for_directives {
782        if let Some(lib_members) = node_members.get(lib_id) {
783            let items: Vec<CompletionItem> = lib_members
784                .iter()
785                .filter(|item| item.kind == Some(CompletionItemKind::FUNCTION))
786                .cloned()
787                .collect();
788            if !items.is_empty() {
789                if let Some(tid) = target_type {
790                    using_for.entry(tid.clone()).or_default().extend(items);
791                } else {
792                    using_for_wildcard.extend(items);
793                }
794            }
795        }
796    }
797
798    // Build method_identifiers from .contracts section
799    if let Some(contracts_val) = contracts
800        && let Some(contracts_obj) = contracts_val.as_object()
801    {
802        for (path, contract_name, node_id) in &contract_locations {
803            // Get AST function signatures for this contract (if available)
804            let fn_sigs = function_signatures.get(node_id);
805
806            if let Some(path_entry) = contracts_obj.get(path)
807                && let Some(contract_entry) = path_entry.get(contract_name)
808                && let Some(evm) = contract_entry.get("evm")
809                && let Some(methods) = evm.get("methodIdentifiers")
810                && let Some(methods_obj) = methods.as_object()
811            {
812                let mut items: Vec<CompletionItem> = Vec::new();
813                for (signature, selector_val) in methods_obj {
814                    // signature is e.g. "swap((address,address,uint24,int24,address),(bool,int256,uint160),bytes)"
815                    // selector_val is e.g. "f3cd914c"
816                    let fn_name = signature.split('(').next().unwrap_or(signature).to_string();
817                    let selector_str = selector_val
818                        .as_str()
819                        .map(|s| crate::types::FuncSelector::new(s).to_prefixed())
820                        .unwrap_or_default();
821
822                    // Look up the AST signature with parameter names
823                    let description =
824                        fn_sigs
825                            .and_then(|sigs| sigs.get(&fn_name))
826                            .and_then(|sig_list| {
827                                if sig_list.len() == 1 {
828                                    // Only one overload — use it directly
829                                    Some(sig_list[0].clone())
830                                } else {
831                                    // Multiple overloads — match by parameter count
832                                    let abi_param_count = count_abi_params(signature);
833                                    sig_list
834                                        .iter()
835                                        .find(|s| count_signature_params(s) == abi_param_count)
836                                        .cloned()
837                                }
838                            });
839
840                    items.push(CompletionItem {
841                        label: fn_name,
842                        kind: Some(CompletionItemKind::FUNCTION),
843                        detail: Some(signature.clone()),
844                        label_details: Some(tower_lsp::lsp_types::CompletionItemLabelDetails {
845                            detail: Some(selector_str),
846                            description,
847                        }),
848                        ..Default::default()
849                    });
850                }
851                if !items.is_empty() {
852                    method_identifiers.insert(*node_id, items);
853                }
854            }
855        }
856    }
857
858    // Pre-build the general completions list (names + statics) once
859    let mut general_completions = names.clone();
860    general_completions.extend(get_static_completions());
861
862    // Sort scope_ranges by span size ascending (smallest first) for innermost-scope lookup
863    scope_ranges.sort_by_key(|r| r.end - r.start);
864
865    // Infer parent links for Block/UncheckedBlock/ModifierDefinition nodes.
866    // These AST nodes have no `scope` field, so scope_parent has no entry for them.
867    // For each orphan, find the next-larger enclosing scope range in the same file.
868    // scope_ranges is sorted smallest-first, so we scan forward from each orphan's
869    // position to find the first range that strictly contains it.
870    let orphan_ids: Vec<NodeId> = scope_ranges
871        .iter()
872        .filter(|r| !scope_parent.contains_key(&r.node_id))
873        .map(|r| r.node_id)
874        .collect();
875    // Build a lookup from node_id → (start, end, file_id) for quick access
876    let range_by_id: HashMap<NodeId, (usize, usize, FileId)> = scope_ranges
877        .iter()
878        .map(|r| (r.node_id, (r.start, r.end, r.file_id)))
879        .collect();
880    for orphan_id in &orphan_ids {
881        if let Some(&(start, end, file_id)) = range_by_id.get(orphan_id) {
882            // Find the smallest range that strictly contains this orphan's range
883            // (same file, starts at or before, ends at or after, and is strictly larger)
884            let parent = scope_ranges
885                .iter()
886                .find(|r| {
887                    r.node_id != *orphan_id
888                        && r.file_id == file_id
889                        && r.start <= start
890                        && r.end >= end
891                        && (r.end - r.start) > (end - start)
892                })
893                .map(|r| r.node_id);
894            if let Some(parent_id) = parent {
895                scope_parent.insert(*orphan_id, parent_id);
896            }
897        }
898    }
899
900    let top_level_importables_by_name =
901        build_top_level_importables_by_name(&top_level_importables_by_file);
902
903    CompletionCache {
904        names,
905        name_to_type,
906        node_members,
907        type_to_node,
908        name_to_node_id,
909        method_identifiers,
910        function_return_types,
911        using_for,
912        using_for_wildcard,
913        general_completions,
914        scope_declarations,
915        scope_parent,
916        scope_ranges,
917        path_to_file_id,
918        linearized_base_contracts,
919        contract_kinds,
920        top_level_importables_by_name,
921        top_level_importables_by_file,
922    }
923}
924
925/// Magic type member definitions (msg, block, tx, abi, address).
926fn magic_members(name: &str) -> Option<Vec<CompletionItem>> {
927    let items = match name {
928        "msg" => vec![
929            ("data", "bytes calldata"),
930            ("sender", "address"),
931            ("sig", "bytes4"),
932            ("value", "uint256"),
933        ],
934        "block" => vec![
935            ("basefee", "uint256"),
936            ("blobbasefee", "uint256"),
937            ("chainid", "uint256"),
938            ("coinbase", "address payable"),
939            ("difficulty", "uint256"),
940            ("gaslimit", "uint256"),
941            ("number", "uint256"),
942            ("prevrandao", "uint256"),
943            ("timestamp", "uint256"),
944        ],
945        "tx" => vec![("gasprice", "uint256"), ("origin", "address")],
946        "abi" => vec![
947            ("decode(bytes memory, (...))", "..."),
948            ("encode(...)", "bytes memory"),
949            ("encodePacked(...)", "bytes memory"),
950            ("encodeWithSelector(bytes4, ...)", "bytes memory"),
951            ("encodeWithSignature(string memory, ...)", "bytes memory"),
952            ("encodeCall(function, (...))", "bytes memory"),
953        ],
954        // type(X) — contract type properties
955        // Also includes interface (interfaceId) and integer (min, max) properties
956        "type" => vec![
957            ("name", "string"),
958            ("creationCode", "bytes memory"),
959            ("runtimeCode", "bytes memory"),
960            ("interfaceId", "bytes4"),
961            ("min", "T"),
962            ("max", "T"),
963        ],
964        // bytes and string type-level members
965        "bytes" => vec![("concat(...)", "bytes memory")],
966        "string" => vec![("concat(...)", "string memory")],
967        _ => return None,
968    };
969
970    Some(
971        items
972            .into_iter()
973            .map(|(label, detail)| CompletionItem {
974                label: label.to_string(),
975                kind: Some(CompletionItemKind::PROPERTY),
976                detail: Some(detail.to_string()),
977                ..Default::default()
978            })
979            .collect(),
980    )
981}
982
983/// The kind of argument passed to `type(X)`, which determines which
984/// meta-type members are available.
985#[derive(Debug, Clone, Copy, PartialEq, Eq)]
986enum TypeMetaKind {
987    /// `type(SomeContract)` — has `name`, `creationCode`, `runtimeCode`
988    Contract,
989    /// `type(SomeInterface)` — has `name`, `interfaceId`
990    Interface,
991    /// `type(uint256)` / `type(int8)` — has `min`, `max`
992    IntegerType,
993    /// Unknown argument — return all possible members as a fallback
994    Unknown,
995}
996
997/// Classify the argument of `type(X)` based on the cache.
998fn classify_type_arg(arg: &str, cache: Option<&CompletionCache>) -> TypeMetaKind {
999    // Check if it's an integer type: int, uint, int8..int256, uint8..uint256
1000    if arg == "int" || arg == "uint" {
1001        return TypeMetaKind::IntegerType;
1002    }
1003    if let Some(suffix) = arg.strip_prefix("uint").or_else(|| arg.strip_prefix("int"))
1004        && let Ok(n) = suffix.parse::<u16>()
1005        && (8..=256).contains(&n)
1006        && n % 8 == 0
1007    {
1008        return TypeMetaKind::IntegerType;
1009    }
1010
1011    // With a cache, look up the name to determine contract vs interface
1012    if let Some(c) = cache
1013        && let Some(&node_id) = c.name_to_node_id.get(arg)
1014    {
1015        return match c.contract_kinds.get(&node_id).map(|s| s.as_str()) {
1016            Some("interface") => TypeMetaKind::Interface,
1017            Some("library") => TypeMetaKind::Contract, // libraries have name/creationCode/runtimeCode
1018            _ => TypeMetaKind::Contract,
1019        };
1020    }
1021
1022    TypeMetaKind::Unknown
1023}
1024
1025/// Return context-sensitive `type(X).` completions based on what `X` is.
1026fn type_meta_members(arg: Option<&str>, cache: Option<&CompletionCache>) -> Vec<CompletionItem> {
1027    let kind = match arg {
1028        Some(a) => classify_type_arg(a, cache),
1029        None => TypeMetaKind::Unknown,
1030    };
1031
1032    let items: Vec<(&str, &str)> = match kind {
1033        TypeMetaKind::Contract => vec![
1034            ("name", "string"),
1035            ("creationCode", "bytes memory"),
1036            ("runtimeCode", "bytes memory"),
1037        ],
1038        TypeMetaKind::Interface => vec![("name", "string"), ("interfaceId", "bytes4")],
1039        TypeMetaKind::IntegerType => vec![("min", "T"), ("max", "T")],
1040        TypeMetaKind::Unknown => vec![
1041            ("name", "string"),
1042            ("creationCode", "bytes memory"),
1043            ("runtimeCode", "bytes memory"),
1044            ("interfaceId", "bytes4"),
1045            ("min", "T"),
1046            ("max", "T"),
1047        ],
1048    };
1049
1050    items
1051        .into_iter()
1052        .map(|(label, detail)| CompletionItem {
1053            label: label.to_string(),
1054            kind: Some(CompletionItemKind::PROPERTY),
1055            detail: Some(detail.to_string()),
1056            ..Default::default()
1057        })
1058        .collect()
1059}
1060
1061/// Address type members (available on any address value).
1062fn address_members() -> Vec<CompletionItem> {
1063    [
1064        ("balance", "uint256", CompletionItemKind::PROPERTY),
1065        ("code", "bytes memory", CompletionItemKind::PROPERTY),
1066        ("codehash", "bytes32", CompletionItemKind::PROPERTY),
1067        ("transfer(uint256)", "", CompletionItemKind::FUNCTION),
1068        ("send(uint256)", "bool", CompletionItemKind::FUNCTION),
1069        (
1070            "call(bytes memory)",
1071            "(bool, bytes memory)",
1072            CompletionItemKind::FUNCTION,
1073        ),
1074        (
1075            "delegatecall(bytes memory)",
1076            "(bool, bytes memory)",
1077            CompletionItemKind::FUNCTION,
1078        ),
1079        (
1080            "staticcall(bytes memory)",
1081            "(bool, bytes memory)",
1082            CompletionItemKind::FUNCTION,
1083        ),
1084    ]
1085    .iter()
1086    .map(|(label, detail, kind)| CompletionItem {
1087        label: label.to_string(),
1088        kind: Some(*kind),
1089        detail: if detail.is_empty() {
1090            None
1091        } else {
1092            Some(detail.to_string())
1093        },
1094        ..Default::default()
1095    })
1096    .collect()
1097}
1098
1099/// What kind of access precedes the dot.
1100#[derive(Debug, Clone, PartialEq)]
1101pub enum AccessKind {
1102    /// Plain identifier: `foo.`
1103    Plain,
1104    /// Function call: `foo().` or `foo(x, bar()).`
1105    Call,
1106    /// Index/storage access: `foo[key].` or `foo[func()].`
1107    Index,
1108}
1109
1110/// A segment of a dot-expression chain.
1111#[derive(Debug, Clone, PartialEq)]
1112pub struct DotSegment {
1113    pub name: String,
1114    pub kind: AccessKind,
1115    /// For `Call` segments, the raw text inside the parentheses.
1116    /// e.g. `type(uint256).` → `call_args = Some("uint256")`
1117    pub call_args: Option<String>,
1118}
1119
1120/// Skip backwards over a matched bracket pair (parens or square brackets).
1121/// `pos` should point to the closing bracket. Returns the position of the matching
1122/// opening bracket, or 0 if not found.
1123fn skip_brackets_backwards(bytes: &[u8], pos: usize) -> usize {
1124    let close = bytes[pos];
1125    let open = match close {
1126        b')' => b'(',
1127        b']' => b'[',
1128        _ => return pos,
1129    };
1130    let mut depth = 1u32;
1131    let mut i = pos;
1132    while i > 0 && depth > 0 {
1133        i -= 1;
1134        if bytes[i] == close {
1135            depth += 1;
1136        } else if bytes[i] == open {
1137            depth -= 1;
1138        }
1139    }
1140    i
1141}
1142
1143/// Parse the expression chain before the dot into segments.
1144/// e.g. `poolManager.swap(key, params).` → [("poolManager", Plain), ("swap", Call)]
1145///      `_pools[poolId].fee.` → [("_pools", Index), ("fee", Plain)]
1146///      `msg.` → [("msg", Plain)]
1147pub fn parse_dot_chain(line: &str, character: u32) -> Vec<DotSegment> {
1148    let col = character as usize;
1149    if col == 0 {
1150        return vec![];
1151    }
1152
1153    let bytes = line.as_bytes();
1154    let mut segments: Vec<DotSegment> = Vec::new();
1155
1156    // Start from the cursor position, skip trailing dot
1157    let mut pos = col;
1158    if pos > 0 && pos <= bytes.len() && bytes[pos - 1] == b'.' {
1159        pos -= 1;
1160    }
1161
1162    loop {
1163        if pos == 0 {
1164            break;
1165        }
1166
1167        // Determine access kind by what's immediately before: ')' = Call, ']' = Index, else Plain
1168        let (kind, call_args) = if bytes[pos - 1] == b')' {
1169            let close = pos - 1; // position of ')'
1170            pos = skip_brackets_backwards(bytes, close);
1171            // Extract the text between '(' and ')'
1172            let args_text = String::from_utf8_lossy(&bytes[pos + 1..close])
1173                .trim()
1174                .to_string();
1175            let args = if args_text.is_empty() {
1176                None
1177            } else {
1178                Some(args_text)
1179            };
1180            (AccessKind::Call, args)
1181        } else if bytes[pos - 1] == b']' {
1182            pos -= 1; // point to ']'
1183            pos = skip_brackets_backwards(bytes, pos);
1184            (AccessKind::Index, None)
1185        } else {
1186            (AccessKind::Plain, None)
1187        };
1188
1189        // Now extract the identifier name (walk backwards over alphanumeric + underscore)
1190        let end = pos;
1191        while pos > 0 && (bytes[pos - 1].is_ascii_alphanumeric() || bytes[pos - 1] == b'_') {
1192            pos -= 1;
1193        }
1194
1195        if pos == end {
1196            // No identifier found (could be something like `().`)
1197            break;
1198        }
1199
1200        let name = String::from_utf8_lossy(&bytes[pos..end]).to_string();
1201        segments.push(DotSegment {
1202            name,
1203            kind,
1204            call_args,
1205        });
1206
1207        // Check if there's a dot before this segment (meaning more chain)
1208        if pos > 0 && bytes[pos - 1] == b'.' {
1209            pos -= 1; // skip the dot, continue parsing next segment
1210        } else {
1211            break;
1212        }
1213    }
1214
1215    segments.reverse(); // We parsed right-to-left, flip to left-to-right
1216    segments
1217}
1218
1219/// Extract the identifier before the cursor (the word before the dot).
1220/// Returns just the last identifier name for backward compatibility.
1221pub fn extract_identifier_before_dot(line: &str, character: u32) -> Option<String> {
1222    let segments = parse_dot_chain(line, character);
1223    segments.last().map(|s| s.name.clone())
1224}
1225
1226#[doc = r"Strip all storage/memory location suffixes from a typeIdentifier to get the base type.
1227Solidity AST uses different suffixes in different contexts:
1228  - `t_struct$_State_$4809_storage_ptr` (UsingForDirective typeName)
1229  - `t_struct$_State_$4809_storage` (mapping value type after extraction)
1230  - `t_struct$_PoolKey_$8887_memory_ptr` (function parameter)
1231All refer to the same logical type. This strips `_ptr` and `_storage`/`_memory`/`_calldata`."]
1232fn strip_type_suffix(type_id: &str) -> &str {
1233    let s = type_id.strip_suffix("_ptr").unwrap_or(type_id);
1234    s.strip_suffix("_storage")
1235        .or_else(|| s.strip_suffix("_memory"))
1236        .or_else(|| s.strip_suffix("_calldata"))
1237        .unwrap_or(s)
1238}
1239
1240/// Look up using-for completions for a type, trying suffix variants.
1241/// The AST stores types with different suffixes (_storage_ptr, _storage, _memory_ptr, etc.)
1242/// across different contexts, so we try multiple forms.
1243fn lookup_using_for(cache: &CompletionCache, type_id: &str) -> Vec<CompletionItem> {
1244    // Exact match first
1245    if let Some(items) = cache.using_for.get(type_id) {
1246        return items.clone();
1247    }
1248
1249    // Strip to base form, then try all common suffix variants
1250    let base = strip_type_suffix(type_id);
1251    let variants = [
1252        base.to_string(),
1253        format!("{}_storage", base),
1254        format!("{}_storage_ptr", base),
1255        format!("{}_memory", base),
1256        format!("{}_memory_ptr", base),
1257        format!("{}_calldata", base),
1258    ];
1259    for variant in &variants {
1260        if variant.as_str() != type_id
1261            && let Some(items) = cache.using_for.get(variant.as_str())
1262        {
1263            return items.clone();
1264        }
1265    }
1266
1267    vec![]
1268}
1269
1270/// Collect completions available for a given typeIdentifier.
1271/// Includes node_members, method_identifiers, using_for, and using_for_wildcard.
1272fn completions_for_type(cache: &CompletionCache, type_id: &str) -> Vec<CompletionItem> {
1273    // Address type
1274    if type_id == "t_address" || type_id == "t_address_payable" {
1275        let mut items = address_members();
1276        // Also add using-for on address
1277        if let Some(uf) = cache.using_for.get(type_id) {
1278            items.extend(uf.iter().cloned());
1279        }
1280        items.extend(cache.using_for_wildcard.iter().cloned());
1281        return items;
1282    }
1283
1284    let resolved_node_id = extract_node_id_from_type(type_id)
1285        .or_else(|| cache.type_to_node.get(type_id).copied())
1286        .or_else(|| {
1287            // Handle synthetic __node_id_ markers from name_to_node_id fallback
1288            type_id
1289                .strip_prefix("__node_id_")
1290                .and_then(|s| s.parse::<u64>().ok())
1291                .map(NodeId)
1292        });
1293
1294    let mut items = Vec::new();
1295    let mut seen_labels: std::collections::HashSet<String> = std::collections::HashSet::new();
1296
1297    if let Some(node_id) = resolved_node_id {
1298        // Method identifiers first — they have full signatures with selectors
1299        if let Some(method_items) = cache.method_identifiers.get(&node_id) {
1300            for item in method_items {
1301                seen_labels.insert(item.label.clone());
1302                items.push(item.clone());
1303            }
1304        }
1305
1306        // Supplement with node_members (state variables, events, errors, modifiers, etc.)
1307        if let Some(members) = cache.node_members.get(&node_id) {
1308            for item in members {
1309                if !seen_labels.contains(&item.label) {
1310                    seen_labels.insert(item.label.clone());
1311                    items.push(item.clone());
1312                }
1313            }
1314        }
1315    }
1316
1317    // Add using-for library functions, but only for value types — not for
1318    // contract/library/interface names. When you type `Lock.`, you want Lock's
1319    // own members, not functions from `using Pool for *` or `using SafeCast for *`.
1320    let is_contract_name = resolved_node_id
1321        .map(|nid| cache.contract_kinds.contains_key(&nid))
1322        .unwrap_or(false);
1323
1324    if !is_contract_name {
1325        // Try exact match first, then try normalized variants (storage_ptr vs storage vs memory_ptr etc.)
1326        let uf_items = lookup_using_for(cache, type_id);
1327        for item in &uf_items {
1328            if !seen_labels.contains(&item.label) {
1329                seen_labels.insert(item.label.clone());
1330                items.push(item.clone());
1331            }
1332        }
1333
1334        // Add wildcard using-for (using X for *)
1335        for item in &cache.using_for_wildcard {
1336            if !seen_labels.contains(&item.label) {
1337                seen_labels.insert(item.label.clone());
1338                items.push(item.clone());
1339            }
1340        }
1341    }
1342
1343    items
1344}
1345
1346/// Resolve a type identifier for a name, considering name_to_type and name_to_node_id.
1347fn resolve_name_to_type_id(cache: &CompletionCache, name: &str) -> Option<String> {
1348    // Direct type lookup
1349    if let Some(tid) = cache.name_to_type.get(name) {
1350        return Some(tid.clone());
1351    }
1352    // Contract/library/interface name → synthesize a type id from node id
1353    if let Some(node_id) = cache.name_to_node_id.get(name) {
1354        // Find a matching typeIdentifier in type_to_node (reverse lookup)
1355        for (tid, nid) in &cache.type_to_node {
1356            if nid == node_id {
1357                return Some(tid.clone());
1358            }
1359        }
1360        // Fallback: use a synthetic marker so completions_for_type can resolve via node_id
1361        return Some(format!("__node_id_{}", node_id));
1362    }
1363    None
1364}
1365
1366/// Find the innermost scope node that contains the given byte position and file.
1367/// `scope_ranges` must be sorted by span size ascending (smallest first).
1368/// Returns the node_id of the smallest scope enclosing the position.
1369pub fn find_innermost_scope(
1370    cache: &CompletionCache,
1371    byte_pos: usize,
1372    file_id: FileId,
1373) -> Option<NodeId> {
1374    // scope_ranges is sorted smallest-first, so the first match is the innermost scope
1375    cache
1376        .scope_ranges
1377        .iter()
1378        .find(|r| r.file_id == file_id && r.start <= byte_pos && byte_pos < r.end)
1379        .map(|r| r.node_id)
1380}
1381
1382/// Resolve a variable name to its type by walking up the scope chain.
1383///
1384/// Starting from the innermost scope at the cursor position, check each scope's
1385/// declarations for a matching name. If not found, follow `scope_parent` to the
1386/// next enclosing scope and check again. Stop at the first match.
1387///
1388/// Falls back to `resolve_name_to_type_id` (flat lookup) if scope resolution
1389/// finds nothing, or if the scope data is unavailable.
1390pub fn resolve_name_in_scope(
1391    cache: &CompletionCache,
1392    name: &str,
1393    byte_pos: usize,
1394    file_id: FileId,
1395) -> Option<String> {
1396    let mut current_scope = find_innermost_scope(cache, byte_pos, file_id)?;
1397
1398    // Walk up the scope chain
1399    loop {
1400        // Check declarations in this scope
1401        if let Some(decls) = cache.scope_declarations.get(&current_scope) {
1402            for decl in decls {
1403                if decl.name == name {
1404                    return Some(decl.type_id.clone());
1405                }
1406            }
1407        }
1408
1409        // If this scope is a contract, also search inherited contracts
1410        // in C3 linearization order (skipping index 0 which is the contract itself,
1411        // since we already checked its declarations above).
1412        if let Some(bases) = cache.linearized_base_contracts.get(&current_scope) {
1413            for &base_id in bases.iter().skip(1) {
1414                if let Some(decls) = cache.scope_declarations.get(&base_id) {
1415                    for decl in decls {
1416                        if decl.name == name {
1417                            return Some(decl.type_id.clone());
1418                        }
1419                    }
1420                }
1421            }
1422        }
1423
1424        // Move up to parent scope
1425        match cache.scope_parent.get(&current_scope) {
1426            Some(&parent_id) => current_scope = parent_id,
1427            None => break, // reached the top (SourceUnit has no parent)
1428        }
1429    }
1430
1431    // Scope walk found nothing — fall back to flat lookup
1432    // (handles contract/library names which aren't in scope_declarations)
1433    resolve_name_to_type_id(cache, name)
1434}
1435
1436/// Resolve a name within a type context to get the member's type.
1437/// `context_type_id` is the type of the object before the dot.
1438/// `member_name` is the name after the dot.
1439/// `kind` determines how to interpret the result (Call = return type, Index = mapping value, Plain = member type).
1440fn resolve_member_type(
1441    cache: &CompletionCache,
1442    context_type_id: &str,
1443    member_name: &str,
1444    kind: &AccessKind,
1445) -> Option<String> {
1446    let resolved_node_id = extract_node_id_from_type(context_type_id)
1447        .or_else(|| cache.type_to_node.get(context_type_id).copied())
1448        .or_else(|| {
1449            // Handle synthetic __node_id_ markers
1450            context_type_id
1451                .strip_prefix("__node_id_")
1452                .and_then(|s| s.parse::<u64>().ok())
1453                .map(NodeId)
1454        });
1455
1456    let node_id = resolved_node_id?;
1457
1458    match kind {
1459        AccessKind::Call => {
1460            // Look up the function's return type
1461            cache
1462                .function_return_types
1463                .get(&(node_id, member_name.to_string()))
1464                .cloned()
1465        }
1466        AccessKind::Index => {
1467            // Look up the member's type, then extract mapping value type
1468            if let Some(members) = cache.node_members.get(&node_id) {
1469                for member in members {
1470                    if member.label == member_name {
1471                        // Get the typeIdentifier from name_to_type
1472                        if let Some(tid) = cache.name_to_type.get(member_name) {
1473                            if tid.starts_with("t_mapping") {
1474                                return extract_mapping_value_type(tid);
1475                            }
1476                            return Some(tid.clone());
1477                        }
1478                    }
1479                }
1480            }
1481            // Also check: the identifier itself might be a mapping variable
1482            if let Some(tid) = cache.name_to_type.get(member_name)
1483                && tid.starts_with("t_mapping")
1484            {
1485                return extract_mapping_value_type(tid);
1486            }
1487            None
1488        }
1489        AccessKind::Plain => {
1490            // Look up member's own type from name_to_type
1491            cache.name_to_type.get(member_name).cloned()
1492        }
1493    }
1494}
1495
1496/// Scope context for scope-aware completion resolution.
1497/// When present, type resolution uses the scope chain at the cursor position
1498/// instead of the flat first-wins `name_to_type` map.
1499pub struct ScopeContext {
1500    /// Byte offset of the cursor in the source file.
1501    pub byte_pos: usize,
1502    /// Source file id (from the AST `src` field).
1503    pub file_id: FileId,
1504}
1505
1506/// Resolve a name to a type, using scope context if available.
1507/// With scope context: walks up the scope chain from the cursor position.
1508/// Without: falls back to flat `name_to_type` lookup.
1509fn resolve_name(
1510    cache: &CompletionCache,
1511    name: &str,
1512    scope_ctx: Option<&ScopeContext>,
1513) -> Option<String> {
1514    if let Some(ctx) = scope_ctx {
1515        resolve_name_in_scope(cache, name, ctx.byte_pos, ctx.file_id)
1516    } else {
1517        resolve_name_to_type_id(cache, name)
1518    }
1519}
1520
1521/// Get completions for a dot-completion request by resolving the full expression chain.
1522pub fn get_dot_completions(
1523    cache: &CompletionCache,
1524    identifier: &str,
1525    scope_ctx: Option<&ScopeContext>,
1526) -> Vec<CompletionItem> {
1527    // Simple single-segment case (backward compat) — just use the identifier directly
1528    if let Some(items) = magic_members(identifier) {
1529        return items;
1530    }
1531
1532    // Try to resolve the identifier's type
1533    let type_id = resolve_name(cache, identifier, scope_ctx);
1534
1535    if let Some(tid) = type_id {
1536        return completions_for_type(cache, &tid);
1537    }
1538
1539    vec![]
1540}
1541
1542/// Get completions by resolving a full dot-expression chain.
1543/// This is the main entry point for dot-completions with chaining support.
1544pub fn get_chain_completions(
1545    cache: &CompletionCache,
1546    chain: &[DotSegment],
1547    scope_ctx: Option<&ScopeContext>,
1548) -> Vec<CompletionItem> {
1549    if chain.is_empty() {
1550        return vec![];
1551    }
1552
1553    // Single segment: simple dot-completion
1554    if chain.len() == 1 {
1555        let seg = &chain[0];
1556
1557        // For Call/Index on the single segment, we need to resolve the return/value type
1558        match seg.kind {
1559            AccessKind::Plain => {
1560                return get_dot_completions(cache, &seg.name, scope_ctx);
1561            }
1562            AccessKind::Call => {
1563                // type(X). — Solidity metatype expression
1564                if seg.name == "type" {
1565                    return type_meta_members(seg.call_args.as_deref(), Some(cache));
1566                }
1567                // foo(). — could be a function call or a type cast like IFoo(addr).
1568                // First check if it's a type cast: name matches a contract/interface/library
1569                if let Some(type_id) = resolve_name(cache, &seg.name, scope_ctx) {
1570                    return completions_for_type(cache, &type_id);
1571                }
1572                // Otherwise look up as a function call — check all function_return_types
1573                for ((_, fn_name), ret_type) in &cache.function_return_types {
1574                    if fn_name == &seg.name {
1575                        return completions_for_type(cache, ret_type);
1576                    }
1577                }
1578                return vec![];
1579            }
1580            AccessKind::Index => {
1581                // foo[key]. — look up foo's type and extract mapping value type
1582                if let Some(tid) = resolve_name(cache, &seg.name, scope_ctx)
1583                    && tid.starts_with("t_mapping")
1584                    && let Some(val_type) = extract_mapping_value_type(&tid)
1585                {
1586                    return completions_for_type(cache, &val_type);
1587                }
1588                return vec![];
1589            }
1590        }
1591    }
1592
1593    // Multi-segment chain: resolve step by step
1594    // First segment: resolve to a type (scope-aware when available)
1595    let first = &chain[0];
1596    let mut current_type = match first.kind {
1597        AccessKind::Plain => resolve_name(cache, &first.name, scope_ctx),
1598        AccessKind::Call => {
1599            // Type cast (e.g. IFoo(addr).) or free function call at the start
1600            resolve_name(cache, &first.name, scope_ctx).or_else(|| {
1601                cache
1602                    .function_return_types
1603                    .iter()
1604                    .find(|((_, fn_name), _)| fn_name == &first.name)
1605                    .map(|(_, ret_type)| ret_type.clone())
1606            })
1607        }
1608        AccessKind::Index => {
1609            // Mapping access at the start
1610            resolve_name(cache, &first.name, scope_ctx).and_then(|tid| {
1611                if tid.starts_with("t_mapping") {
1612                    extract_mapping_value_type(&tid)
1613                } else {
1614                    Some(tid)
1615                }
1616            })
1617        }
1618    };
1619
1620    // Middle segments: resolve each to advance the type
1621    for seg in &chain[1..] {
1622        let ctx_type = match &current_type {
1623            Some(t) => t.clone(),
1624            None => return vec![],
1625        };
1626
1627        current_type = resolve_member_type(cache, &ctx_type, &seg.name, &seg.kind);
1628    }
1629
1630    // Return completions for the final resolved type
1631    match current_type {
1632        Some(tid) => completions_for_type(cache, &tid),
1633        None => vec![],
1634    }
1635}
1636
1637/// Get static completions that never change (keywords, magic globals, global functions, units).
1638/// These are available immediately without an AST cache.
1639pub fn get_static_completions() -> Vec<CompletionItem> {
1640    let mut items = Vec::new();
1641
1642    // Add Solidity keywords
1643    for kw in SOLIDITY_KEYWORDS {
1644        items.push(CompletionItem {
1645            label: kw.to_string(),
1646            kind: Some(CompletionItemKind::KEYWORD),
1647            ..Default::default()
1648        });
1649    }
1650
1651    // Add magic globals
1652    for (name, detail) in MAGIC_GLOBALS {
1653        items.push(CompletionItem {
1654            label: name.to_string(),
1655            kind: Some(CompletionItemKind::VARIABLE),
1656            detail: Some(detail.to_string()),
1657            ..Default::default()
1658        });
1659    }
1660
1661    // Add global functions
1662    for (name, detail) in GLOBAL_FUNCTIONS {
1663        items.push(CompletionItem {
1664            label: name.to_string(),
1665            kind: Some(CompletionItemKind::FUNCTION),
1666            detail: Some(detail.to_string()),
1667            ..Default::default()
1668        });
1669    }
1670
1671    // Add ether denomination units
1672    for (name, detail) in ETHER_UNITS {
1673        items.push(CompletionItem {
1674            label: name.to_string(),
1675            kind: Some(CompletionItemKind::UNIT),
1676            detail: Some(detail.to_string()),
1677            ..Default::default()
1678        });
1679    }
1680
1681    // Add time units
1682    for (name, detail) in TIME_UNITS {
1683        items.push(CompletionItem {
1684            label: name.to_string(),
1685            kind: Some(CompletionItemKind::UNIT),
1686            detail: Some(detail.to_string()),
1687            ..Default::default()
1688        });
1689    }
1690
1691    items
1692}
1693
1694/// Get general completions (all known names + static completions).
1695pub fn get_general_completions(cache: &CompletionCache) -> Vec<CompletionItem> {
1696    let mut items = cache.names.clone();
1697    items.extend(get_static_completions());
1698    items
1699}
1700
1701/// Append auto-import candidates at the tail of completion results.
1702///
1703/// This enforces lower priority ordering by:
1704/// 1) appending after base completions
1705/// 2) assigning high `sortText` values (`zz_autoimport_*`) when missing
1706pub fn append_auto_import_candidates_last(
1707    mut base: Vec<CompletionItem>,
1708    mut auto_import_candidates: Vec<CompletionItem>,
1709) -> Vec<CompletionItem> {
1710    let mut unique_label_edits: HashMap<String, Option<Vec<TextEdit>>> = HashMap::new();
1711    for item in &auto_import_candidates {
1712        let entry = unique_label_edits.entry(item.label.clone()).or_insert_with(|| {
1713            item.additional_text_edits.clone()
1714        });
1715        if *entry != item.additional_text_edits {
1716            *entry = None;
1717        }
1718    }
1719
1720    // If a label maps to exactly one import edit, attach it to the corresponding
1721    // base completion item too. This ensures accepting the normal item can still
1722    // apply import edits in clients that de-prioritize or collapse duplicate labels.
1723    for item in &mut base {
1724        if item.additional_text_edits.is_none()
1725            && let Some(Some(edits)) = unique_label_edits.get(&item.label)
1726        {
1727            item.additional_text_edits = Some(edits.clone());
1728        }
1729    }
1730
1731    for (idx, item) in auto_import_candidates.iter_mut().enumerate() {
1732        if item.sort_text.is_none() {
1733            item.sort_text = Some(format!("zz_autoimport_{idx:06}"));
1734        }
1735    }
1736
1737    base.extend(auto_import_candidates);
1738    base
1739}
1740
1741/// Convert cached top-level importable symbols into completion items.
1742///
1743/// These are intended as low-priority tail candidates appended after normal
1744/// per-file completions.
1745pub fn top_level_importable_completion_candidates(
1746    cache: &CompletionCache,
1747    current_file_path: Option<&str>,
1748    source_text: &str,
1749) -> Vec<CompletionItem> {
1750    let mut out = Vec::new();
1751    for symbols in cache.top_level_importables_by_name.values() {
1752        for symbol in symbols {
1753            if let Some(cur) = current_file_path
1754                && cur == symbol.declaring_path
1755            {
1756                continue;
1757            }
1758
1759            let import_path = match current_file_path.and_then(|cur| {
1760                to_relative_import_path(Path::new(cur), Path::new(&symbol.declaring_path))
1761            }) {
1762                Some(p) => p,
1763                None => continue,
1764            };
1765
1766            if import_statement_already_present(source_text, &symbol.name, &import_path) {
1767                continue;
1768            }
1769
1770            let import_edit = build_import_text_edit(source_text, &symbol.name, &import_path);
1771            out.push(CompletionItem {
1772                label: symbol.name.clone(),
1773                kind: Some(symbol.kind),
1774                detail: Some(format!("{} ({import_path})", symbol.node_type)),
1775                additional_text_edits: import_edit.map(|e| vec![e]),
1776                ..Default::default()
1777            });
1778        }
1779    }
1780    out
1781}
1782
1783fn to_relative_import_path(current_file: &Path, target_file: &Path) -> Option<String> {
1784    let from_dir = current_file.parent()?;
1785    let rel = pathdiff::diff_paths(target_file, from_dir)?;
1786    let mut s = rel.to_string_lossy().replace('\\', "/");
1787    if !s.starts_with("./") && !s.starts_with("../") {
1788        s = format!("./{s}");
1789    }
1790    Some(s)
1791}
1792
1793fn import_statement_already_present(source_text: &str, symbol: &str, import_path: &str) -> bool {
1794    let named = format!("import {{{symbol}}} from \"{import_path}\";");
1795    let full = format!("import \"{import_path}\";");
1796    source_text.contains(&named) || source_text.contains(&full)
1797}
1798
1799fn build_import_text_edit(source_text: &str, symbol: &str, import_path: &str) -> Option<TextEdit> {
1800    let import_stmt = format!("import {{{symbol}}} from \"{import_path}\";\n");
1801    let lines: Vec<&str> = source_text.lines().collect();
1802
1803    let last_import_line = lines
1804        .iter()
1805        .enumerate()
1806        .filter(|(_, line)| line.trim_start().starts_with("import "))
1807        .map(|(idx, _)| idx)
1808        .last();
1809
1810    let insert_line = if let Some(idx) = last_import_line {
1811        idx + 1
1812    } else if let Some(idx) = lines
1813        .iter()
1814        .enumerate()
1815        .filter(|(_, line)| line.trim_start().starts_with("pragma "))
1816        .map(|(idx, _)| idx)
1817        .last()
1818    {
1819        idx + 1
1820    } else {
1821        0
1822    };
1823
1824    Some(TextEdit {
1825        range: Range {
1826            start: Position {
1827                line: insert_line as u32,
1828                character: 0,
1829            },
1830            end: Position {
1831                line: insert_line as u32,
1832                character: 0,
1833            },
1834        },
1835        new_text: import_stmt,
1836    })
1837}
1838
1839/// Handle a completion request with optional tail candidates.
1840///
1841/// Tail candidates are only appended for non-dot completions and are always
1842/// ordered last via `append_auto_import_candidates_last`.
1843pub fn handle_completion_with_tail_candidates(
1844    cache: Option<&CompletionCache>,
1845    source_text: &str,
1846    position: Position,
1847    trigger_char: Option<&str>,
1848    file_id: Option<FileId>,
1849    tail_candidates: Vec<CompletionItem>,
1850) -> Option<CompletionResponse> {
1851    let lines: Vec<&str> = source_text.lines().collect();
1852    let line = lines.get(position.line as usize)?;
1853
1854    // Convert encoding-aware column to a byte offset within this line.
1855    let abs_byte = crate::utils::position_to_byte_offset(source_text, position);
1856    let line_start_byte: usize = source_text[..abs_byte]
1857        .rfind('\n')
1858        .map(|i| i + 1)
1859        .unwrap_or(0);
1860    let col_byte = (abs_byte - line_start_byte) as u32;
1861
1862    // Build scope context for scope-aware type resolution
1863    let scope_ctx = file_id.map(|fid| ScopeContext {
1864        byte_pos: abs_byte,
1865        file_id: fid,
1866    });
1867
1868    let items = if trigger_char == Some(".") {
1869        let chain = parse_dot_chain(line, col_byte);
1870        if chain.is_empty() {
1871            return None;
1872        }
1873        match cache {
1874            Some(c) => get_chain_completions(c, &chain, scope_ctx.as_ref()),
1875            None => {
1876                // No cache yet — serve magic dot completions (msg., block., etc.)
1877                if chain.len() == 1 {
1878                    let seg = &chain[0];
1879                    if seg.name == "type" && seg.kind == AccessKind::Call {
1880                        // type(X). without cache — classify based on name alone
1881                        type_meta_members(seg.call_args.as_deref(), None)
1882                    } else if seg.kind == AccessKind::Plain {
1883                        magic_members(&seg.name).unwrap_or_default()
1884                    } else {
1885                        vec![]
1886                    }
1887                } else {
1888                    vec![]
1889                }
1890            }
1891        }
1892    } else {
1893        match cache {
1894            Some(c) => {
1895                append_auto_import_candidates_last(c.general_completions.clone(), tail_candidates)
1896            }
1897            None => get_static_completions(),
1898        }
1899    };
1900
1901    Some(CompletionResponse::List(CompletionList {
1902        is_incomplete: cache.is_none(),
1903        items,
1904    }))
1905}
1906
1907/// Handle a completion request.
1908///
1909/// When `cache` is `Some`, full AST-aware completions are returned.
1910/// When `cache` is `None`, only static completions (keywords, globals, units)
1911/// and magic dot completions (msg., block., tx., abi., type().) are returned
1912/// immediately — no blocking.
1913///
1914/// `file_id` is the AST source file id, needed for scope-aware resolution.
1915/// When `None`, scope resolution is skipped and flat lookup is used.
1916pub fn handle_completion(
1917    cache: Option<&CompletionCache>,
1918    source_text: &str,
1919    position: Position,
1920    trigger_char: Option<&str>,
1921    file_id: Option<FileId>,
1922) -> Option<CompletionResponse> {
1923    handle_completion_with_tail_candidates(
1924        cache,
1925        source_text,
1926        position,
1927        trigger_char,
1928        file_id,
1929        vec![],
1930    )
1931}
1932
1933const SOLIDITY_KEYWORDS: &[&str] = &[
1934    "abstract",
1935    "address",
1936    "assembly",
1937    "bool",
1938    "break",
1939    "bytes",
1940    "bytes1",
1941    "bytes4",
1942    "bytes32",
1943    "calldata",
1944    "constant",
1945    "constructor",
1946    "continue",
1947    "contract",
1948    "delete",
1949    "do",
1950    "else",
1951    "emit",
1952    "enum",
1953    "error",
1954    "event",
1955    "external",
1956    "fallback",
1957    "false",
1958    "for",
1959    "function",
1960    "if",
1961    "immutable",
1962    "import",
1963    "indexed",
1964    "int8",
1965    "int24",
1966    "int128",
1967    "int256",
1968    "interface",
1969    "internal",
1970    "library",
1971    "mapping",
1972    "memory",
1973    "modifier",
1974    "new",
1975    "override",
1976    "payable",
1977    "pragma",
1978    "private",
1979    "public",
1980    "pure",
1981    "receive",
1982    "return",
1983    "returns",
1984    "revert",
1985    "storage",
1986    "string",
1987    "struct",
1988    "true",
1989    "type",
1990    "uint8",
1991    "uint24",
1992    "uint128",
1993    "uint160",
1994    "uint256",
1995    "unchecked",
1996    "using",
1997    "view",
1998    "virtual",
1999    "while",
2000];
2001
2002/// Ether denomination units — suffixes for literal numbers.
2003const ETHER_UNITS: &[(&str, &str)] = &[("wei", "1"), ("gwei", "1e9"), ("ether", "1e18")];
2004
2005/// Time units — suffixes for literal numbers.
2006const TIME_UNITS: &[(&str, &str)] = &[
2007    ("seconds", "1"),
2008    ("minutes", "60 seconds"),
2009    ("hours", "3600 seconds"),
2010    ("days", "86400 seconds"),
2011    ("weeks", "604800 seconds"),
2012];
2013
2014const MAGIC_GLOBALS: &[(&str, &str)] = &[
2015    ("msg", "msg"),
2016    ("block", "block"),
2017    ("tx", "tx"),
2018    ("abi", "abi"),
2019    ("this", "address"),
2020    ("super", "contract"),
2021    ("type", "type information"),
2022];
2023
2024const GLOBAL_FUNCTIONS: &[(&str, &str)] = &[
2025    // Mathematical and Cryptographic Functions
2026    ("addmod(uint256, uint256, uint256)", "uint256"),
2027    ("mulmod(uint256, uint256, uint256)", "uint256"),
2028    ("keccak256(bytes memory)", "bytes32"),
2029    ("sha256(bytes memory)", "bytes32"),
2030    ("ripemd160(bytes memory)", "bytes20"),
2031    (
2032        "ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s)",
2033        "address",
2034    ),
2035    // Block and Transaction Properties (functions)
2036    ("blockhash(uint256 blockNumber)", "bytes32"),
2037    ("blobhash(uint256 index)", "bytes32"),
2038    ("gasleft()", "uint256"),
2039    // Error Handling
2040    ("assert(bool condition)", ""),
2041    ("require(bool condition)", ""),
2042    ("require(bool condition, string memory message)", ""),
2043    ("revert()", ""),
2044    ("revert(string memory reason)", ""),
2045    // Contract-related
2046    ("selfdestruct(address payable recipient)", ""),
2047];
2048
2049#[cfg(test)]
2050mod tests {
2051    use super::{
2052        CompletionCache, TopLevelImportable, append_auto_import_candidates_last,
2053        build_completion_cache, extract_top_level_importables_for_file,
2054    };
2055    use serde_json::json;
2056    use std::collections::HashMap;
2057    use tower_lsp::lsp_types::CompletionItemKind;
2058    use tower_lsp::lsp_types::{CompletionItem, CompletionResponse, Position, Range, TextEdit};
2059
2060    fn empty_cache() -> CompletionCache {
2061        CompletionCache {
2062            names: vec![],
2063            name_to_type: HashMap::new(),
2064            node_members: HashMap::new(),
2065            type_to_node: HashMap::new(),
2066            name_to_node_id: HashMap::new(),
2067            method_identifiers: HashMap::new(),
2068            function_return_types: HashMap::new(),
2069            using_for: HashMap::new(),
2070            using_for_wildcard: vec![],
2071            general_completions: vec![],
2072            scope_declarations: HashMap::new(),
2073            scope_parent: HashMap::new(),
2074            scope_ranges: vec![],
2075            path_to_file_id: HashMap::new(),
2076            linearized_base_contracts: HashMap::new(),
2077            contract_kinds: HashMap::new(),
2078            top_level_importables_by_name: HashMap::new(),
2079            top_level_importables_by_file: HashMap::new(),
2080        }
2081    }
2082
2083    #[test]
2084    fn top_level_importables_include_only_direct_declared_symbols() {
2085        let sources = json!({
2086            "/tmp/A.sol": {
2087                "id": 0,
2088                "ast": {
2089                    "id": 1,
2090                    "nodeType": "SourceUnit",
2091                    "src": "0:100:0",
2092                    "nodes": [
2093                        { "id": 10, "nodeType": "ImportDirective", "name": "Alias", "scope": 1, "src": "1:1:0" },
2094                        { "id": 11, "nodeType": "ContractDefinition", "name": "C", "scope": 1, "src": "2:1:0", "nodes": [
2095                            { "id": 21, "nodeType": "VariableDeclaration", "name": "inside", "scope": 11, "constant": true, "src": "3:1:0" }
2096                        ] },
2097                        { "id": 12, "nodeType": "StructDefinition", "name": "S", "scope": 1, "src": "4:1:0" },
2098                        { "id": 13, "nodeType": "EnumDefinition", "name": "E", "scope": 1, "src": "5:1:0" },
2099                        { "id": 14, "nodeType": "UserDefinedValueTypeDefinition", "name": "Wad", "scope": 1, "src": "6:1:0" },
2100                        { "id": 15, "nodeType": "FunctionDefinition", "name": "freeFn", "scope": 1, "src": "7:1:0" },
2101                        { "id": 16, "nodeType": "VariableDeclaration", "name": "TOP_CONST", "scope": 1, "constant": true, "src": "8:1:0" },
2102                        { "id": 17, "nodeType": "VariableDeclaration", "name": "TOP_VAR", "scope": 1, "constant": false, "src": "9:1:0" }
2103                    ]
2104                }
2105            }
2106        });
2107
2108        let cache = build_completion_cache(&sources, None);
2109        let map = &cache.top_level_importables_by_name;
2110        let by_file = &cache.top_level_importables_by_file;
2111
2112        assert!(map.contains_key("C"));
2113        assert!(map.contains_key("S"));
2114        assert!(map.contains_key("E"));
2115        assert!(map.contains_key("Wad"));
2116        assert!(map.contains_key("freeFn"));
2117        assert!(map.contains_key("TOP_CONST"));
2118
2119        assert!(!map.contains_key("Alias"));
2120        assert!(!map.contains_key("inside"));
2121        assert!(!map.contains_key("TOP_VAR"));
2122
2123        let file_symbols = by_file.get("/tmp/A.sol").unwrap();
2124        let file_names: Vec<&str> = file_symbols.iter().map(|s| s.name.as_str()).collect();
2125        assert!(file_names.contains(&"C"));
2126        assert!(file_names.contains(&"TOP_CONST"));
2127        assert!(!file_names.contains(&"Alias"));
2128    }
2129
2130    #[test]
2131    fn top_level_importables_keep_multiple_declarations_for_same_name() {
2132        let sources = json!({
2133            "/tmp/A.sol": {
2134                "id": 0,
2135                "ast": {
2136                    "id": 1,
2137                    "nodeType": "SourceUnit",
2138                    "src": "0:100:0",
2139                    "nodes": [
2140                        { "id": 11, "nodeType": "FunctionDefinition", "name": "dup", "scope": 1, "src": "1:1:0" }
2141                    ]
2142                }
2143            },
2144            "/tmp/B.sol": {
2145                "id": 1,
2146                "ast": {
2147                    "id": 2,
2148                    "nodeType": "SourceUnit",
2149                    "src": "0:100:1",
2150                    "nodes": [
2151                        { "id": 22, "nodeType": "FunctionDefinition", "name": "dup", "scope": 2, "src": "2:1:1" }
2152                    ]
2153                }
2154            }
2155        });
2156
2157        let cache = build_completion_cache(&sources, None);
2158        let entries = cache.top_level_importables_by_name.get("dup").unwrap();
2159        assert_eq!(entries.len(), 2);
2160    }
2161
2162    #[test]
2163    fn extract_top_level_importables_for_file_finds_expected_symbols() {
2164        let ast = json!({
2165            "id": 1,
2166            "nodeType": "SourceUnit",
2167            "src": "0:100:0",
2168            "nodes": [
2169                { "id": 2, "nodeType": "FunctionDefinition", "name": "f", "scope": 1, "src": "1:1:0" },
2170                { "id": 3, "nodeType": "VariableDeclaration", "name": "K", "scope": 1, "constant": true, "src": "2:1:0" },
2171                { "id": 4, "nodeType": "VariableDeclaration", "name": "V", "scope": 1, "constant": false, "src": "3:1:0" }
2172            ]
2173        });
2174
2175        let symbols = extract_top_level_importables_for_file("/tmp/A.sol", &ast);
2176        let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect();
2177        assert!(names.contains(&"f"));
2178        assert!(names.contains(&"K"));
2179        assert!(!names.contains(&"V"));
2180    }
2181
2182    #[test]
2183    fn top_level_importables_can_be_replaced_per_file() {
2184        let sources = json!({
2185            "/tmp/A.sol": {
2186                "id": 0,
2187                "ast": {
2188                    "id": 1,
2189                    "nodeType": "SourceUnit",
2190                    "src": "0:100:0",
2191                    "nodes": [
2192                        { "id": 11, "nodeType": "FunctionDefinition", "name": "dup", "scope": 1, "src": "1:1:0" }
2193                    ]
2194                }
2195            },
2196            "/tmp/B.sol": {
2197                "id": 1,
2198                "ast": {
2199                    "id": 2,
2200                    "nodeType": "SourceUnit",
2201                    "src": "0:100:1",
2202                    "nodes": [
2203                        { "id": 22, "nodeType": "FunctionDefinition", "name": "dup", "scope": 2, "src": "2:1:1" }
2204                    ]
2205                }
2206            }
2207        });
2208
2209        let mut cache = build_completion_cache(&sources, None);
2210        assert_eq!(cache.top_level_importables_by_name["dup"].len(), 2);
2211
2212        cache.replace_top_level_importables_for_path(
2213            "/tmp/A.sol".to_string(),
2214            vec![TopLevelImportable {
2215                name: "newA".to_string(),
2216                declaring_path: "/tmp/A.sol".to_string(),
2217                node_type: "FunctionDefinition".to_string(),
2218                kind: CompletionItemKind::FUNCTION,
2219            }],
2220        );
2221        assert_eq!(cache.top_level_importables_by_name["dup"].len(), 1);
2222        assert!(cache.top_level_importables_by_name.contains_key("newA"));
2223
2224        cache.replace_top_level_importables_for_path("/tmp/A.sol".to_string(), vec![]);
2225        assert!(!cache.top_level_importables_by_name.contains_key("newA"));
2226    }
2227
2228    #[test]
2229    fn append_auto_import_candidates_last_sets_tail_sort_text() {
2230        let base = vec![CompletionItem {
2231            label: "localVar".to_string(),
2232            ..Default::default()
2233        }];
2234        let auto = vec![CompletionItem {
2235            label: "ImportMe".to_string(),
2236            ..Default::default()
2237        }];
2238
2239        let out = append_auto_import_candidates_last(base, auto);
2240        assert_eq!(out.len(), 2);
2241        assert_eq!(out[1].label, "ImportMe");
2242        assert!(
2243            out[1]
2244                .sort_text
2245                .as_deref()
2246                .is_some_and(|s| s.starts_with("zz_autoimport_"))
2247        );
2248    }
2249
2250    #[test]
2251    fn append_auto_import_candidates_last_keeps_same_label_candidates() {
2252        let base = vec![CompletionItem {
2253            label: "B".to_string(),
2254            ..Default::default()
2255        }];
2256        let auto = vec![
2257            CompletionItem {
2258                label: "B".to_string(),
2259                detail: Some("ContractDefinition (./B.sol)".to_string()),
2260                ..Default::default()
2261            },
2262            CompletionItem {
2263                label: "B".to_string(),
2264                detail: Some("ContractDefinition (./deps/B.sol)".to_string()),
2265                ..Default::default()
2266            },
2267        ];
2268
2269        let out = append_auto_import_candidates_last(base, auto);
2270        assert_eq!(out.len(), 3);
2271    }
2272
2273    #[test]
2274    fn append_auto_import_candidates_last_enriches_unique_base_label_with_edit() {
2275        let base = vec![CompletionItem {
2276            label: "B".to_string(),
2277            ..Default::default()
2278        }];
2279        let auto = vec![CompletionItem {
2280            label: "B".to_string(),
2281            additional_text_edits: Some(vec![TextEdit {
2282                range: Range {
2283                    start: Position {
2284                        line: 0,
2285                        character: 0,
2286                    },
2287                    end: Position {
2288                        line: 0,
2289                        character: 0,
2290                    },
2291                },
2292                new_text: "import {B} from \"./B.sol\";\n".to_string(),
2293            }]),
2294            ..Default::default()
2295        }];
2296        let out = append_auto_import_candidates_last(base, auto);
2297        assert!(
2298            out[0].additional_text_edits.is_some(),
2299            "base item should inherit unique import edit"
2300        );
2301    }
2302
2303    #[test]
2304    fn top_level_importable_candidates_include_import_edit() {
2305        let mut cache = empty_cache();
2306        cache.top_level_importables_by_name.insert(
2307            "B".to_string(),
2308            vec![TopLevelImportable {
2309                name: "B".to_string(),
2310                declaring_path: "/tmp/example/B.sol".to_string(),
2311                node_type: "ContractDefinition".to_string(),
2312                kind: CompletionItemKind::CLASS,
2313            }],
2314        );
2315
2316        let source = "// SPDX-License-Identifier: MIT\npragma solidity ^0.8.26;\n\ncontract A {}\n";
2317        let items = super::top_level_importable_completion_candidates(
2318            &cache,
2319            Some("/tmp/example/A.sol"),
2320            source,
2321        );
2322        assert_eq!(items.len(), 1);
2323        let edit_text = items[0]
2324            .additional_text_edits
2325            .as_ref()
2326            .and_then(|edits| edits.first())
2327            .map(|e| e.new_text.clone())
2328            .unwrap_or_default();
2329        assert!(edit_text.contains("import {B} from \"./B.sol\";"));
2330    }
2331
2332    #[test]
2333    fn handle_completion_general_path_keeps_base_items() {
2334        let mut cache = empty_cache();
2335        cache.general_completions = vec![CompletionItem {
2336            label: "A".to_string(),
2337            ..Default::default()
2338        }];
2339
2340        let resp = super::handle_completion(
2341            Some(&cache),
2342            "contract X {}",
2343            Position {
2344                line: 0,
2345                character: 0,
2346            },
2347            None,
2348            None,
2349        );
2350        match resp {
2351            Some(CompletionResponse::List(list)) => {
2352                assert_eq!(list.items.len(), 1);
2353                assert_eq!(list.items[0].label, "A");
2354            }
2355            _ => panic!("expected completion list"),
2356        }
2357    }
2358}