Skip to main content

shape_lsp/
inlay_hints.rs

1//! Inlay hints provider for Shape
2//!
3//! Provides inline type hints for variables and parameter name hints for function calls.
4//! Uses the Visitor trait for exhaustive AST traversal.
5
6use std::collections::HashMap;
7
8use crate::type_inference::simplify_result_type;
9use crate::util::offset_to_position;
10use shape_ast::ast::expr_helpers::ComptimeForExpr;
11use shape_ast::ast::{
12    Expr, FunctionDef, Item, Program, Span, Spanned, Statement, TypeAnnotation, VariableDecl,
13};
14use shape_ast::parser::parse_program;
15use shape_runtime::visitor::{Visitor, walk_program};
16use tower_lsp_server::ls_types::{InlayHint, InlayHintKind, InlayHintLabel, Position, Range};
17
18use crate::type_inference::{
19    FunctionTypeInfo, ParamReferenceMode, infer_expr_type, infer_expr_type_via_engine,
20    infer_expr_type_with_env_public, infer_function_signatures, infer_program_types,
21    infer_program_types_with_context, infer_variable_type_for_display, unified_metadata,
22};
23
24/// Configuration for inlay hints
25#[derive(Debug, Clone)]
26pub struct InlayHintConfig {
27    pub show_type_hints: bool,
28    pub show_parameter_hints: bool,
29    /// Show `: type` hints after variable names in let/var/const without explicit annotations
30    pub show_variable_type_hints: bool,
31    /// Show `-> type` hints after function parameter lists without explicit return annotations
32    pub show_return_type_hints: bool,
33    /// W2.4 / 1.27: render the inferred type after every intermediate `.method()`
34    /// call in a method chain (e.g. `xs.map(f).filter(g).sum()` → hint after
35    /// `.map(f)` and `.filter(g)`).
36    pub show_chain_hints: bool,
37    /// W2.4 / 1.25 + LSP-I (R8 W11): render an approximate
38    /// `BindingStorageClass` label after `let`/`var` bindings. The five
39    /// ADR-006 §2 categories — `[Direct]` / `[UniqueHeap]` / `[SharedCow]` /
40    /// `[SharedAtomic]` / `[SharedAtomicMut]` — are rendered as best-effort
41    /// labels from the LSP-side AST (the authoritative classifier lives in
42    /// the bytecode compiler at `crates/shape-vm/src/type_tracking.rs:286`
43    /// + the MIR borrow solver). Always surfaced with the `[… approx]`
44    /// qualifier so the user knows it is not the compiler verdict.
45    ///
46    /// **Standing pattern (binding 2026-05-26):** Shape-unique inlay hints
47    /// (chain hints, binding-storage-class hints, anything not Rust/TS
48    /// idiomatic) ship **opt-in default-OFF** under the `shape.inlayHints.*`
49    /// namespace. The canonical LSP key for this toggle is
50    /// `shape.inlayHints.bindingStorageClass.enable: boolean` (Decision 2
51    /// ratify). The legacy `bindingKindHints` key (W2.4 / 1.25 vintage) is
52    /// accepted as an alias for backward compatibility; future inlay-hint
53    /// sub-clusters should follow the same opt-in default-OFF shape.
54    pub show_binding_kind_hints: bool,
55}
56
57impl Default for InlayHintConfig {
58    fn default() -> Self {
59        Self {
60            show_type_hints: true,
61            show_parameter_hints: true,
62            show_variable_type_hints: true,
63            show_return_type_hints: true,
64            show_chain_hints: true,
65            show_binding_kind_hints: false,
66        }
67    }
68}
69
70impl InlayHintConfig {
71    /// W2.4 / 1.70 + LSP-I (R8 W11): parse an `InlayHintConfig` from a
72    /// `workspace/configuration` JSON value. Honors keys such as
73    /// `shape.inlayHints.enable`, `shape.inlayHints.typeHints`,
74    /// `shape.inlayHints.parameterHints`, `shape.inlayHints.variableTypeHints`,
75    /// `shape.inlayHints.returnTypeHints`, `shape.inlayHints.chainHints`,
76    /// `shape.inlayHints.bindingStorageClass.enable` (canonical Decision 2 key
77    /// for binding-storage-class hints), and `shape.inlayHints.bindingKindHints`
78    /// (legacy W2.4 / 1.25 alias, retained for backward compatibility).
79    ///
80    /// snake_case aliases (e.g. `chain_hints`, `binding_storage_class`) are also
81    /// accepted. Unknown keys are ignored. Returns `Default::default()` when
82    /// the input is `None` or contains no recognized keys.
83    pub fn from_lsp_settings(value: Option<&serde_json::Value>) -> Self {
84        let mut cfg = Self::default();
85        let Some(root) = value else {
86            return cfg;
87        };
88
89        // Accept the nested `shape.inlayHints` shape, or a top-level
90        // `inlayHints` shape, or top-level direct keys.
91        let mut sources: Vec<&serde_json::Value> = Vec::new();
92        if let Some(shape) = root.get("shape") {
93            if let Some(ih) = shape.get("inlayHints").or_else(|| shape.get("inlay_hints")) {
94                sources.push(ih);
95            }
96        }
97        if let Some(ih) = root.get("inlayHints").or_else(|| root.get("inlay_hints")) {
98            sources.push(ih);
99        }
100        sources.push(root);
101
102        let mut master = None;
103        for src in &sources {
104            if let Some(v) = src.get("enable").and_then(serde_json::Value::as_bool) {
105                master = Some(v);
106                break;
107            }
108        }
109
110        let read_bool = |keys: &[&str]| -> Option<bool> {
111            for src in &sources {
112                for k in keys {
113                    if let Some(v) = src.get(*k).and_then(serde_json::Value::as_bool) {
114                        return Some(v);
115                    }
116                }
117            }
118            None
119        };
120
121        if let Some(v) = read_bool(&["typeHints", "type_hints"]) {
122            cfg.show_type_hints = v;
123        }
124        if let Some(v) = read_bool(&["parameterHints", "parameter_hints"]) {
125            cfg.show_parameter_hints = v;
126        }
127        if let Some(v) = read_bool(&["variableTypeHints", "variable_type_hints"]) {
128            cfg.show_variable_type_hints = v;
129        }
130        if let Some(v) = read_bool(&["returnTypeHints", "return_type_hints"]) {
131            cfg.show_return_type_hints = v;
132        }
133        if let Some(v) = read_bool(&["chainHints", "chain_hints"]) {
134            cfg.show_chain_hints = v;
135        }
136        if let Some(v) = read_bool(&["bindingKindHints", "binding_kind_hints"]) {
137            cfg.show_binding_kind_hints = v;
138        }
139
140        // LSP-I (R8 W11) Decision 2 canonical key:
141        // `shape.inlayHints.bindingStorageClass.enable: boolean`.
142        // Accepts either the nested-object shape
143        //   { "bindingStorageClass": { "enable": true } }
144        // (camelCase or snake_case "binding_storage_class"), or a flat boolean
145        //   { "bindingStorageClass": true }
146        // for client convenience. The legacy `bindingKindHints` parse above
147        // remains the backward-compatible alias.
148        for src in &sources {
149            let nested = src
150                .get("bindingStorageClass")
151                .or_else(|| src.get("binding_storage_class"));
152            if let Some(node) = nested {
153                if let Some(v) = node.as_bool() {
154                    cfg.show_binding_kind_hints = v;
155                    break;
156                }
157                if let Some(v) = node.get("enable").and_then(serde_json::Value::as_bool) {
158                    cfg.show_binding_kind_hints = v;
159                    break;
160                }
161            }
162        }
163
164        // Master enable: when explicitly set to false, suppress everything.
165        if master == Some(false) {
166            cfg.show_type_hints = false;
167            cfg.show_parameter_hints = false;
168            cfg.show_variable_type_hints = false;
169            cfg.show_return_type_hints = false;
170            cfg.show_chain_hints = false;
171            cfg.show_binding_kind_hints = false;
172        }
173
174        cfg
175    }
176}
177
178/// Context for collecting inlay hints.
179/// Implements the Visitor trait for exhaustive AST traversal.
180struct HintContext<'a> {
181    text: &'a str,
182    program: &'a Program,
183    range: Range,
184    config: &'a InlayHintConfig,
185    hints: Vec<InlayHint>,
186    /// Program-level type map from TypeInferenceEngine (primary) + heuristic (fallback)
187    type_map: HashMap<String, String>,
188    /// Per-function inferred parameter and return types from TypeInferenceEngine
189    function_types: HashMap<String, FunctionTypeInfo>,
190    /// Offsets at which a chain hint has already been emitted, used to dedupe
191    /// when the Visitor revisits inner MethodCall nodes of the chain spine
192    /// (W2.4 / 1.27).
193    chain_hint_offsets: std::collections::HashSet<usize>,
194}
195
196impl<'a> HintContext<'a> {
197    fn is_primitive_value_type_name(name: &str) -> bool {
198        let normalized = name.trim().trim_end_matches('?');
199        matches!(
200            normalized,
201            "int"
202                | "integer"
203                | "i64"
204                | "number"
205                | "float"
206                | "f64"
207                | "decimal"
208                | "bool"
209                | "boolean"
210                | "()"
211                | "void"
212                | "unit"
213                | "none"
214                | "null"
215                | "undefined"
216                | "never"
217        )
218    }
219
220    fn split_top_level_union(type_str: &str) -> Vec<String> {
221        let mut parts = Vec::new();
222        let mut start = 0usize;
223        let mut paren_depth = 0usize;
224        let mut bracket_depth = 0usize;
225        let mut brace_depth = 0usize;
226        let mut angle_depth = 0usize;
227
228        for (idx, ch) in type_str.char_indices() {
229            match ch {
230                '(' => paren_depth += 1,
231                ')' => paren_depth = paren_depth.saturating_sub(1),
232                '[' => bracket_depth += 1,
233                ']' => bracket_depth = bracket_depth.saturating_sub(1),
234                '{' => brace_depth += 1,
235                '}' => brace_depth = brace_depth.saturating_sub(1),
236                '<' => angle_depth += 1,
237                '>' => angle_depth = angle_depth.saturating_sub(1),
238                _ => {}
239            }
240            if ch == '|'
241                && paren_depth == 0
242                && bracket_depth == 0
243                && brace_depth == 0
244                && angle_depth == 0
245            {
246                parts.push(type_str[start..idx].trim().to_string());
247                start = idx + ch.len_utf8();
248            }
249        }
250
251        parts.push(type_str[start..].trim().to_string());
252        parts.into_iter().filter(|part| !part.is_empty()).collect()
253    }
254
255    fn apply_ref_prefix(type_str: &str, mode: &ParamReferenceMode) -> String {
256        let trimmed = type_str.trim();
257        if trimmed.starts_with('&') {
258            trimmed.to_string()
259        } else {
260            format!("{}{}", mode.prefix(), trimmed)
261        }
262    }
263
264    fn format_reference_aware_type(type_str: &str, mode: Option<&ParamReferenceMode>) -> String {
265        let Some(mode) = mode else {
266            return type_str.to_string();
267        };
268
269        let union_parts = Self::split_top_level_union(type_str);
270        if union_parts.len() <= 1 {
271            return Self::apply_ref_prefix(type_str, mode);
272        }
273
274        union_parts
275            .into_iter()
276            .map(|part| {
277                if Self::is_primitive_value_type_name(&part) {
278                    part
279                } else {
280                    Self::apply_ref_prefix(&part, mode)
281                }
282            })
283            .collect::<Vec<_>>()
284            .join(" | ")
285    }
286
287    fn new(
288        text: &'a str,
289        program: &'a Program,
290        range: Range,
291        config: &'a InlayHintConfig,
292        type_map: HashMap<String, String>,
293        function_types: HashMap<String, FunctionTypeInfo>,
294    ) -> Self {
295        Self {
296            text,
297            program,
298            range,
299            config,
300            hints: Vec::new(),
301            type_map,
302            function_types,
303            chain_hint_offsets: std::collections::HashSet::new(),
304        }
305    }
306
307    /// Collect type hint for a variable declaration without explicit type annotation.
308    /// Uses the program-level type_map (from TypeInferenceEngine) first,
309    /// falls back to heuristic infer_expr_type for unresolved variables.
310    fn collect_variable_type_hint(&mut self, decl: &VariableDecl) {
311        if !self.config.show_type_hints {
312            return;
313        }
314
315        let want_type_hint =
316            self.config.show_variable_type_hints && decl.type_annotation.is_none();
317        let want_binding_hint = self.config.show_binding_kind_hints;
318
319        if !want_type_hint && !want_binding_hint {
320            return;
321        }
322
323        // W2.4 / 1.04: when the RHS is a closure (`FunctionExpr`), prefer
324        // the LSP's `render_closure_signature` over the engine's
325        // `(unknown) -> _` format — the engine erases param types when
326        // unannotated; our renderer at least preserves source-annotated
327        // params and surfaces inferred return types.
328        let closure_signature: Option<String> = decl.value.as_ref().and_then(|v| {
329            if let Expr::FunctionExpr {
330                params,
331                return_type,
332                body,
333                ..
334            } = v
335            {
336                Some(crate::type_inference::render_closure_signature(
337                    params,
338                    return_type.as_ref(),
339                    body,
340                    &std::collections::HashMap::new(),
341                ))
342            } else {
343                None
344            }
345        });
346
347        // Try engine-inferred type from type_map first, fall back to heuristic
348        let var_name = decl.pattern.as_identifier();
349        let inferred_type = closure_signature.or_else(|| {
350            var_name
351                .and_then(|name| {
352                    decl.pattern
353                        .as_identifier_span()
354                        .and_then(|span| {
355                            if span.is_dummy() {
356                                None
357                            } else {
358                                infer_variable_type_for_display(self.program, name, span.end)
359                            }
360                        })
361                        .or_else(|| self.type_map.get(name).cloned())
362                })
363                .or_else(|| {
364                    // LSP-H: program-level type_map flows in as env so the
365                    // syntactic fallback can resolve identifier receivers in
366                    // method-chain RHS (`xs.map(...)` learns `xs: int[]`).
367                    decl.value
368                        .as_ref()
369                        .and_then(infer_expr_type_via_engine)
370                        .or_else(|| {
371                            decl.value
372                                .as_ref()
373                                .and_then(|v| infer_expr_type_with_env_public(v, &self.type_map))
374                        })
375                        .or_else(|| decl.value.as_ref().and_then(infer_expr_type))
376                })
377        });
378
379        let Some(span) = decl.pattern.as_identifier_span() else {
380            return;
381        };
382        if span.is_dummy() {
383            return;
384        }
385        let position = offset_to_position(self.text, span.end);
386        if !is_in_range(position, self.range) {
387            return;
388        }
389
390        // 1.21 type hint (existing behavior, behind want_type_hint).
391        if want_type_hint {
392            if let Some(inferred_type) = inferred_type.as_ref() {
393                // LSP-B: if the variable's initializer is a `&expr` / `&mut
394                // expr` reference expression, surface the reference mode in
395                // the inlay hint. The TypeInferenceEngine erases reference
396                // mode on `Expr::Reference` (infers the inner expr's type
397                // only) — without this prefix the hint would silently drop
398                // `&` and the rendered type would diverge from what the
399                // user could write as an annotation.
400                let label_type = match decl.value.as_ref() {
401                    Some(Expr::Reference { is_mutable, .. })
402                        if !inferred_type.starts_with('&') =>
403                    {
404                        let prefix = if *is_mutable { "&mut " } else { "&" };
405                        format!("{}{}", prefix, inferred_type)
406                    }
407                    _ => inferred_type.clone(),
408                };
409                self.hints.push(InlayHint {
410                    position,
411                    label: InlayHintLabel::String(format!(": {}", label_type)),
412                    kind: Some(InlayHintKind::TYPE),
413                    text_edits: None,
414                    tooltip: None,
415                    padding_left: Some(false),
416                    padding_right: Some(true),
417                    data: None,
418                });
419            }
420        }
421
422        // 1.25 binding-kind hint (LSP-side heuristic) — flagged on
423        // `show_binding_kind_hints`. The compiler's `BindingStorageClass`
424        // (`crates/shape-vm/src/type_tracking.rs:286`) is the authoritative
425        // classifier; this LSP heuristic uses the declared/inferred type +
426        // `is_mut` to render a plausible label without paying full bytecode
427        // compilation cost on every keystroke.
428        if want_binding_hint {
429            let label_type = decl
430                .type_annotation
431                .as_ref()
432                .and_then(crate::type_inference::type_annotation_to_string)
433                .or(inferred_type.clone());
434            let label = binding_kind_label_for(decl, label_type.as_deref());
435            self.hints.push(InlayHint {
436                position,
437                label: InlayHintLabel::String(label),
438                kind: Some(InlayHintKind::TYPE),
439                text_edits: None,
440                tooltip: Some(tower_lsp_server::ls_types::InlayHintTooltip::String(
441                    "LSP-side approximation of BindingStorageClass (ADR-006 §2). The compiler's bytecode pass at crates/shape-vm/src/type_tracking.rs:286 is authoritative.".to_string(),
442                )),
443                padding_left: Some(true),
444                padding_right: Some(true),
445                data: Some(serde_json::json!({
446                    "kind": "binding-kind",
447                    "name": decl.pattern.as_identifier().unwrap_or_default(),
448                })),
449            });
450        }
451    }
452
453    /// W2.4 / 1.27: emit a `: type` hint after an intermediate `.method()`
454    /// call in a method chain. Skips the chain's final call (its type is
455    /// already covered by `let x = chain` hints or by hover) and skips chains
456    /// whose receiver is not itself a method-call / property-access (single
457    /// `obj.method()` carries no chain).
458    fn collect_chain_hint(&mut self, expr: &Expr) {
459        if !self.config.show_type_hints || !self.config.show_chain_hints {
460            return;
461        }
462
463        let Expr::MethodCall { receiver, .. } = expr else {
464            return;
465        };
466
467        // Only emit hints for *intermediate* nodes — when this MethodCall is
468        // itself the receiver of an outer MethodCall, we handle it from the
469        // outer node so the chain renders once, in source order, top-down.
470        // To detect chain depth, walk down the receiver spine and emit hints
471        // for each intermediate node we find. Use a guard so we only handle
472        // the outermost call.
473        let outer_span = expr.span();
474        if outer_span.is_dummy() {
475            return;
476        }
477
478        // Walk receiver spine collecting intermediate method-call nodes whose
479        // result has an inferable type.
480        let mut spine: Vec<&Expr> = Vec::new();
481        let mut cur: &Expr = receiver.as_ref();
482        loop {
483            match cur {
484                Expr::MethodCall {
485                    receiver: inner, ..
486                } => {
487                    spine.push(cur);
488                    cur = inner.as_ref();
489                }
490                _ => break,
491            }
492        }
493
494        if spine.is_empty() {
495            return;
496        }
497
498        for node in spine {
499            let span = node.span();
500            if span.is_dummy() {
501                continue;
502            }
503            if !self.chain_hint_offsets.insert(span.end) {
504                continue;
505            }
506            let position = offset_to_position(self.text, span.end);
507            if !is_in_range(position, self.range) {
508                continue;
509            }
510            // Use the program-level type_map as env so an identifier receiver
511            // like `xs.filter(...)` can resolve `xs`'s type from the engine.
512            let inferred = infer_expr_type_via_engine(node)
513                .or_else(|| crate::type_inference::infer_expr_type_with_env_public(node, &self.type_map))
514                .or_else(|| infer_expr_type(node));
515            let Some(inferred) = inferred else {
516                continue;
517            };
518            self.hints.push(InlayHint {
519                position,
520                label: InlayHintLabel::String(format!(": {}", inferred)),
521                kind: Some(InlayHintKind::TYPE),
522                text_edits: None,
523                tooltip: None,
524                padding_left: Some(false),
525                padding_right: Some(false),
526                data: Some(serde_json::json!({
527                    "kind": "chain",
528                })),
529            });
530        }
531    }
532
533    /// Collect type hints for function parameters and return type.
534    /// Uses types inferred by the TypeInferenceEngine — no manual AST walking.
535    fn collect_function_type_hints(&mut self, func_def: &FunctionDef) {
536        if !self.config.show_type_hints {
537            return;
538        }
539
540        let info = match self.function_types.get(&func_def.name) {
541            Some(info) => info.clone(),
542            None => return,
543        };
544
545        // Parameter type hints: show `: type` after each unannotated parameter name
546        for (param_name, type_str) in &info.param_types {
547            // Find the matching AST parameter to get its span
548            if let Some(ast_param) = func_def
549                .params
550                .iter()
551                .find(|p| p.simple_name() == Some(param_name.as_str()))
552            {
553                let span = ast_param.span();
554                if !span.is_dummy() {
555                    let display_type = Self::format_reference_aware_type(
556                        type_str,
557                        info.param_ref_modes.get(param_name),
558                    );
559                    let position = offset_to_position(self.text, span.end);
560                    if is_in_range(position, self.range) {
561                        self.hints.push(InlayHint {
562                            position,
563                            label: InlayHintLabel::String(format!(": {}", display_type)),
564                            kind: Some(InlayHintKind::TYPE),
565                            text_edits: None,
566                            tooltip: None,
567                            padding_left: Some(false),
568                            padding_right: Some(true),
569                            data: None,
570                        });
571                    }
572                }
573            }
574        }
575
576        // Return type hint: show `-> type` after the closing `)` of the parameter list
577        if let Some(return_type) = &info.return_type {
578            if !self.config.show_return_type_hints {
579                return;
580            }
581            if let Some(hint_offset) = self.return_hint_offset(func_def) {
582                let position = offset_to_position(self.text, hint_offset);
583                if is_in_range(position, self.range) {
584                    let display_type = simplify_result_type(return_type);
585                    self.hints.push(InlayHint {
586                        position,
587                        label: InlayHintLabel::String(format!("-> {}", display_type)),
588                        kind: Some(InlayHintKind::TYPE),
589                        text_edits: None,
590                        tooltip: None,
591                        padding_left: Some(true),
592                        padding_right: Some(true),
593                        data: None,
594                    });
595                }
596            }
597        }
598    }
599
600    /// Compute an AST-driven offset for function return inlay hints.
601    ///
602    /// We anchor after the closing `)` of the parameter list when possible.
603    /// Fallback to the end of the last parameter span if the header cannot be
604    /// recovered from source text.
605    fn return_hint_offset(&self, func_def: &FunctionDef) -> Option<usize> {
606        let text_len = self.text.len();
607        let header_start = func_def.name_span.end.min(text_len);
608        let header_tail = &self.text[header_start..];
609        if let Some(open_brace_rel) = header_tail.find('{') {
610            let header = &header_tail[..open_brace_rel];
611            if let Some(close_paren_rel) = header.rfind(')') {
612                return Some(header_start + close_paren_rel + 1);
613            }
614        }
615
616        let last_param_end = func_def
617            .params
618            .iter()
619            .filter_map(|param| {
620                let span = param.span();
621                if span.is_dummy() {
622                    None
623                } else {
624                    Some(span.end)
625                }
626            })
627            .max();
628
629        if let Some(end) = last_param_end {
630            return Some(end);
631        }
632
633        if !func_def.name_span.is_dummy() {
634            return Some(func_def.name_span.end);
635        }
636
637        None
638    }
639
640    /// Collect parameter name hints for a function call
641    /// Show an inlay hint for `comptime for` indicating unrolled iteration count.
642    ///
643    /// When the iterable is `target.fields` and we can resolve the struct type,
644    /// shows the number of fields that will be unrolled. Otherwise shows a generic
645    /// "comptime unrolled" indicator.
646    fn collect_comptime_for_hint(&mut self, comptime_for: &ComptimeForExpr, span: &Span) {
647        if span.is_dummy() {
648            return;
649        }
650
651        // Try to resolve iteration count from iterable
652        let hint_label =
653            if let Expr::PropertyAccess { property, .. } = comptime_for.iterable.as_ref() {
654                if property == "fields" {
655                    // The iterable is `something.fields` — we can try to count struct fields
656                    // In practice, this requires knowing what `target` refers to, which needs
657                    // annotation context. For now, show a generic hint.
658                    "comptime unrolled".to_string()
659                } else {
660                    "comptime unrolled".to_string()
661                }
662            } else {
663                "comptime unrolled".to_string()
664            };
665
666        let position = offset_to_position(self.text, span.end);
667        if is_in_range(position, self.range) {
668            self.hints.push(InlayHint {
669                position,
670                label: InlayHintLabel::String(hint_label),
671                kind: Some(InlayHintKind::TYPE),
672                text_edits: None,
673                tooltip: Some(tower_lsp_server::ls_types::InlayHintTooltip::String(
674                    "This loop is unrolled at compile time by the comptime system.".to_string(),
675                )),
676                padding_left: Some(true),
677                padding_right: Some(false),
678                data: None,
679            });
680        }
681    }
682
683    /// Collect parameter-style hints for table row literals.
684    /// Shows struct field names before each positional element in `[a, b, c], [d, e, f]`
685    /// when the variable has a `Table<T>` type annotation.
686    fn collect_table_row_hints(&mut self, decl: &VariableDecl) {
687        if !self.config.show_parameter_hints {
688            return;
689        }
690
691        // Check if the init expression is TableRows
692        let rows = match &decl.value {
693            Some(Expr::TableRows(rows, _)) => rows,
694            _ => return,
695        };
696
697        // Extract inner type name from Table<T> annotation
698        let inner_type = match &decl.type_annotation {
699            Some(TypeAnnotation::Generic { name, args }) if name == "Table" => args
700                .first()
701                .and_then(|a| a.as_simple_name())
702                .map(String::from),
703            _ => None,
704        };
705        let inner_type = match inner_type {
706            Some(t) => t,
707            None => return,
708        };
709
710        // Find struct field names from the program
711        let field_names: Vec<String> = self
712            .program
713            .items
714            .iter()
715            .find_map(|item| {
716                if let Item::StructType(struct_def, _) = item {
717                    if struct_def.name == inner_type {
718                        Some(
719                            struct_def
720                                .fields
721                                .iter()
722                                .filter(|f| !f.is_comptime)
723                                .map(|f| f.name.clone())
724                                .collect(),
725                        )
726                    } else {
727                        None
728                    }
729                } else {
730                    None
731                }
732            })
733            .unwrap_or_default();
734
735        if field_names.is_empty() {
736            return;
737        }
738
739        // Emit parameter hints for each element in each row
740        for row in rows {
741            for (i, elem) in row.iter().enumerate() {
742                if let Some(field_name) = field_names.get(i) {
743                    let elem_span = elem.span();
744                    if !elem_span.is_dummy() {
745                        let position = offset_to_position(self.text, elem_span.start);
746                        if is_in_range(position, self.range) {
747                            self.hints.push(InlayHint {
748                                position,
749                                label: InlayHintLabel::String(format!("{}:", field_name)),
750                                kind: Some(InlayHintKind::PARAMETER),
751                                text_edits: None,
752                                tooltip: None,
753                                padding_left: Some(false),
754                                padding_right: Some(true),
755                                data: None,
756                            });
757                        }
758                    }
759                }
760            }
761        }
762    }
763
764    fn collect_parameter_hints(&mut self, args: &[Expr], func_name: &str) {
765        if func_name == "print" {
766            return;
767        }
768
769        let func_info = unified_metadata().get_function(func_name);
770
771        if let Some(func) = func_info {
772            for (i, arg) in args.iter().enumerate() {
773                if let Some(param) = func.parameters.get(i) {
774                    // Don't show hints for single-letter parameter names
775                    if param.name.len() > 1 {
776                        // Use the argument's span to get the position before it
777                        let arg_span = arg.span();
778                        if !arg_span.is_dummy() {
779                            let position = offset_to_position(self.text, arg_span.start);
780                            if is_in_range(position, self.range) {
781                                self.hints.push(InlayHint {
782                                    position,
783                                    label: InlayHintLabel::String(format!("{}:", param.name)),
784                                    kind: Some(InlayHintKind::PARAMETER),
785                                    text_edits: None,
786                                    tooltip: Some(
787                                        tower_lsp_server::ls_types::InlayHintTooltip::String(
788                                            param.description.clone(),
789                                        ),
790                                    ),
791                                    padding_left: Some(false),
792                                    padding_right: Some(true),
793                                    data: None,
794                                });
795                            }
796                        }
797                    }
798                }
799            }
800        }
801    }
802}
803
804impl<'a> Visitor for HintContext<'a> {
805    fn visit_item(&mut self, item: &Item) -> bool {
806        match item {
807            Item::VariableDecl(decl, _) => {
808                self.collect_variable_type_hint(decl);
809                self.collect_table_row_hints(decl);
810            }
811            Item::Function(func_def, _) => self.collect_function_type_hints(func_def),
812            _ => {}
813        }
814        true // Continue visiting children
815    }
816
817    fn visit_stmt(&mut self, stmt: &Statement) -> bool {
818        // Handle variable declarations at the statement level
819        if let Statement::VariableDecl(decl, _) = stmt {
820            self.collect_variable_type_hint(decl);
821            self.collect_table_row_hints(decl);
822        }
823        true // Continue visiting children
824    }
825
826    fn visit_expr(&mut self, expr: &Expr) -> bool {
827        // Handle function calls for parameter hints
828        if let Expr::FunctionCall { name, args, .. } = expr {
829            if self.config.show_parameter_hints {
830                self.collect_parameter_hints(args, name);
831            }
832        }
833
834        // Handle method-chain intermediate hints (W2.4 / 1.27)
835        if matches!(expr, Expr::MethodCall { .. }) {
836            self.collect_chain_hint(expr);
837        }
838
839        // Handle comptime for — show unroll hint
840        if let Expr::ComptimeFor(comptime_for, span) = expr {
841            if self.config.show_type_hints {
842                self.collect_comptime_for_hint(comptime_for, span);
843            }
844        }
845
846        true // Continue visiting children
847    }
848}
849
850/// Get inlay hints for a document within a range.
851///
852/// Hint positions must always be derived from the current text buffer.
853/// On parse errors we use resilient parsing of the current text and return no
854/// hints only if nothing can be recovered.
855pub fn get_inlay_hints(
856    text: &str,
857    range: Range,
858    config: &InlayHintConfig,
859    _cached_program: Option<&Program>,
860) -> Vec<InlayHint> {
861    get_inlay_hints_with_context(text, range, config, _cached_program, None, None)
862}
863
864/// Get inlay hints with optional file/workspace context for extension-aware inference.
865pub fn get_inlay_hints_with_context(
866    text: &str,
867    range: Range,
868    config: &InlayHintConfig,
869    _cached_program: Option<&Program>,
870    current_file: Option<&std::path::Path>,
871    workspace_root: Option<&std::path::Path>,
872) -> Vec<InlayHint> {
873    // Parse the current document; never use cached AST spans for hint placement.
874    let program = match parse_program(text) {
875        Ok(p) => p,
876        Err(_) => {
877            let partial = shape_ast::parse_program_resilient(text);
878            if partial.items.is_empty() {
879                return Vec::new();
880            }
881            partial.into_program()
882        }
883    };
884
885    // Run type inference once for the whole program
886    let type_map = if current_file.is_none() && workspace_root.is_none() {
887        infer_program_types(&program)
888    } else {
889        infer_program_types_with_context(&program, current_file, workspace_root, Some(text))
890    };
891    let function_types = infer_function_signatures(&program);
892
893    let mut ctx = HintContext::new(text, &program, range, config, type_map, function_types);
894
895    // Use the Visitor trait for exhaustive AST traversal
896    walk_program(&mut ctx, &program);
897
898    // Collect comptime value hints for type aliases
899    if config.show_type_hints {
900        collect_comptime_alias_hints(text, &program, range, &mut ctx.hints);
901    }
902
903    ctx.hints
904}
905
906/// Collect inlay hints showing resolved comptime values on type alias definitions.
907///
908/// For `type EUR = Currency { symbol: "EUR" }`, shows the full resolved comptime values
909/// as an inlay hint after the alias name, including inherited defaults.
910fn collect_comptime_alias_hints(
911    text: &str,
912    program: &shape_ast::ast::Program,
913    range: Range,
914    hints: &mut Vec<InlayHint>,
915) {
916    use std::collections::HashMap;
917
918    // First pass: collect struct definitions with comptime fields
919    let mut struct_comptime: HashMap<String, Vec<(String, Option<String>)>> = HashMap::new();
920    for item in &program.items {
921        if let Item::StructType(struct_def, _) = item {
922            let comptime_fields: Vec<(String, Option<String>)> = struct_def
923                .fields
924                .iter()
925                .filter(|f| f.is_comptime)
926                .map(|f| {
927                    let default = f.default_value.as_ref().map(format_comptime_value);
928                    (f.name.clone(), default)
929                })
930                .collect();
931            if !comptime_fields.is_empty() {
932                struct_comptime.insert(struct_def.name.clone(), comptime_fields);
933            }
934        }
935    }
936
937    // Second pass: for each type alias with overrides, show resolved values
938    for item in &program.items {
939        if let Item::TypeAlias(alias_def, span) = item {
940            let base_type = match &alias_def.type_annotation {
941                shape_ast::ast::TypeAnnotation::Basic(name) => name.clone(),
942                _ => continue,
943            };
944
945            let comptime_fields = match struct_comptime.get(&base_type) {
946                Some(fields) => fields,
947                None => continue,
948            };
949
950            // Build the resolved values: override > default
951            let resolved: Vec<String> = comptime_fields
952                .iter()
953                .filter_map(|(name, default)| {
954                    let value = alias_def
955                        .meta_param_overrides
956                        .as_ref()
957                        .and_then(|o| o.get(name))
958                        .map(format_comptime_value)
959                        .or_else(|| default.clone());
960                    value.map(|v| format!("{} = {}", name, v))
961                })
962                .collect();
963
964            if resolved.is_empty() {
965                continue;
966            }
967
968            let hint_offset = span.end;
969            let position = offset_to_position(text, hint_offset);
970            if is_in_range(position, range) {
971                hints.push(InlayHint {
972                    position,
973                    label: InlayHintLabel::String(format!(" [{}]", resolved.join(", "))),
974                    kind: Some(InlayHintKind::TYPE),
975                    text_edits: None,
976                    tooltip: Some(tower_lsp_server::ls_types::InlayHintTooltip::String(
977                        format!("Resolved comptime values from {}", base_type),
978                    )),
979                    padding_left: Some(false),
980                    padding_right: Some(true),
981                    data: None,
982                });
983            }
984        }
985    }
986}
987
988/// Format a comptime expression value for display
989fn format_comptime_value(expr: &Expr) -> String {
990    match expr {
991        Expr::Literal(lit, _) => match lit {
992            shape_ast::ast::Literal::String(s) => format!("\"{}\"", s),
993            shape_ast::ast::Literal::Number(n) => format!("{}", n),
994            shape_ast::ast::Literal::Int(n) => format!("{}", n),
995            shape_ast::ast::Literal::Decimal(d) => format!("{}D", d),
996            shape_ast::ast::Literal::Bool(b) => format!("{}", b),
997            shape_ast::ast::Literal::None => "None".to_string(),
998            _ => "...".to_string(),
999        },
1000        _ => "...".to_string(),
1001    }
1002}
1003
1004/// W2.4 / 1.25 + LSP-I (R8 W11): heuristic label approximating
1005/// `BindingStorageClass` (ADR-006 §2) for a `let`/`var` binding. This is
1006/// intentionally a best-effort LSP-side heuristic — the authoritative
1007/// classification requires the MIR borrow solver + storage planner. The
1008/// output uses the ADR-006 §2 vocabulary — `Direct`, `UniqueHeap`,
1009/// `SharedCow`, `SharedAtomic`, `SharedAtomicMut` — with an `[… approx]`
1010/// qualifier so the user knows it is not the compiler verdict.
1011///
1012/// **Mapping rules (LSP-side approximation):**
1013/// - RHS calls `Channel(...)`/`Channel.new(...)`/`Mutex(...)`/`Mutex.new(...)`,
1014///   declared type starts with `Channel`/`Mutex`, or the initializer is an
1015///   `async let` / `spawn { ... }` shape → `SharedAtomicMut` when the binding
1016///   is mutable, `SharedAtomic` otherwise (covers ADR-006 §2 line 119 / 143).
1017/// - RHS calls `Atomic(...)`/`Atomic.new(...)` or declared type starts with
1018///   `Atomic` → `SharedAtomic` (cross-thread read-shared, ADR-006 §2 line 118).
1019/// - Initializer is itself a closure `FunctionExpr` (the var IS a closure),
1020///   or the declared/inferred type contains `closure` / `Fn(`-like shape →
1021///   `SharedCow` (closures escape their defining scope; ADR-006 §2 line 117).
1022/// - Primitive value type (`int`, `number`, `bool`, ...) → `Direct`.
1023/// - Any other heap-resident type → `UniqueHeap` (ADR-006 §2 default).
1024/// - Reference initializer (`&expr` / `&mut expr`) → `Direct` (the binding
1025///   itself is a stack-resident typed pointer; ADR-006 §2 line 50).
1026fn binding_kind_label_for(decl: &VariableDecl, label_type: Option<&str>) -> String {
1027    let class = classify_binding_storage(decl, label_type);
1028    let mut_suffix = if decl.is_mut { " mut" } else { "" };
1029    format!("[{}{} approx]", class, mut_suffix)
1030}
1031
1032/// Return the ADR-006 §2 `BindingStorageClass` name as best the LSP-side
1033/// heuristic can determine. Names match the canonical ADR-006 §2 vocabulary
1034/// (`Direct`, `UniqueHeap`, `SharedCow`, `SharedAtomic`, `SharedAtomicMut`).
1035fn classify_binding_storage(decl: &VariableDecl, label_type: Option<&str>) -> &'static str {
1036    // Reference initializer: typed pointer carrier sits on the stack — Direct.
1037    if let Some(Expr::Reference { .. }) = decl.value.as_ref() {
1038        return "Direct";
1039    }
1040
1041    // Initializer-shape probes (RHS-driven). These dominate over type-driven
1042    // fallback when the user wrote an explicit concurrency-primitive construction.
1043    if let Some(value) = decl.value.as_ref() {
1044        if is_async_or_spawn_shape(value) {
1045            return if decl.is_mut { "SharedAtomicMut" } else { "SharedAtomic" };
1046        }
1047        if let Some(class) = concurrency_constructor_class(value, decl.is_mut) {
1048            return class;
1049        }
1050        if matches!(value, Expr::FunctionExpr { .. }) {
1051            // The variable IS a closure — closures escape their defining scope
1052            // by definition (ADR-006 §2 line 117).
1053            return "SharedCow";
1054        }
1055    }
1056
1057    // Type-driven fallback.
1058    if let Some(ty) = label_type {
1059        let t = ty.trim();
1060        let bare = t.trim_start_matches('&').trim_start_matches("mut ").trim();
1061        if is_primitive_value_type(bare) {
1062            return "Direct";
1063        }
1064        if bare.starts_with("Mutex") || bare.starts_with("Channel") {
1065            return "SharedAtomicMut";
1066        }
1067        if bare.starts_with("Atomic") {
1068            return "SharedAtomic";
1069        }
1070        if bare.starts_with("Arc<") {
1071            // Bare Arc<T> is read-shared cross-thread — SharedAtomic. Arc<Mutex<T>>
1072            // would be SharedAtomicMut but we'd need to peek inside.
1073            if bare.contains("Mutex<") {
1074                return "SharedAtomicMut";
1075            }
1076            return "SharedAtomic";
1077        }
1078        // Closure-typed binding (e.g. `(int) -> int`) — escapes when captured.
1079        if bare.contains("->") && (bare.starts_with('(') || bare.starts_with("Fn")) {
1080            return "SharedCow";
1081        }
1082        // Heap-resident default per ADR-006 §2 line 92.
1083        return "UniqueHeap";
1084    }
1085
1086    // No type info at all — pessimistic UniqueHeap default for heap-shaped
1087    // initializers, Direct otherwise.
1088    match decl.value.as_ref() {
1089        Some(Expr::Literal(lit, _)) => match lit {
1090            shape_ast::ast::Literal::Int(_)
1091            | shape_ast::ast::Literal::Number(_)
1092            | shape_ast::ast::Literal::Bool(_)
1093            | shape_ast::ast::Literal::None => "Direct",
1094            _ => "UniqueHeap",
1095        },
1096        _ => "UniqueHeap",
1097    }
1098}
1099
1100/// Detect RHS shapes that imply cross-thread/task escape (ADR-006 §2 line 143).
1101fn is_async_or_spawn_shape(expr: &Expr) -> bool {
1102    matches!(
1103        expr,
1104        Expr::AsyncLet(..) | Expr::AsyncScope(..) | Expr::Await(..) | Expr::Join(..)
1105    )
1106}
1107
1108/// Detect `Channel`/`Mutex`/`Atomic` constructor calls. Returns the appropriate
1109/// `BindingStorageClass` name when matched.
1110fn concurrency_constructor_class(expr: &Expr, is_mut: bool) -> Option<&'static str> {
1111    let head_name = match expr {
1112        Expr::FunctionCall { name, .. } => name.as_str(),
1113        Expr::QualifiedFunctionCall { function, .. } => function.as_str(),
1114        Expr::MethodCall { receiver, method, .. } => {
1115            // Pattern: `Channel.new(...)` / `Mutex.new(...)` etc. — the receiver
1116            // is the type-name `Identifier`, the method is `new`.
1117            if method == "new" || method == "open" || method == "with_capacity" {
1118                if let Expr::Identifier(name, _) = receiver.as_ref() {
1119                    name.as_str()
1120                } else {
1121                    return None;
1122                }
1123            } else {
1124                return None;
1125            }
1126        }
1127        _ => return None,
1128    };
1129
1130    // `Channel`/`Mutex` both imply interior mutability (ADR-006 §2.7.20:
1131    // Channel's `Mutex<ChannelInner>` shape; ADR-006 §2 line 119 for Mutex)
1132    // — surface as `SharedAtomicMut` regardless of the outer binding's
1133    // `let mut` flag, since the *mutation capability* is what the storage
1134    // class records.
1135    let _ = is_mut;
1136    match head_name {
1137        "Channel" | "Mutex" => Some("SharedAtomicMut"),
1138        "Atomic" => Some("SharedAtomic"),
1139        _ => None,
1140    }
1141}
1142
1143/// Match the same primitive vocabulary `HintContext::is_primitive_value_type_name`
1144/// uses but free-standing so the binding-kind label can share it. Keep the
1145/// two sets aligned.
1146fn is_primitive_value_type(name: &str) -> bool {
1147    let normalized = name.trim().trim_end_matches('?');
1148    matches!(
1149        normalized,
1150        "int"
1151            | "integer"
1152            | "i8"
1153            | "i16"
1154            | "i32"
1155            | "i64"
1156            | "u8"
1157            | "u16"
1158            | "u32"
1159            | "u64"
1160            | "number"
1161            | "float"
1162            | "f32"
1163            | "f64"
1164            | "bool"
1165            | "boolean"
1166            | "()"
1167            | "void"
1168            | "unit"
1169            | "none"
1170            | "null"
1171            | "undefined"
1172            | "never"
1173    )
1174}
1175
1176/// Check if a position is within a range
1177fn is_in_range(pos: Position, range: Range) -> bool {
1178    if pos.line < range.start.line || pos.line > range.end.line {
1179        return false;
1180    }
1181    if pos.line == range.start.line && pos.character < range.start.character {
1182        return false;
1183    }
1184    if pos.line == range.end.line && pos.character > range.end.character {
1185        return false;
1186    }
1187    true
1188}
1189
1190#[cfg(test)]
1191mod tests {
1192    use super::*;
1193    use crate::type_inference::infer_literal_type;
1194    use shape_ast::ast::Literal;
1195
1196    #[test]
1197    fn test_infer_literal_type() {
1198        assert_eq!(infer_literal_type(&Literal::Number(42.0)), "number");
1199        assert_eq!(
1200            infer_literal_type(&Literal::String("hello".to_string())),
1201            "string"
1202        );
1203        assert_eq!(infer_literal_type(&Literal::Bool(true)), "bool");
1204        assert_eq!(infer_literal_type(&Literal::None), "Option");
1205    }
1206
1207    #[test]
1208    fn test_infer_literal_type_int() {
1209        assert_eq!(infer_literal_type(&Literal::Int(42)), "int");
1210    }
1211
1212    #[test]
1213    fn test_infer_literal_type_decimal() {
1214        use rust_decimal::Decimal;
1215        assert_eq!(
1216            infer_literal_type(&Literal::Decimal(Decimal::new(1050, 2))),
1217            "decimal"
1218        );
1219    }
1220
1221    #[test]
1222    fn test_numeric_type_hints_int() {
1223        let config = InlayHintConfig::default();
1224        let range = Range {
1225            start: Position {
1226                line: 0,
1227                character: 0,
1228            },
1229            end: Position {
1230                line: 10,
1231                character: 100,
1232            },
1233        };
1234
1235        let hints = get_inlay_hints("let i = 10", range, &config, None);
1236        assert!(!hints.is_empty(), "Expected at least one hint for integer");
1237        let label = match &hints[0].label {
1238            InlayHintLabel::String(s) => s.clone(),
1239            _ => panic!("Expected string label"),
1240        };
1241        assert!(
1242            label.contains("int"),
1243            "Expected 'int' in hint, got: {}",
1244            label
1245        );
1246    }
1247
1248    #[test]
1249    fn test_numeric_type_hints_decimal() {
1250        let config = InlayHintConfig::default();
1251        let range = Range {
1252            start: Position {
1253                line: 0,
1254                character: 0,
1255            },
1256            end: Position {
1257                line: 10,
1258                character: 100,
1259            },
1260        };
1261
1262        let hints = get_inlay_hints("let d = 10D", range, &config, None);
1263        assert!(!hints.is_empty(), "Expected at least one hint for decimal");
1264        let label = match &hints[0].label {
1265            InlayHintLabel::String(s) => s.clone(),
1266            _ => panic!("Expected string label"),
1267        };
1268        assert!(
1269            label.contains("decimal"),
1270            "Expected 'decimal' in hint, got: {}",
1271            label
1272        );
1273    }
1274
1275    #[test]
1276    fn test_numeric_type_hints_number() {
1277        let config = InlayHintConfig::default();
1278        let range = Range {
1279            start: Position {
1280                line: 0,
1281                character: 0,
1282            },
1283            end: Position {
1284                line: 10,
1285                character: 100,
1286            },
1287        };
1288
1289        let hints = get_inlay_hints("let f = 10.0", range, &config, None);
1290        assert!(!hints.is_empty(), "Expected at least one hint for float");
1291        let label = match &hints[0].label {
1292            InlayHintLabel::String(s) => s.clone(),
1293            _ => panic!("Expected string label"),
1294        };
1295        assert!(
1296            label.contains("number"),
1297            "Expected 'number' in hint, got: {}",
1298            label
1299        );
1300    }
1301
1302    #[test]
1303    fn test_offset_to_position() {
1304        let text = "let x = 42;\nlet y = 10;";
1305        let pos = offset_to_position(text, 0);
1306        assert_eq!(pos.line, 0);
1307        assert_eq!(pos.character, 0);
1308
1309        let pos = offset_to_position(text, 12);
1310        assert_eq!(pos.line, 1);
1311        assert_eq!(pos.character, 0);
1312    }
1313
1314    #[test]
1315    fn test_match_expression_type_hint() {
1316        let config = InlayHintConfig::default();
1317        let range = Range {
1318            start: Position {
1319                line: 0,
1320                character: 0,
1321            },
1322            end: Position {
1323                line: 10,
1324                character: 100,
1325            },
1326        };
1327
1328        let code = "let test = match 2 {\n  0 => true,\n  _ => false,\n}";
1329        let hints = get_inlay_hints(code, range, &config, None);
1330        eprintln!(
1331            "Hints for match: {:?}",
1332            hints
1333                .iter()
1334                .map(|h| match &h.label {
1335                    InlayHintLabel::String(s) => s.clone(),
1336                    _ => "non-string".to_string(),
1337                })
1338                .collect::<Vec<_>>()
1339        );
1340        let type_hints: Vec<_> = hints
1341            .iter()
1342            .filter(|h| h.kind == Some(InlayHintKind::TYPE))
1343            .collect();
1344        assert!(
1345            !type_hints.is_empty(),
1346            "Expected a type hint for 'let test = match ...'"
1347        );
1348        let label = match &type_hints[0].label {
1349            InlayHintLabel::String(s) => s.clone(),
1350            _ => panic!("Expected string label"),
1351        };
1352        assert!(
1353            label.contains("bool"),
1354            "Expected 'bool' in hint, got: {}",
1355            label
1356        );
1357    }
1358
1359    // LSP-H regression: `let d = distance(p, q)` for a user-defined function
1360    // must propagate `-> number` (not `: any` / `: unknown`). Before LSP-H the
1361    // syntactic fallback was used with an empty env so identifier receivers
1362    // couldn't resolve through `.map(...)` chains; we now thread the
1363    // program-level type_map as env.
1364    #[test]
1365    fn lsp_h_type_hint_propagates_through_user_fn_call() {
1366        let code = "\
1367type Point { x: number, y: number }
1368fn distance(a: Point, b: Point) -> number { 0.0 }
1369let p = Point { x: 0.0, y: 0.0 }
1370let q = Point { x: 1.0, y: 1.0 }
1371let d = distance(p, q)
1372";
1373        let cfg = InlayHintConfig::default();
1374        let hints = get_inlay_hints(code, full_range(), &cfg, None);
1375        let labels = type_hint_labels(&hints);
1376        assert!(
1377            labels.iter().any(|l| l == ": number"),
1378            "expected `: number` for `let d = distance(...)`, got {:?}",
1379            labels
1380        );
1381    }
1382
1383    // LSP-H regression: `.map` chain element type recovered from the closure
1384    // body (was bare `Array` pre-LSP-H, leaking element type).
1385    #[test]
1386    fn lsp_h_map_chain_preserves_element_type() {
1387        let code = "\
1388let xs = [1, 2, 3]
1389let doubled = xs.map(|x| x * 2)
1390";
1391        let cfg = InlayHintConfig::default();
1392        let hints = get_inlay_hints(code, full_range(), &cfg, None);
1393        let labels = type_hint_labels(&hints);
1394        assert!(
1395            labels.iter().any(|l| l == ": Array<int>"),
1396            "expected `: Array<int>` for `let doubled = xs.map(...)`, got {:?}",
1397            labels
1398        );
1399        // Regression guard: bare `: Array` would indicate the leak returned.
1400        assert!(
1401            !labels.iter().any(|l| l == ": Array"),
1402            "regression: bare `: Array` leak returned, labels = {:?}",
1403            labels
1404        );
1405    }
1406
1407    // LSP-H new feature: chain hints after intermediate `.method()` calls
1408    // (`xs.map(...).sum()` shows `: Array<int>` after `.map` and `: number`
1409    // for the binding). Default-on per the LSP-H brief.
1410    #[test]
1411    fn lsp_h_chain_hint_emitted_for_intermediate_method() {
1412        let code = "\
1413let xs = [1, 2, 3]
1414let total = xs.map(|x| x * 2).sum()
1415";
1416        let cfg = InlayHintConfig::default();
1417        let hints = get_inlay_hints(code, full_range(), &cfg, None);
1418        let chain_hints: Vec<_> = hints
1419            .iter()
1420            .filter(|h| {
1421                h.data
1422                    .as_ref()
1423                    .and_then(|d| d.get("kind"))
1424                    .and_then(|k| k.as_str())
1425                    == Some("chain")
1426            })
1427            .collect();
1428        assert!(
1429            !chain_hints.is_empty(),
1430            "expected at least one chain hint on multi-`.` method chain, got hints = {:?}",
1431            hints
1432                .iter()
1433                .map(|h| match &h.label {
1434                    InlayHintLabel::String(s) => s.clone(),
1435                    _ => "non-string".to_string(),
1436                })
1437                .collect::<Vec<_>>()
1438        );
1439    }
1440
1441    #[test]
1442    fn test_infer_try_operator_type() {
1443        use shape_ast::ast::Span;
1444
1445        let expr = Expr::TryOperator(
1446            Box::new(Expr::FunctionCall {
1447                name: "some_func".to_string(),
1448                args: vec![],
1449                named_args: vec![],
1450                span: Span::DUMMY,
1451            }),
1452            Span::DUMMY,
1453        );
1454        let _ = infer_expr_type(&expr);
1455    }
1456
1457    fn full_range() -> Range {
1458        Range {
1459            start: Position {
1460                line: 0,
1461                character: 0,
1462            },
1463            end: Position {
1464                line: 100,
1465                character: 100,
1466            },
1467        }
1468    }
1469
1470    fn type_hint_labels(hints: &[InlayHint]) -> Vec<String> {
1471        hints
1472            .iter()
1473            .filter(|h| h.kind == Some(InlayHintKind::TYPE))
1474            .map(|h| match &h.label {
1475                InlayHintLabel::String(s) => s.clone(),
1476                _ => "non-string".to_string(),
1477            })
1478            .collect()
1479    }
1480
1481    #[test]
1482    fn test_function_return_type_hint() {
1483        let code = "fn add(a: int, b: int) {\n  return a + b\n}";
1484        let config = InlayHintConfig::default();
1485        let hints = get_inlay_hints(code, full_range(), &config, None);
1486        let labels = type_hint_labels(&hints);
1487        eprintln!("Return type hints: {:?}", labels);
1488        // Engine should infer a return type from the body
1489        let has_return_hint = labels.iter().any(|l| l.starts_with("->"));
1490        assert!(
1491            has_return_hint,
1492            "Expected a return type hint for fn without return annotation, got: {:?}",
1493            labels
1494        );
1495    }
1496
1497    #[test]
1498    fn test_function_return_hint_for_empty_params_anchors_after_close_paren() {
1499        let code = "fn test() {\n}\n";
1500        let config = InlayHintConfig::default();
1501        let hints = get_inlay_hints(code, full_range(), &config, None);
1502
1503        let return_hint = hints
1504            .iter()
1505            .find(|hint| match &hint.label {
1506                InlayHintLabel::String(label) => label.starts_with("->"),
1507                _ => false,
1508            })
1509            .expect("expected return type hint");
1510
1511        let expected_col = code
1512            .lines()
1513            .next()
1514            .and_then(|line| line.find(')'))
1515            .map(|idx| idx as u32 + 1)
1516            .expect("header should contain ')'");
1517
1518        assert_eq!(
1519            return_hint.position,
1520            Position {
1521                line: 0,
1522                character: expected_col
1523            }
1524        );
1525        match &return_hint.label {
1526            InlayHintLabel::String(label) => assert_eq!(label, "-> ()"),
1527            _ => panic!("expected string inlay label"),
1528        }
1529    }
1530
1531    #[test]
1532    fn test_print_parameter_hints_are_suppressed() {
1533        let code = "print(\"hello\")\n";
1534        let hints = get_inlay_hints(code, full_range(), &InlayHintConfig::default(), None);
1535        let has_parameter_hints = hints
1536            .iter()
1537            .any(|h| h.kind == Some(InlayHintKind::PARAMETER));
1538        assert!(
1539            !has_parameter_hints,
1540            "print() should not emit parameter hints, got: {:?}",
1541            hints
1542        );
1543    }
1544
1545    #[test]
1546    fn test_function_param_type_hint_not_shown_when_annotated() {
1547        let code = "fn greet(name: string) {\n  return name\n}";
1548        let config = InlayHintConfig::default();
1549        let hints = get_inlay_hints(code, full_range(), &config, None);
1550        let labels = type_hint_labels(&hints);
1551        // Parameter already has type annotation — should NOT show a hint for it
1552        let has_param_hint = labels
1553            .iter()
1554            .any(|l| l.contains("string") && l.starts_with(":"));
1555        assert!(
1556            !has_param_hint,
1557            "Should not show param type hint when annotation exists, got: {:?}",
1558            labels
1559        );
1560    }
1561
1562    #[test]
1563    fn test_function_no_hint_when_return_annotated() {
1564        let code = "fn double(x: int) -> int {\n  return x * 2\n}";
1565        let config = InlayHintConfig::default();
1566        let hints = get_inlay_hints(code, full_range(), &config, None);
1567        let labels = type_hint_labels(&hints);
1568        // Both param and return are annotated — no type hints expected
1569        let has_return_hint = labels.iter().any(|l| l.starts_with("->"));
1570        assert!(
1571            !has_return_hint,
1572            "Should not show return type hint when annotation exists, got: {:?}",
1573            labels
1574        );
1575    }
1576
1577    #[test]
1578    fn test_function_param_hint_shows_inferred_shared_reference() {
1579        let code = r#"
1580fn read_only(a) {
1581  return a.len()
1582}
1583let s = "abc"
1584read_only(s)
1585"#;
1586        let hints = get_inlay_hints(code, full_range(), &InlayHintConfig::default(), None);
1587        let labels = type_hint_labels(&hints);
1588        assert!(
1589            labels.iter().any(|l| l == ": &string"),
1590            "Expected inferred shared reference hint ': &string', got: {:?}",
1591            labels
1592        );
1593    }
1594
1595    #[test]
1596    fn test_function_param_hint_shows_inferred_exclusive_reference() {
1597        let code = r#"
1598fn write_ref(a) {
1599  a = a + "!"
1600  return a
1601}
1602let s = "abc"
1603write_ref(s)
1604"#;
1605        let hints = get_inlay_hints(code, full_range(), &InlayHintConfig::default(), None);
1606        let labels = type_hint_labels(&hints);
1607        assert!(
1608            labels.iter().any(|l| l == ": &mut string"),
1609            "Expected inferred exclusive reference hint ': &mut string', got: {:?}",
1610            labels
1611        );
1612    }
1613
1614    #[test]
1615    fn test_function_param_hint_union_is_memberwise_reference_aware() {
1616        let code = r#"
1617fn foo(a) { return a }
1618let i = foo(1)
1619let s = foo("hi")
1620"#;
1621        let hints = get_inlay_hints(code, full_range(), &InlayHintConfig::default(), None);
1622        let labels = type_hint_labels(&hints);
1623        let union_hint = labels
1624            .iter()
1625            .find(|l| l.starts_with(":") && l.contains("int") && l.contains("string"))
1626            .cloned()
1627            .unwrap_or_default();
1628        assert!(
1629            union_hint.contains("&string"),
1630            "Expected union hint to show reference-aware heap member, got: {:?}",
1631            labels
1632        );
1633        assert!(
1634            union_hint.contains("int"),
1635            "Expected union hint to keep primitive member by value, got: {:?}",
1636            labels
1637        );
1638    }
1639
1640    #[test]
1641    fn test_variable_type_hint_disabled() {
1642        let config = InlayHintConfig {
1643            show_variable_type_hints: false,
1644            ..InlayHintConfig::default()
1645        };
1646        let hints = get_inlay_hints("let x = 42", full_range(), &config, None);
1647        let type_labels = type_hint_labels(&hints);
1648        assert!(
1649            type_labels.is_empty(),
1650            "Should not show variable type hints when disabled, got: {:?}",
1651            type_labels
1652        );
1653    }
1654
1655    #[test]
1656    fn test_return_type_hint_disabled() {
1657        let config = InlayHintConfig {
1658            show_return_type_hints: false,
1659            ..InlayHintConfig::default()
1660        };
1661        let code = "fn add(a: int, b: int) {\n  return a + b\n}";
1662        let hints = get_inlay_hints(code, full_range(), &config, None);
1663        let labels = type_hint_labels(&hints);
1664        let has_return_hint = labels.iter().any(|l| l.starts_with("->"));
1665        assert!(
1666            !has_return_hint,
1667            "Should not show return type hints when disabled, got: {:?}",
1668            labels
1669        );
1670    }
1671
1672    #[test]
1673    fn test_variable_inside_function_gets_hint() {
1674        let config = InlayHintConfig::default();
1675        let code = "fn foo() {\n  let x = 42\n  return x\n}";
1676        let hints = get_inlay_hints(code, full_range(), &config, None);
1677        let type_labels = type_hint_labels(&hints);
1678        let has_int_hint = type_labels.iter().any(|l| l.contains("int"));
1679        assert!(
1680            has_int_hint,
1681            "Should show type hint for variable inside function body, got: {:?}",
1682            type_labels
1683        );
1684    }
1685
1686    #[test]
1687    fn test_string_variable_hint() {
1688        let config = InlayHintConfig::default();
1689        let hints = get_inlay_hints("let name = \"hello\"", full_range(), &config, None);
1690        let labels = type_hint_labels(&hints);
1691        let has_string = labels.iter().any(|l| l.contains("string"));
1692        assert!(
1693            has_string,
1694            "Should show 'string' hint for string literal, got: {:?}",
1695            labels
1696        );
1697    }
1698
1699    #[test]
1700    fn test_bool_variable_hint() {
1701        let config = InlayHintConfig::default();
1702        let hints = get_inlay_hints("let flag = true", full_range(), &config, None);
1703        let labels = type_hint_labels(&hints);
1704        let has_bool = labels.iter().any(|l| l.contains("bool"));
1705        assert!(
1706            has_bool,
1707            "Should show 'bool' hint for bool literal, got: {:?}",
1708            labels
1709        );
1710    }
1711
1712    #[test]
1713    fn test_no_hint_when_type_annotated() {
1714        let config = InlayHintConfig::default();
1715        let hints = get_inlay_hints("let x: int = 42", full_range(), &config, None);
1716        let type_labels = type_hint_labels(&hints);
1717        // Should not produce a type hint since the variable already has an annotation
1718        let has_int = type_labels.iter().any(|l| l.contains("int"));
1719        assert!(
1720            !has_int,
1721            "Should not show type hint when annotation exists, got: {:?}",
1722            type_labels
1723        );
1724    }
1725
1726    #[test]
1727    fn test_table_row_literal_field_hints() {
1728        let code = r#"type FinRecord {
1729  month: int,
1730  revenue: number,
1731  profit: number,
1732  note: string
1733}
1734let t: Table<FinRecord> = [1, 100.0, 60.0, "jan"], [2, 120.0, 70.0, "feb"]
1735"#;
1736        let config = InlayHintConfig::default();
1737        let hints = get_inlay_hints(code, full_range(), &config, None);
1738        let param_hints: Vec<String> = hints
1739            .iter()
1740            .filter(|h| h.kind == Some(InlayHintKind::PARAMETER))
1741            .map(|h| match &h.label {
1742                InlayHintLabel::String(s) => s.clone(),
1743                _ => "non-string".to_string(),
1744            })
1745            .collect();
1746        // Should have 8 parameter hints (4 fields x 2 rows)
1747        assert_eq!(
1748            param_hints.len(),
1749            8,
1750            "Expected 8 parameter hints for 2 rows x 4 fields, got: {:?}",
1751            param_hints
1752        );
1753        assert_eq!(param_hints[0], "month:");
1754        assert_eq!(param_hints[1], "revenue:");
1755        assert_eq!(param_hints[2], "profit:");
1756        assert_eq!(param_hints[3], "note:");
1757        // Second row repeats
1758        assert_eq!(param_hints[4], "month:");
1759        assert_eq!(param_hints[7], "note:");
1760    }
1761
1762    // ---------- W2.4 / 1.04: closure type rendering ----------
1763
1764    #[test]
1765    fn test_closure_type_hint_renders_signature() {
1766        // A `let f = |y| y + 1` should render `fn(_) -> int` (or similar)
1767        // rather than the bare "Function".
1768        let code = "let f = |y| y + 1\n";
1769        let hints = get_inlay_hints(code, full_range(), &InlayHintConfig::default(), None);
1770        let labels = type_hint_labels(&hints);
1771        let has_fn_sig = labels
1772            .iter()
1773            .any(|l| l.starts_with(": fn(") && l.contains("->"));
1774        assert!(
1775            has_fn_sig,
1776            "Expected closure type hint like ': fn(_) -> int', got: {:?}",
1777            labels
1778        );
1779    }
1780
1781    #[test]
1782    fn test_closure_type_hint_no_param_annotation_still_renders_signature() {
1783        let code = "let f = |x| 42\n";
1784        let hints = get_inlay_hints(code, full_range(), &InlayHintConfig::default(), None);
1785        let labels = type_hint_labels(&hints);
1786        let has_fn_sig = labels.iter().any(|l| l.starts_with(": fn(") && l.contains("->"));
1787        assert!(
1788            has_fn_sig,
1789            "Expected closure type hint with `_` for unannotated param, got: {:?}",
1790            labels
1791        );
1792    }
1793
1794    // ---------- W2.4 / 1.27: chain hints ----------
1795
1796    #[test]
1797    fn test_chain_hints_emit_for_intermediate_method_calls() {
1798        // `xs.filter(...).reverse().sort()` should produce hints at each
1799        // intermediate `.method()` call site (chain spine length == 3).
1800        let code = r#"
1801let xs = [1, 2, 3]
1802let r = xs.filter(|x| x > 0).reverse().sort()
1803"#;
1804        let hints = get_inlay_hints(code, full_range(), &InlayHintConfig::default(), None);
1805        let chain_hints: Vec<_> = hints
1806            .iter()
1807            .filter(|h| {
1808                h.data
1809                    .as_ref()
1810                    .and_then(|d| d.get("kind"))
1811                    .and_then(|v| v.as_str())
1812                    == Some("chain")
1813            })
1814            .collect();
1815        assert!(
1816            !chain_hints.is_empty(),
1817            "Expected at least one chain hint, got hints: {:?}",
1818            hints.iter().map(|h| &h.label).collect::<Vec<_>>()
1819        );
1820    }
1821
1822    #[test]
1823    fn test_chain_hints_disabled_emits_none() {
1824        let code = r#"
1825let xs = [1, 2, 3]
1826let r = xs.filter(|x| x > 0).reverse().sort()
1827"#;
1828        let cfg = InlayHintConfig {
1829            show_chain_hints: false,
1830            ..InlayHintConfig::default()
1831        };
1832        let hints = get_inlay_hints(code, full_range(), &cfg, None);
1833        let any_chain = hints.iter().any(|h| {
1834            h.data
1835                .as_ref()
1836                .and_then(|d| d.get("kind"))
1837                .and_then(|v| v.as_str())
1838                == Some("chain")
1839        });
1840        assert!(
1841            !any_chain,
1842            "Expected no chain hints when disabled, got: {:?}",
1843            hints.iter().map(|h| &h.label).collect::<Vec<_>>()
1844        );
1845    }
1846
1847    // ---------- W2.4 / 1.25: binding-kind hints ----------
1848
1849    #[test]
1850    fn test_binding_kind_hint_emits_for_primitive_let() {
1851        let code = "let x = 42\n";
1852        let cfg = InlayHintConfig {
1853            show_binding_kind_hints: true,
1854            ..InlayHintConfig::default()
1855        };
1856        let hints = get_inlay_hints(code, full_range(), &cfg, None);
1857        let any_kind = hints.iter().any(|h| {
1858            h.data
1859                .as_ref()
1860                .and_then(|d| d.get("kind"))
1861                .and_then(|v| v.as_str())
1862                == Some("binding-kind")
1863        });
1864        assert!(
1865            any_kind,
1866            "Expected a binding-kind hint, got: {:?}",
1867            hints
1868                .iter()
1869                .map(|h| (h.label.clone(), h.data.clone()))
1870                .collect::<Vec<_>>()
1871        );
1872    }
1873
1874    #[test]
1875    fn test_binding_kind_label_covers_five_adr006_categories() {
1876        // LSP-I (R8 W11): every ADR-006 §2 BindingStorageClass main category
1877        // (Direct / UniqueHeap / SharedCow / SharedAtomic / SharedAtomicMut)
1878        // must have a label-rendering path through the LSP-side heuristic.
1879        let cfg = InlayHintConfig {
1880            show_binding_kind_hints: true,
1881            ..InlayHintConfig::default()
1882        };
1883
1884        let cases: &[(&str, &str)] = &[
1885            ("let x = 42\n", "[Direct approx]"),
1886            ("let s = \"hello\"\n", "[UniqueHeap approx]"),
1887            ("let f = |x| x + 1\n", "[SharedCow approx]"),
1888            ("let a = Atomic.new(0)\n", "[SharedAtomic approx]"),
1889            ("let mut q = Channel.new()\n", "[SharedAtomicMut mut approx]"),
1890        ];
1891
1892        for (code, expected) in cases {
1893            let hints = get_inlay_hints(code, full_range(), &cfg, None);
1894            let labels: Vec<String> = hints
1895                .iter()
1896                .filter_map(|h| match &h.label {
1897                    InlayHintLabel::String(s) => Some(s.clone()),
1898                    _ => None,
1899                })
1900                .collect();
1901            assert!(
1902                labels.iter().any(|l| l == expected),
1903                "code {:?} expected label {:?}, got labels {:?}",
1904                code,
1905                expected,
1906                labels
1907            );
1908        }
1909    }
1910
1911    #[test]
1912    fn test_binding_kind_hint_off_by_default() {
1913        let code = "let x = 42\n";
1914        let hints = get_inlay_hints(code, full_range(), &InlayHintConfig::default(), None);
1915        let any_kind = hints.iter().any(|h| {
1916            h.data
1917                .as_ref()
1918                .and_then(|d| d.get("kind"))
1919                .and_then(|v| v.as_str())
1920                == Some("binding-kind")
1921        });
1922        assert!(
1923            !any_kind,
1924            "Binding-kind hints should be opt-in (default off); got: {:?}",
1925            hints.iter().map(|h| h.label.clone()).collect::<Vec<_>>()
1926        );
1927    }
1928
1929    // ---------- W2.4 / 1.70: client-respecting config ----------
1930
1931    #[test]
1932    fn test_from_lsp_settings_master_disable() {
1933        let json = serde_json::json!({
1934            "shape": {
1935                "inlayHints": {
1936                    "enable": false
1937                }
1938            }
1939        });
1940        let cfg = InlayHintConfig::from_lsp_settings(Some(&json));
1941        assert!(!cfg.show_type_hints);
1942        assert!(!cfg.show_parameter_hints);
1943        assert!(!cfg.show_variable_type_hints);
1944        assert!(!cfg.show_return_type_hints);
1945        assert!(!cfg.show_chain_hints);
1946        assert!(!cfg.show_binding_kind_hints);
1947    }
1948
1949    #[test]
1950    fn test_from_lsp_settings_individual_toggles() {
1951        let json = serde_json::json!({
1952            "shape": {
1953                "inlayHints": {
1954                    "parameterHints": false,
1955                    "chainHints": false,
1956                    "bindingKindHints": true
1957                }
1958            }
1959        });
1960        let cfg = InlayHintConfig::from_lsp_settings(Some(&json));
1961        assert!(cfg.show_type_hints);
1962        assert!(!cfg.show_parameter_hints);
1963        assert!(!cfg.show_chain_hints);
1964        assert!(cfg.show_binding_kind_hints);
1965    }
1966
1967    #[test]
1968    fn test_from_lsp_settings_snake_case_alias_accepted() {
1969        let json = serde_json::json!({
1970            "inlay_hints": {
1971                "binding_kind_hints": true,
1972                "chain_hints": false
1973            }
1974        });
1975        let cfg = InlayHintConfig::from_lsp_settings(Some(&json));
1976        assert!(cfg.show_binding_kind_hints);
1977        assert!(!cfg.show_chain_hints);
1978    }
1979
1980    // ---------- LSP-I (R8 W11): bindingStorageClass.enable canonical key ----------
1981
1982    #[test]
1983    fn test_from_lsp_settings_binding_storage_class_nested_enable_on() {
1984        let json = serde_json::json!({
1985            "shape": {
1986                "inlayHints": {
1987                    "bindingStorageClass": { "enable": true }
1988                }
1989            }
1990        });
1991        let cfg = InlayHintConfig::from_lsp_settings(Some(&json));
1992        assert!(
1993            cfg.show_binding_kind_hints,
1994            "Decision 2 canonical key bindingStorageClass.enable=true must opt the user in"
1995        );
1996    }
1997
1998    #[test]
1999    fn test_from_lsp_settings_binding_storage_class_default_off() {
2000        // Decision 2 mandate: default is OFF when the user has not set the key.
2001        let json = serde_json::json!({
2002            "shape": { "inlayHints": {} }
2003        });
2004        let cfg = InlayHintConfig::from_lsp_settings(Some(&json));
2005        assert!(
2006            !cfg.show_binding_kind_hints,
2007            "Decision 2 default OFF — no opt-in implies hint stays off"
2008        );
2009    }
2010
2011    #[test]
2012    fn test_from_lsp_settings_binding_storage_class_snake_case_alias() {
2013        let json = serde_json::json!({
2014            "shape": {
2015                "inlayHints": {
2016                    "binding_storage_class": { "enable": true }
2017                }
2018            }
2019        });
2020        let cfg = InlayHintConfig::from_lsp_settings(Some(&json));
2021        assert!(cfg.show_binding_kind_hints);
2022    }
2023
2024    #[test]
2025    fn test_from_lsp_settings_binding_storage_class_flat_bool() {
2026        let json = serde_json::json!({
2027            "shape": {
2028                "inlayHints": {
2029                    "bindingStorageClass": true
2030                }
2031            }
2032        });
2033        let cfg = InlayHintConfig::from_lsp_settings(Some(&json));
2034        assert!(cfg.show_binding_kind_hints);
2035    }
2036
2037    #[test]
2038    fn test_from_lsp_settings_legacy_binding_kind_hints_still_honored() {
2039        // Backward compat: the older W2.4 / 1.25 `bindingKindHints` key still
2040        // toggles the same field so existing clients don't break.
2041        let json = serde_json::json!({
2042            "shape": {
2043                "inlayHints": {
2044                    "bindingKindHints": true
2045                }
2046            }
2047        });
2048        let cfg = InlayHintConfig::from_lsp_settings(Some(&json));
2049        assert!(cfg.show_binding_kind_hints);
2050    }
2051
2052    #[test]
2053    fn test_default_inlay_hint_config_keeps_binding_storage_class_off() {
2054        let cfg = InlayHintConfig::default();
2055        assert!(
2056            !cfg.show_binding_kind_hints,
2057            "Default InlayHintConfig must keep binding-storage-class hint OFF \
2058             (Decision 2: Shape-unique inlay hints are opt-in default-OFF)."
2059        );
2060    }
2061
2062    #[test]
2063    fn test_from_lsp_settings_none_returns_defaults() {
2064        let cfg = InlayHintConfig::from_lsp_settings(None);
2065        let default_cfg = InlayHintConfig::default();
2066        assert_eq!(cfg.show_type_hints, default_cfg.show_type_hints);
2067        assert_eq!(cfg.show_chain_hints, default_cfg.show_chain_hints);
2068        assert_eq!(
2069            cfg.show_binding_kind_hints,
2070            default_cfg.show_binding_kind_hints
2071        );
2072    }
2073
2074    #[test]
2075    fn test_parse_error_with_no_recoverable_items_emits_no_hints() {
2076        let code = r#"
2077from std.core.snapshot import { Snapshot }
2078
2079let x = {x: 1}
2080x.y = 1
2081let i = 10D
2082"#;
2083        let hints = get_inlay_hints(code, full_range(), &InlayHintConfig::default(), None);
2084
2085        assert!(
2086            hints.is_empty(),
2087            "invalid parse with no recoverable AST should not emit hints"
2088        );
2089    }
2090
2091    // W14.2-B1: per-line coverage additions
2092    #[test]
2093    fn test_is_primitive_value_type_inlay() {
2094        assert!(is_primitive_value_type("int"));
2095        assert!(is_primitive_value_type("number"));
2096        assert!(is_primitive_value_type("bool"));
2097        assert!(is_primitive_value_type("i32"));
2098        assert!(is_primitive_value_type("f64"));
2099        assert!(is_primitive_value_type("int?"));
2100        assert!(is_primitive_value_type("null"));
2101        assert!(is_primitive_value_type("void"));
2102        // string is NOT a value primitive here (it's a heap-allocated ref type)
2103        assert!(!is_primitive_value_type("string"));
2104        assert!(!is_primitive_value_type(""));
2105        assert!(!is_primitive_value_type("MyType"));
2106        assert!(!is_primitive_value_type("Array<int>"));
2107    }
2108
2109    #[test]
2110    fn test_is_in_range_inside() {
2111        let range = Range {
2112            start: Position {
2113                line: 0,
2114                character: 0,
2115            },
2116            end: Position {
2117                line: 10,
2118                character: 50,
2119            },
2120        };
2121        assert!(is_in_range(
2122            Position {
2123                line: 5,
2124                character: 25,
2125            },
2126            range
2127        ));
2128    }
2129
2130    #[test]
2131    fn test_is_in_range_outside() {
2132        let range = Range {
2133            start: Position {
2134                line: 0,
2135                character: 0,
2136            },
2137            end: Position {
2138                line: 5,
2139                character: 0,
2140            },
2141        };
2142        // Past the end line
2143        assert!(!is_in_range(
2144            Position {
2145                line: 10,
2146                character: 0,
2147            },
2148            range
2149        ));
2150    }
2151
2152    #[test]
2153    fn test_format_comptime_value_number() {
2154        let expr = Expr::Literal(Literal::Int(42), Span::DUMMY);
2155        let formatted = format_comptime_value(&expr);
2156        assert!(
2157            formatted.contains("42"),
2158            "expected '42' in formatted output, got {formatted:?}"
2159        );
2160    }
2161
2162    #[test]
2163    fn test_format_comptime_value_string() {
2164        let expr = Expr::Literal(Literal::String("hi".to_string()), Span::DUMMY);
2165        let formatted = format_comptime_value(&expr);
2166        assert!(
2167            formatted.contains("hi") || formatted.contains("\""),
2168            "expected string-ish in formatted output, got {formatted:?}"
2169        );
2170    }
2171
2172    #[test]
2173    fn test_get_inlay_hints_empty_source() {
2174        let hints = get_inlay_hints("", full_range(), &InlayHintConfig::default(), None);
2175        assert!(hints.is_empty(), "expected no hints for empty source");
2176    }
2177
2178    #[test]
2179    fn test_get_inlay_hints_respects_range_filter() {
2180        let code = "let x = 1\nlet y = 2\nlet z = 3\n";
2181        let limited_range = Range {
2182            start: Position {
2183                line: 0,
2184                character: 0,
2185            },
2186            end: Position {
2187                line: 0,
2188                character: 100,
2189            },
2190        };
2191        let hints = get_inlay_hints(code, limited_range, &InlayHintConfig::default(), None);
2192        // Only hints for line 0 should be present
2193        for h in &hints {
2194            assert_eq!(
2195                h.position.line, 0,
2196                "expected only line 0 hints when range is limited to line 0"
2197            );
2198        }
2199    }
2200
2201    #[test]
2202    fn test_inlay_hints_config_show_type_hints_default() {
2203        let cfg = InlayHintConfig::default();
2204        assert!(cfg.show_type_hints, "show_type_hints should default to true");
2205    }
2206
2207    #[test]
2208    fn test_inlay_hints_with_disabled_type_hints() {
2209        let cfg = InlayHintConfig {
2210            show_type_hints: false,
2211            ..InlayHintConfig::default()
2212        };
2213        let hints = get_inlay_hints("let x = 1\n", full_range(), &cfg, None);
2214        // With type hints disabled, no TYPE hints should appear.
2215        assert!(
2216            hints.iter().all(|h| h.kind != Some(InlayHintKind::TYPE)),
2217            "expected no TYPE hints when show_type_hints=false"
2218        );
2219    }
2220
2221    #[test]
2222    fn test_get_inlay_hints_function_definition() {
2223        let code = "fn add(a, b) { return a + b }\n";
2224        let hints = get_inlay_hints(code, full_range(), &InlayHintConfig::default(), None);
2225        // Should produce parameter type hints (Variable kind).
2226        let _ = hints; // just-don't-crash check on the inferred-param flow
2227    }
2228
2229    #[test]
2230    fn test_format_comptime_value_bool() {
2231        let expr = Expr::Literal(Literal::Bool(true), Span::DUMMY);
2232        let formatted = format_comptime_value(&expr);
2233        assert!(
2234            formatted.contains("true") || !formatted.is_empty(),
2235            "expected non-empty formatted output for bool, got {formatted:?}"
2236        );
2237    }
2238}