1use 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#[derive(Debug, Clone)]
26pub struct InlayHintConfig {
27 pub show_type_hints: bool,
28 pub show_parameter_hints: bool,
29 pub show_variable_type_hints: bool,
31 pub show_return_type_hints: bool,
33 pub show_chain_hints: bool,
37 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 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 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 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 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
178struct HintContext<'a> {
181 text: &'a str,
182 program: &'a Program,
183 range: Range,
184 config: &'a InlayHintConfig,
185 hints: Vec<InlayHint>,
186 type_map: HashMap<String, String>,
188 function_types: HashMap<String, FunctionTypeInfo>,
190 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 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 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 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 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 if want_type_hint {
392 if let Some(inferred_type) = inferred_type.as_ref() {
393 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 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 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 let outer_span = expr.span();
474 if outer_span.is_dummy() {
475 return;
476 }
477
478 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 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 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 for (param_name, type_str) in &info.param_types {
547 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 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 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 fn collect_comptime_for_hint(&mut self, comptime_for: &ComptimeForExpr, span: &Span) {
647 if span.is_dummy() {
648 return;
649 }
650
651 let hint_label =
653 if let Expr::PropertyAccess { property, .. } = comptime_for.iterable.as_ref() {
654 if property == "fields" {
655 "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 fn collect_table_row_hints(&mut self, decl: &VariableDecl) {
687 if !self.config.show_parameter_hints {
688 return;
689 }
690
691 let rows = match &decl.value {
693 Some(Expr::TableRows(rows, _)) => rows,
694 _ => return,
695 };
696
697 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 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 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 if param.name.len() > 1 {
776 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 }
816
817 fn visit_stmt(&mut self, stmt: &Statement) -> bool {
818 if let Statement::VariableDecl(decl, _) = stmt {
820 self.collect_variable_type_hint(decl);
821 self.collect_table_row_hints(decl);
822 }
823 true }
825
826 fn visit_expr(&mut self, expr: &Expr) -> bool {
827 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 if matches!(expr, Expr::MethodCall { .. }) {
836 self.collect_chain_hint(expr);
837 }
838
839 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 }
848}
849
850pub 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
864pub 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 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 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 walk_program(&mut ctx, &program);
897
898 if config.show_type_hints {
900 collect_comptime_alias_hints(text, &program, range, &mut ctx.hints);
901 }
902
903 ctx.hints
904}
905
906fn 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 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 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 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
988fn 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
1004fn 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
1032fn classify_binding_storage(decl: &VariableDecl, label_type: Option<&str>) -> &'static str {
1036 if let Some(Expr::Reference { .. }) = decl.value.as_ref() {
1038 return "Direct";
1039 }
1040
1041 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 return "SharedCow";
1054 }
1055 }
1056
1057 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 if bare.contains("Mutex<") {
1074 return "SharedAtomicMut";
1075 }
1076 return "SharedAtomic";
1077 }
1078 if bare.contains("->") && (bare.starts_with('(') || bare.starts_with("Fn")) {
1080 return "SharedCow";
1081 }
1082 return "UniqueHeap";
1084 }
1085
1086 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
1100fn is_async_or_spawn_shape(expr: &Expr) -> bool {
1102 matches!(
1103 expr,
1104 Expr::AsyncLet(..) | Expr::AsyncScope(..) | Expr::Await(..) | Expr::Join(..)
1105 )
1106}
1107
1108fn 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 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 let _ = is_mut;
1136 match head_name {
1137 "Channel" | "Mutex" => Some("SharedAtomicMut"),
1138 "Atomic" => Some("SharedAtomic"),
1139 _ => None,
1140 }
1141}
1142
1143fn 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
1176fn 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 #[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 #[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 assert!(
1401 !labels.iter().any(|l| l == ": Array"),
1402 "regression: bare `: Array` leak returned, labels = {:?}",
1403 labels
1404 );
1405 }
1406
1407 #[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 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 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 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 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 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 assert_eq!(param_hints[4], "month:");
1759 assert_eq!(param_hints[7], "note:");
1760 }
1761
1762 #[test]
1765 fn test_closure_type_hint_renders_signature() {
1766 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 #[test]
1797 fn test_chain_hints_emit_for_intermediate_method_calls() {
1798 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 #[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 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 #[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 #[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 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 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 #[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 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 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 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 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 let _ = hints; }
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}