1use crate::annotation_discovery::AnnotationDiscovery;
4use crate::type_inference::unified_metadata;
5use crate::util::span_to_range;
6use shape_ast::ast::{Annotation, Expr, Item, Literal, Program, Span, Statement};
7use shape_ast::error::{
8 ErrorNote, ErrorRenderer, ErrorSeverity, ParseErrorKind, ShapeError, SourceLocation,
9 StructuredParseError,
10};
11use tower_lsp_server::ls_types::{
12 Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, Location, NumberOrString,
13 Position, Range, Uri,
14};
15
16pub struct LspErrorRenderer {
18 uri: Uri,
20}
21
22impl LspErrorRenderer {
23 pub fn new(uri: Uri) -> Self {
24 Self { uri }
25 }
26
27 pub fn structured_error_to_diagnostic(&self, error: &StructuredParseError) -> Diagnostic {
29 let severity = match error.severity {
30 ErrorSeverity::Error => DiagnosticSeverity::ERROR,
31 ErrorSeverity::Warning => DiagnosticSeverity::WARNING,
32 ErrorSeverity::Info => DiagnosticSeverity::INFORMATION,
33 ErrorSeverity::Hint => DiagnosticSeverity::HINT,
34 };
35
36 let range = self.structured_location_to_range(error);
38
39 let message = format_structured_message(&error.kind, &error.suggestions);
41
42 let related_information = if !error.related.is_empty() {
44 Some(
45 error
46 .related
47 .iter()
48 .map(|rel| DiagnosticRelatedInformation {
49 location: Location {
50 uri: self.uri.clone(),
51 range: self.source_location_to_range(&rel.location),
52 },
53 message: rel.message.clone(),
54 })
55 .collect(),
56 )
57 } else {
58 None
59 };
60
61 Diagnostic {
62 range,
63 severity: Some(severity),
64 code: Some(NumberOrString::String(error.code.as_str().to_string())),
65 code_description: None,
66 source: Some("shape".to_string()),
67 message,
68 related_information,
69 tags: None,
70 data: None,
71 }
72 }
73
74 fn structured_location_to_range(&self, error: &StructuredParseError) -> Range {
75 let line = error.location.line.saturating_sub(1) as u32;
76 let column = error.location.column.saturating_sub(1) as u32;
77
78 let start = Position {
79 line,
80 character: column,
81 };
82
83 let end = if let Some((end_line, end_col)) = error.span_end {
84 Position {
85 line: end_line.saturating_sub(1) as u32,
86 character: end_col.saturating_sub(1) as u32,
87 }
88 } else if let Some(len) = error.location.length {
89 Position {
90 line,
91 character: column + len as u32,
92 }
93 } else {
94 Position {
96 line,
97 character: column + 1,
98 }
99 };
100
101 Range { start, end }
102 }
103
104 fn source_location_to_range(&self, location: &SourceLocation) -> Range {
105 let line = location.line.saturating_sub(1) as u32;
106 let column = location.column.saturating_sub(1) as u32;
107
108 Range {
109 start: Position {
110 line,
111 character: column,
112 },
113 end: Position {
114 line,
115 character: column + location.length.unwrap_or(1) as u32,
116 },
117 }
118 }
119}
120
121impl ErrorRenderer for LspErrorRenderer {
122 type Output = Vec<Diagnostic>;
123
124 fn render(&self, error: &StructuredParseError) -> Self::Output {
125 vec![self.structured_error_to_diagnostic(error)]
126 }
127
128 fn render_all(&self, errors: &[StructuredParseError]) -> Self::Output {
129 errors
130 .iter()
131 .map(|e| self.structured_error_to_diagnostic(e))
132 .collect()
133 }
134}
135
136fn format_structured_message(
138 kind: &ParseErrorKind,
139 suggestions: &[shape_ast::error::Suggestion],
140) -> String {
141 use shape_ast::error::parse_error::format_error_message;
142
143 let base_message = format_error_message(kind);
144
145 if suggestions.is_empty() {
146 return base_message;
147 }
148
149 let suggestion_text: Vec<String> = suggestions.iter().map(|s| s.message.clone()).collect();
151
152 if suggestion_text.is_empty() {
153 base_message
154 } else {
155 format!("{}\n\n{}", base_message, suggestion_text.join("\n"))
156 }
157}
158
159pub fn error_to_diagnostic(error: &ShapeError) -> Vec<Diagnostic> {
161 error_to_diagnostic_with_uri(error, None)
162}
163
164pub fn error_to_diagnostic_with_uri(error: &ShapeError, uri: Option<Uri>) -> Vec<Diagnostic> {
166 let error_code = error.error_code().map(|c| c.as_str());
168
169 match error {
170 ShapeError::StructuredParse(structured) => {
171 if let Some(uri) = uri {
173 let renderer = LspErrorRenderer::new(uri);
174 renderer.render(structured)
175 } else {
176 vec![create_diagnostic_with_code(
178 &structured.to_string(),
179 Some(&structured.location),
180 DiagnosticSeverity::ERROR,
181 "shape",
182 Some(structured.code.as_str()),
183 )]
184 }
185 }
186 ShapeError::ParseError { message, location } => {
187 vec![create_diagnostic_with_code(
188 message,
189 location.as_ref(),
190 DiagnosticSeverity::ERROR,
191 "shape",
192 error_code,
193 )]
194 }
195 ShapeError::LexError { message, location } => {
196 vec![create_diagnostic_with_code(
197 message,
198 location.as_ref(),
199 DiagnosticSeverity::ERROR,
200 "shape",
201 error_code,
202 )]
203 }
204 ShapeError::SemanticError { message, location } => {
205 vec![create_diagnostic_with_code(
206 message,
207 location.as_ref(),
208 DiagnosticSeverity::ERROR,
209 "shape",
210 error_code,
211 )]
212 }
213 ShapeError::RuntimeError { message, location } => {
214 vec![create_diagnostic_with_code(
215 message,
216 location.as_ref(),
217 DiagnosticSeverity::WARNING,
218 "shape",
219 error_code,
220 )]
221 }
222 ShapeError::TypeError(type_error) => {
223 vec![create_diagnostic_with_code(
225 &type_error.to_string(),
226 None,
227 DiagnosticSeverity::ERROR,
228 "shape",
229 error_code,
230 )]
231 }
232 ShapeError::PatternError {
233 message,
234 pattern_name,
235 } => {
236 let msg = if let Some(name) = pattern_name {
237 format!("Pattern '{}': {}", name, message)
238 } else {
239 message.clone()
240 };
241 vec![create_diagnostic_with_code(
242 &msg,
243 None,
244 DiagnosticSeverity::ERROR,
245 "shape",
246 error_code,
247 )]
248 }
249 ShapeError::DataError {
250 message,
251 symbol,
252 timeframe,
253 } => {
254 let mut msg = message.clone();
255 if let Some(sym) = symbol {
256 msg.push_str(&format!(" (symbol: {})", sym));
257 }
258 if let Some(tf) = timeframe {
259 msg.push_str(&format!(" (timeframe: {})", tf));
260 }
261 vec![create_diagnostic_with_code(
262 &msg,
263 None,
264 DiagnosticSeverity::WARNING,
265 "shape",
266 error_code,
267 )]
268 }
269 ShapeError::ModuleError {
270 message,
271 module_path,
272 } => {
273 let msg = if let Some(path) = module_path {
274 format!("{}: {}", path.display(), message)
275 } else {
276 message.clone()
277 };
278 vec![create_diagnostic_with_code(
279 &msg,
280 None,
281 DiagnosticSeverity::ERROR,
282 "shape",
283 error_code,
284 )]
285 }
286
287 ShapeError::MultiError(errors) => {
288 errors
290 .iter()
291 .flat_map(|e| error_to_diagnostic_with_uri(e, uri.clone()))
292 .collect()
293 }
294
295 _ => vec![create_diagnostic_with_code(
297 &error.to_string(),
298 None,
299 DiagnosticSeverity::ERROR,
300 "shape",
301 error_code,
302 )],
303 }
304}
305
306fn create_diagnostic(
308 message: &str,
309 location: Option<&SourceLocation>,
310 severity: DiagnosticSeverity,
311 source: &str,
312) -> Diagnostic {
313 let range = location_to_range(location);
314
315 let full_message = if let Some(loc) = location {
317 if !loc.hints.is_empty() {
318 let hints = loc
319 .hints
320 .iter()
321 .map(|h| format!("help: {}", h))
322 .collect::<Vec<_>>()
323 .join("\n");
324 format!("{}\n{}", message, hints)
325 } else {
326 message.to_string()
327 }
328 } else {
329 message.to_string()
330 };
331
332 let related_information = if let Some(loc) = location {
334 let mut related = if !loc.notes.is_empty() {
335 notes_to_related_info(&loc.notes, loc.file.as_deref())
336 } else {
337 Vec::new()
338 };
339
340 if let Some(ref file) = loc.file {
343 if let Some(file_uri) = Uri::from_file_path(file) {
344 let line = if loc.line > 0 { loc.line - 1 } else { 0 } as u32;
345 let col = if loc.column > 0 { loc.column - 1 } else { 0 } as u32;
346 let end_char = loc.length.map(|l| col + l as u32).unwrap_or(col + 1);
347 related.push(DiagnosticRelatedInformation {
348 location: Location {
349 uri: file_uri,
350 range: Range {
351 start: Position {
352 line,
353 character: col,
354 },
355 end: Position {
356 line,
357 character: end_char,
358 },
359 },
360 },
361 message: format!("error originates in {}", file),
362 });
363 }
364 }
365
366 if related.is_empty() {
367 None
368 } else {
369 Some(related)
370 }
371 } else {
372 None
373 };
374
375 Diagnostic {
376 range,
377 severity: Some(severity),
378 code: None, code_description: None,
380 source: Some(source.to_string()),
381 message: full_message,
382 related_information,
383 tags: None,
384 data: None,
385 }
386}
387
388fn create_diagnostic_with_code(
390 message: &str,
391 location: Option<&SourceLocation>,
392 severity: DiagnosticSeverity,
393 source: &str,
394 error_code: Option<&str>,
395) -> Diagnostic {
396 let mut diag = create_diagnostic(message, location, severity, source);
397 if let Some(code) = error_code {
398 diag.code = Some(NumberOrString::String(code.to_string()));
399 }
400 diag
401}
402
403fn notes_to_related_info(
405 notes: &[ErrorNote],
406 default_file: Option<&str>,
407) -> Vec<DiagnosticRelatedInformation> {
408 notes
409 .iter()
410 .filter_map(|note| {
411 let location = note.location.as_ref()?;
413 let file = location.file.as_deref().or(default_file)?;
414 let uri = Uri::from_file_path(file)?;
415
416 let line = if location.line > 0 {
417 location.line - 1
418 } else {
419 0
420 } as u32;
421 let column = if location.column > 0 {
422 location.column - 1
423 } else {
424 0
425 } as u32;
426
427 Some(DiagnosticRelatedInformation {
428 location: Location {
429 uri,
430 range: Range {
431 start: Position {
432 line,
433 character: column,
434 },
435 end: Position {
436 line,
437 character: column + 1,
438 },
439 },
440 },
441 message: note.message.clone(),
442 })
443 })
444 .collect()
445}
446
447fn location_to_range(location: Option<&SourceLocation>) -> Range {
449 let full_first_line = Range {
452 start: Position {
453 line: 0,
454 character: 0,
455 },
456 end: Position {
457 line: 0,
458 character: 1000,
459 },
460 };
461
462 if let Some(loc) = location {
463 if loc.is_synthetic {
464 return full_first_line;
465 }
466
467 let line = if loc.line > 0 { loc.line - 1 } else { 0 } as u32;
469 let column = if loc.column > 0 { loc.column - 1 } else { 0 } as u32;
470
471 let start = Position {
472 line,
473 character: column,
474 };
475
476 let end = if let Some(len) = loc.length {
478 Position {
479 line,
480 character: column + len as u32,
481 }
482 } else {
483 Position {
485 line,
486 character: column + 100, }
488 };
489
490 Range { start, end }
491 } else {
492 full_first_line
493 }
494}
495
496pub fn validate_annotations(
502 program: &Program,
503 annotation_discovery: &AnnotationDiscovery,
504 source: &str,
505) -> Vec<Diagnostic> {
506 let mut diagnostics = Vec::new();
507
508 for item in &program.items {
509 let (annotations, span) = match item {
510 Item::Function(func, span) => (&func.annotations, span),
511 Item::ForeignFunction(foreign_fn, span) => (&foreign_fn.annotations, span),
512 _ => continue,
513 };
514 for annotation in annotations {
515 if let Some(diag) = validate_annotation(annotation, span, annotation_discovery, source)
516 {
517 diagnostics.push(diag);
518 }
519 }
520 }
521
522 diagnostics
523}
524
525fn validate_annotation(
527 annotation: &Annotation,
528 item_span: &Span,
529 annotation_discovery: &AnnotationDiscovery,
530 source: &str,
531) -> Option<Diagnostic> {
532 let name = &annotation.name;
533
534 if !annotation_discovery.is_defined(name) {
536 let available: Vec<_> = annotation_discovery
537 .all_annotations()
538 .iter()
539 .map(|a| format!("@{}", a.name))
540 .collect();
541
542 let message = if available.is_empty() {
543 format!("Undefined annotation: @{}", name)
544 } else {
545 format!(
546 "Undefined annotation: @{}. Available: {}",
547 name,
548 available.join(", ")
549 )
550 };
551
552 let range = span_to_range(source, item_span);
554
555 return Some(Diagnostic {
556 range,
557 severity: Some(DiagnosticSeverity::ERROR),
558 code: Some(NumberOrString::String("E0100".to_string())),
559 code_description: None,
560 source: Some("shape".to_string()),
561 message,
562 related_information: None,
563 tags: None,
564 data: None,
565 });
566 }
567
568 if let Some(ann_info) = annotation_discovery.get(name) {
570 let expected = ann_info.params.len();
571 let actual = annotation.args.len();
572
573 if actual > expected || (actual < expected && expected > 0 && actual > 0) {
575 let range = span_to_range(source, item_span);
576
577 return Some(Diagnostic {
578 range,
579 severity: Some(DiagnosticSeverity::WARNING),
580 code: Some(NumberOrString::String("W0101".to_string())),
581 code_description: None,
582 source: Some("shape".to_string()),
583 message: format!("@{} expects {} argument(s), got {}", name, expected, actual),
584 related_information: None,
585 tags: None,
586 data: None,
587 });
588 }
589 }
590
591 None
592}
593
594pub fn validate_async_join(program: &Program, source: &str) -> Vec<Diagnostic> {
598 use shape_ast::ast::Expr;
599 use shape_runtime::visitor::{Visitor, walk_program};
600
601 struct AsyncJoinValidator<'a> {
602 source: &'a str,
603 async_depth_stack: Vec<bool>,
604 diagnostics: Vec<Diagnostic>,
605 }
606
607 impl AsyncJoinValidator<'_> {
608 fn is_in_async(&self) -> bool {
609 self.async_depth_stack.last().copied().unwrap_or(false)
610 }
611 }
612
613 impl Visitor for AsyncJoinValidator<'_> {
614 fn visit_function(&mut self, func: &shape_ast::ast::FunctionDef) -> bool {
615 self.async_depth_stack.push(func.is_async);
616 true
617 }
618
619 fn leave_function(&mut self, _func: &shape_ast::ast::FunctionDef) {
620 self.async_depth_stack.pop();
621 }
622
623 fn visit_expr(&mut self, expr: &Expr) -> bool {
624 if let Expr::Join(_, span) = expr {
625 if !self.is_in_async() {
626 let range = span_to_range(self.source, span);
627 self.diagnostics.push(Diagnostic {
628 range,
629 severity: Some(DiagnosticSeverity::ERROR),
630 code: Some(NumberOrString::String("E0200".to_string())),
631 code_description: None,
632 source: Some("shape".to_string()),
633 message: "`await join` can only be used inside an async function"
634 .to_string(),
635 related_information: None,
636 tags: None,
637 data: None,
638 });
639 }
640 }
641 true
642 }
643 }
644
645 let mut validator = AsyncJoinValidator {
646 source,
647 async_depth_stack: Vec::new(),
648 diagnostics: Vec::new(),
649 };
650 walk_program(&mut validator, program);
651 validator.diagnostics
652}
653
654pub fn validate_async_structured_concurrency(program: &Program, source: &str) -> Vec<Diagnostic> {
657 use shape_ast::ast::Expr;
658 use shape_runtime::visitor::{Visitor, walk_program};
659
660 struct AsyncStructuredValidator<'a> {
661 source: &'a str,
662 async_depth_stack: Vec<bool>,
663 diagnostics: Vec<Diagnostic>,
664 }
665
666 impl AsyncStructuredValidator<'_> {
667 fn is_in_async(&self) -> bool {
668 self.async_depth_stack.last().copied().unwrap_or(false)
669 }
670 }
671
672 impl Visitor for AsyncStructuredValidator<'_> {
673 fn visit_function(&mut self, func: &shape_ast::ast::FunctionDef) -> bool {
674 self.async_depth_stack.push(func.is_async);
675 true
676 }
677
678 fn leave_function(&mut self, _func: &shape_ast::ast::FunctionDef) {
679 self.async_depth_stack.pop();
680 }
681
682 fn visit_expr(&mut self, expr: &Expr) -> bool {
683 match expr {
684 Expr::AsyncLet(_, span) => {
685 if !self.is_in_async() {
686 let range = span_to_range(self.source, span);
687 self.diagnostics.push(Diagnostic {
688 range,
689 severity: Some(DiagnosticSeverity::ERROR),
690 code: Some(NumberOrString::String("E0201".to_string())),
691 code_description: None,
692 source: Some("shape".to_string()),
693 message: "`async let` can only be used inside an async function"
694 .to_string(),
695 related_information: None,
696 tags: None,
697 data: None,
698 });
699 }
700 }
701 Expr::AsyncScope(_, span) => {
702 if !self.is_in_async() {
703 let range = span_to_range(self.source, span);
704 self.diagnostics.push(Diagnostic {
705 range,
706 severity: Some(DiagnosticSeverity::ERROR),
707 code: Some(NumberOrString::String("E0202".to_string())),
708 code_description: None,
709 source: Some("shape".to_string()),
710 message: "`async scope` can only be used inside an async function"
711 .to_string(),
712 related_information: None,
713 tags: None,
714 data: None,
715 });
716 }
717 }
718 Expr::For(for_expr, span) if for_expr.is_async => {
719 if !self.is_in_async() {
720 let range = span_to_range(self.source, span);
721 self.diagnostics.push(Diagnostic {
722 range,
723 severity: Some(DiagnosticSeverity::ERROR),
724 code: Some(NumberOrString::String("E0203".to_string())),
725 code_description: None,
726 source: Some("shape".to_string()),
727 message: "`for await` can only be used inside an async function"
728 .to_string(),
729 related_information: None,
730 tags: None,
731 data: None,
732 });
733 }
734 }
735 _ => {}
736 }
737 true
738 }
739
740 fn visit_stmt(&mut self, stmt: &shape_ast::ast::Statement) -> bool {
741 if let shape_ast::ast::Statement::For(for_loop, span) = stmt {
742 if for_loop.is_async && !self.is_in_async() {
743 let range = span_to_range(self.source, span);
744 self.diagnostics.push(Diagnostic {
745 range,
746 severity: Some(DiagnosticSeverity::ERROR),
747 code: Some(NumberOrString::String("E0203".to_string())),
748 code_description: None,
749 source: Some("shape".to_string()),
750 message: "`for await` can only be used inside an async function"
751 .to_string(),
752 related_information: None,
753 tags: None,
754 data: None,
755 });
756 }
757 }
758 true
759 }
760 }
761
762 let mut validator = AsyncStructuredValidator {
763 source,
764 async_depth_stack: Vec::new(),
765 diagnostics: Vec::new(),
766 };
767 walk_program(&mut validator, program);
768 validator.diagnostics
769}
770
771pub fn validate_interpolation_format_specs(program: &Program, source: &str) -> Vec<Diagnostic> {
776 use shape_ast::interpolation::parse_interpolation_with_mode;
777 use shape_runtime::visitor::{Visitor, walk_program};
778
779 struct InterpolationFormatSpecValidator<'a> {
780 source: &'a str,
781 diagnostics: Vec<Diagnostic>,
782 }
783
784 impl Visitor for InterpolationFormatSpecValidator<'_> {
785 fn visit_expr(&mut self, expr: &Expr) -> bool {
786 if let Expr::Literal(
787 Literal::FormattedString { value, mode } | Literal::ContentString { value, mode },
788 span,
789 ) = expr
790 {
791 if let Err(err) = parse_interpolation_with_mode(value, *mode) {
792 let range = span_to_range(self.source, span);
793 self.diagnostics.push(Diagnostic {
794 range,
795 severity: Some(DiagnosticSeverity::ERROR),
796 code: Some(NumberOrString::String("E0300".to_string())),
797 code_description: None,
798 source: Some("shape".to_string()),
799 message: format!("Invalid interpolation format spec: {}", err),
800 related_information: None,
801 tags: None,
802 data: None,
803 });
804 }
805 }
806 true
807 }
808 }
809
810 let mut validator = InterpolationFormatSpecValidator {
811 source,
812 diagnostics: Vec::new(),
813 };
814 walk_program(&mut validator, program);
815 validator.diagnostics
816}
817
818pub fn validate_comptime_overrides(program: &Program, source: &str) -> Vec<Diagnostic> {
823 use std::collections::HashMap;
824
825 let mut diagnostics = Vec::new();
826
827 let mut struct_comptime_fields: HashMap<String, Vec<String>> = HashMap::new();
829 for item in &program.items {
830 if let Item::StructType(struct_def, _) = item {
831 let comptime_names: Vec<String> = struct_def
832 .fields
833 .iter()
834 .filter(|f| f.is_comptime)
835 .map(|f| f.name.clone())
836 .collect();
837 struct_comptime_fields.insert(struct_def.name.clone(), comptime_names);
838 }
839 }
840
841 for item in &program.items {
843 if let Item::TypeAlias(alias_def, span) = item {
844 if let Some(overrides) = &alias_def.meta_param_overrides {
845 let base_type = match &alias_def.type_annotation {
847 shape_ast::ast::TypeAnnotation::Basic(name) => name.clone(),
848 _ => continue,
849 };
850
851 if let Some(comptime_fields) = struct_comptime_fields.get(&base_type) {
852 for (field_name, _value) in overrides {
853 if !comptime_fields.contains(field_name) {
854 let range = span_to_range(source, span);
856 diagnostics.push(Diagnostic {
857 range,
858 severity: Some(DiagnosticSeverity::ERROR),
859 code: Some(NumberOrString::String("E0300".to_string())),
860 code_description: None,
861 source: Some("shape".to_string()),
862 message: format!(
863 "Cannot override field '{}': only comptime fields can be overridden in type alias. \
864 '{}' is not a comptime field of '{}'.",
865 field_name, field_name, base_type
866 ),
867 related_information: None,
868 tags: None,
869 data: None,
870 });
871 }
872 }
873 }
874 }
875 }
876 }
877
878 diagnostics
879}
880
881pub fn validate_comptime_side_effects(program: &Program, source: &str) -> Vec<Diagnostic> {
886 let mut diagnostics = Vec::new();
887
888 for item in &program.items {
890 match item {
891 Item::Comptime(stmts, span) => {
892 check_stmts_for_side_effects(stmts, span, source, &mut diagnostics);
893 }
894 _ => {
895 visit_item_exprs(item, source, &mut diagnostics);
896 }
897 }
898 }
899
900 diagnostics
901}
902
903const SIDE_EFFECT_FNS: &[&str] = &["print", "println", "debug", "log", "write", "fetch"];
905
906fn check_stmts_for_side_effects(
907 stmts: &[Statement],
908 _block_span: &Span,
909 source: &str,
910 diagnostics: &mut Vec<Diagnostic>,
911) {
912 for stmt in stmts {
913 check_stmt_for_side_effects(stmt, source, diagnostics);
914 }
915}
916
917fn check_stmt_for_side_effects(stmt: &Statement, source: &str, diagnostics: &mut Vec<Diagnostic>) {
918 match stmt {
919 Statement::Expression(expr, _) => check_expr_for_side_effects(expr, source, diagnostics),
920 Statement::VariableDecl(decl, _) => {
921 if let Some(init) = &decl.value {
922 check_expr_for_side_effects(init, source, diagnostics);
923 }
924 }
925 Statement::Return(Some(expr), _) => check_expr_for_side_effects(expr, source, diagnostics),
926 Statement::For(for_loop, _) => {
927 for s in &for_loop.body {
928 check_stmt_for_side_effects(s, source, diagnostics);
929 }
930 }
931 Statement::While(while_loop, _) => {
932 for s in &while_loop.body {
933 check_stmt_for_side_effects(s, source, diagnostics);
934 }
935 }
936 Statement::If(if_stmt, _) => {
937 for s in &if_stmt.then_body {
938 check_stmt_for_side_effects(s, source, diagnostics);
939 }
940 if let Some(else_body) = &if_stmt.else_body {
941 for s in else_body {
942 check_stmt_for_side_effects(s, source, diagnostics);
943 }
944 }
945 }
946 _ => {}
947 }
948}
949
950fn check_expr_for_side_effects(expr: &Expr, source: &str, diagnostics: &mut Vec<Diagnostic>) {
951 match expr {
952 Expr::FunctionCall {
953 name,
954 span,
955 args,
956 named_args,
957 } => {
958 if SIDE_EFFECT_FNS.contains(&name.as_str()) {
959 let range = span_to_range(source, span);
960 diagnostics.push(Diagnostic {
961 range,
962 severity: Some(DiagnosticSeverity::WARNING),
963 code: Some(NumberOrString::String("W0100".to_string())),
964 code_description: None,
965 source: Some("shape".to_string()),
966 message: format!(
967 "Side effect in comptime block: `{}()` performs I/O at compile time. \
968 Consider removing or using a comptime-safe alternative.",
969 name
970 ),
971 related_information: None,
972 tags: None,
973 data: None,
974 });
975 }
976 for arg in args {
978 check_expr_for_side_effects(arg, source, diagnostics);
979 }
980 for (_, arg) in named_args {
981 check_expr_for_side_effects(arg, source, diagnostics);
982 }
983 }
984 Expr::Comptime(stmts, span) => {
985 check_stmts_for_side_effects(stmts, span, source, diagnostics);
987 }
988 _ => {}
989 }
990}
991
992fn visit_item_exprs(item: &Item, source: &str, diagnostics: &mut Vec<Diagnostic>) {
994 match item {
996 Item::Function(func_def, _) => {
997 for stmt in &func_def.body {
998 visit_stmt_for_comptime(stmt, source, diagnostics);
999 }
1000 }
1001 Item::VariableDecl(decl, _) => {
1002 if let Some(init) = &decl.value {
1003 visit_expr_for_comptime(init, source, diagnostics);
1004 }
1005 }
1006 Item::Expression(expr, _) => {
1007 visit_expr_for_comptime(expr, source, diagnostics);
1008 }
1009 Item::Statement(stmt, _) => {
1010 visit_stmt_for_comptime(stmt, source, diagnostics);
1011 }
1012 _ => {}
1013 }
1014}
1015
1016fn visit_stmt_for_comptime(stmt: &Statement, source: &str, diagnostics: &mut Vec<Diagnostic>) {
1017 match stmt {
1018 Statement::Expression(expr, _) => visit_expr_for_comptime(expr, source, diagnostics),
1019 Statement::VariableDecl(decl, _) => {
1020 if let Some(init) = &decl.value {
1021 visit_expr_for_comptime(init, source, diagnostics);
1022 }
1023 }
1024 Statement::Return(Some(expr), _) => visit_expr_for_comptime(expr, source, diagnostics),
1025 Statement::For(for_loop, _) => {
1026 for s in &for_loop.body {
1027 visit_stmt_for_comptime(s, source, diagnostics);
1028 }
1029 }
1030 Statement::While(while_loop, _) => {
1031 for s in &while_loop.body {
1032 visit_stmt_for_comptime(s, source, diagnostics);
1033 }
1034 }
1035 Statement::If(if_stmt, _) => {
1036 for s in &if_stmt.then_body {
1037 visit_stmt_for_comptime(s, source, diagnostics);
1038 }
1039 if let Some(else_body) = &if_stmt.else_body {
1040 for s in else_body {
1041 visit_stmt_for_comptime(s, source, diagnostics);
1042 }
1043 }
1044 }
1045 _ => {}
1046 }
1047}
1048
1049fn visit_expr_for_comptime(expr: &Expr, source: &str, diagnostics: &mut Vec<Diagnostic>) {
1050 match expr {
1051 Expr::Comptime(stmts, span) => {
1052 check_stmts_for_side_effects(stmts, span, source, diagnostics);
1053 }
1054 Expr::FunctionCall {
1055 args, named_args, ..
1056 } => {
1057 for arg in args {
1058 visit_expr_for_comptime(arg, source, diagnostics);
1059 }
1060 for (_, arg) in named_args {
1061 visit_expr_for_comptime(arg, source, diagnostics);
1062 }
1063 }
1064 Expr::Conditional {
1065 condition,
1066 then_expr,
1067 else_expr,
1068 ..
1069 } => {
1070 visit_expr_for_comptime(condition, source, diagnostics);
1071 visit_expr_for_comptime(then_expr, source, diagnostics);
1072 if let Some(e) = else_expr {
1073 visit_expr_for_comptime(e, source, diagnostics);
1074 }
1075 }
1076 Expr::BinaryOp { left, right, .. } => {
1077 visit_expr_for_comptime(left, source, diagnostics);
1078 visit_expr_for_comptime(right, source, diagnostics);
1079 }
1080 Expr::UnaryOp { operand, .. } => {
1081 visit_expr_for_comptime(operand, source, diagnostics);
1082 }
1083 _ => {}
1084 }
1085}
1086
1087pub fn validate_comptime_builtins_context(program: &Program, source: &str) -> Vec<Diagnostic> {
1089 let mut diagnostics = Vec::new();
1090
1091 for item in &program.items {
1092 match item {
1093 Item::Comptime(_, _) => {
1094 }
1096 Item::Function(func_def, _) => {
1097 for stmt in &func_def.body {
1098 check_stmt_comptime_only(stmt, false, source, &mut diagnostics);
1099 }
1100 }
1101 Item::VariableDecl(decl, _) => {
1102 if let Some(init) = &decl.value {
1103 check_expr_comptime_only(init, false, source, &mut diagnostics);
1104 }
1105 }
1106 Item::Expression(expr, _) => {
1107 check_expr_comptime_only(expr, false, source, &mut diagnostics);
1108 }
1109 Item::Statement(stmt, _) => {
1110 check_stmt_comptime_only(stmt, false, source, &mut diagnostics);
1111 }
1112 _ => {}
1113 }
1114 }
1115
1116 diagnostics
1117}
1118
1119fn check_stmt_comptime_only(
1120 stmt: &Statement,
1121 in_comptime: bool,
1122 source: &str,
1123 diagnostics: &mut Vec<Diagnostic>,
1124) {
1125 match stmt {
1126 Statement::Expression(expr, _) => {
1127 check_expr_comptime_only(expr, in_comptime, source, diagnostics);
1128 }
1129 Statement::VariableDecl(decl, _) => {
1130 if let Some(init) = &decl.value {
1131 check_expr_comptime_only(init, in_comptime, source, diagnostics);
1132 }
1133 }
1134 Statement::Return(Some(expr), _) => {
1135 check_expr_comptime_only(expr, in_comptime, source, diagnostics);
1136 }
1137 Statement::For(for_loop, _) => {
1138 for s in &for_loop.body {
1139 check_stmt_comptime_only(s, in_comptime, source, diagnostics);
1140 }
1141 }
1142 Statement::While(while_loop, _) => {
1143 for s in &while_loop.body {
1144 check_stmt_comptime_only(s, in_comptime, source, diagnostics);
1145 }
1146 }
1147 Statement::If(if_stmt, _) => {
1148 for s in &if_stmt.then_body {
1149 check_stmt_comptime_only(s, in_comptime, source, diagnostics);
1150 }
1151 if let Some(else_body) = &if_stmt.else_body {
1152 for s in else_body {
1153 check_stmt_comptime_only(s, in_comptime, source, diagnostics);
1154 }
1155 }
1156 }
1157 _ => {}
1158 }
1159}
1160
1161fn check_expr_comptime_only(
1162 expr: &Expr,
1163 in_comptime: bool,
1164 source: &str,
1165 diagnostics: &mut Vec<Diagnostic>,
1166) {
1167 match expr {
1168 Expr::Comptime(stmts, _) => {
1169 for stmt in stmts {
1171 check_stmt_comptime_only(stmt, true, source, diagnostics);
1172 }
1173 }
1174 Expr::FunctionCall {
1175 name,
1176 span,
1177 args,
1178 named_args,
1179 } => {
1180 let is_comptime_only = unified_metadata()
1181 .get_function(name)
1182 .map(|f| f.comptime_only)
1183 .unwrap_or(false);
1184 if !in_comptime && is_comptime_only {
1185 let range = span_to_range(source, span);
1186 diagnostics.push(Diagnostic {
1187 range,
1188 severity: Some(DiagnosticSeverity::ERROR),
1189 code: Some(NumberOrString::String("E0301".to_string())),
1190 code_description: None,
1191 source: Some("shape".to_string()),
1192 message: format!(
1193 "`{}()` is a comptime-only builtin and can only be called inside a `comptime {{ }}` block.",
1194 name
1195 ),
1196 related_information: None,
1197 tags: None,
1198 data: None,
1199 });
1200 }
1201 for arg in args {
1202 check_expr_comptime_only(arg, in_comptime, source, diagnostics);
1203 }
1204 for (_, arg) in named_args {
1205 check_expr_comptime_only(arg, in_comptime, source, diagnostics);
1206 }
1207 }
1208 Expr::Conditional {
1209 condition,
1210 then_expr,
1211 else_expr,
1212 ..
1213 } => {
1214 check_expr_comptime_only(condition, in_comptime, source, diagnostics);
1215 check_expr_comptime_only(then_expr, in_comptime, source, diagnostics);
1216 if let Some(e) = else_expr {
1217 check_expr_comptime_only(e, in_comptime, source, diagnostics);
1218 }
1219 }
1220 Expr::BinaryOp { left, right, .. } => {
1221 check_expr_comptime_only(left, in_comptime, source, diagnostics);
1222 check_expr_comptime_only(right, in_comptime, source, diagnostics);
1223 }
1224 Expr::UnaryOp { operand, .. } => {
1225 check_expr_comptime_only(operand, in_comptime, source, diagnostics);
1226 }
1227 _ => {}
1228 }
1229}
1230
1231pub fn validate_trait_bounds(program: &Program, source: &str) -> Vec<Diagnostic> {
1237 let mut diagnostics = Vec::new();
1238
1239 let mut trait_methods: std::collections::HashMap<String, Vec<String>> =
1241 std::collections::HashMap::new();
1242 let mut trait_spans: std::collections::HashMap<String, Span> = std::collections::HashMap::new();
1243 for item in &program.items {
1244 if let Item::Trait(trait_def, span) = item {
1245 let required: Vec<String> = trait_def
1246 .members
1247 .iter()
1248 .filter_map(|m| match m {
1249 shape_ast::ast::TraitMember::Required(
1250 shape_ast::ast::InterfaceMember::Method { name, .. },
1251 ) => Some(name.clone()),
1252 _ => None,
1253 })
1254 .collect();
1255 trait_methods.insert(trait_def.name.clone(), required);
1256 trait_spans.insert(trait_def.name.clone(), *span);
1257 }
1258 }
1259
1260 for item in &program.items {
1262 if let Item::Function(func, span) = item {
1263 if let Some(type_params) = &func.type_params {
1264 for tp in type_params {
1265 for bound in &tp.trait_bounds {
1266 if !trait_methods.contains_key(bound.as_str()) {
1267 let range = span_to_range(source, span);
1268 diagnostics.push(Diagnostic {
1269 range,
1270 severity: Some(DiagnosticSeverity::ERROR),
1271 code: Some(NumberOrString::String("E0400".to_string())),
1272 code_description: None,
1273 source: Some("shape".to_string()),
1274 message: format!(
1275 "Trait bound '{}' on type parameter '{}' refers to an undefined trait.",
1276 bound, tp.name
1277 ),
1278 related_information: None,
1279 tags: None,
1280 data: None,
1281 });
1282 }
1283 }
1284 }
1285 }
1286 }
1287 }
1288
1289 for item in &program.items {
1291 if let Item::Impl(impl_block, span) = item {
1292 let trait_name = match &impl_block.trait_name {
1293 shape_ast::ast::TypeName::Simple(n) => n.to_string(),
1294 shape_ast::ast::TypeName::Generic { name, .. } => name.to_string(),
1295 };
1296 let target_type = match &impl_block.target_type {
1297 shape_ast::ast::TypeName::Simple(n) => n.to_string(),
1298 shape_ast::ast::TypeName::Generic { name, .. } => name.to_string(),
1299 };
1300
1301 if let Some(required_methods) = trait_methods.get(&trait_name) {
1302 let implemented: Vec<String> =
1303 impl_block.methods.iter().map(|m| m.name.clone()).collect();
1304 for required in required_methods {
1305 if !implemented.contains(required) {
1306 let range = span_to_range(source, span);
1307 diagnostics.push(Diagnostic {
1308 range,
1309 severity: Some(DiagnosticSeverity::ERROR),
1310 code: Some(NumberOrString::String("E0401".to_string())),
1311 code_description: None,
1312 source: Some("shape".to_string()),
1313 message: format!(
1314 "Missing required method '{}' in impl {} for {}.",
1315 required, trait_name, target_type
1316 ),
1317 related_information: None,
1318 tags: None,
1319 data: None,
1320 });
1321 }
1322 }
1323 }
1324 }
1325 }
1326
1327 diagnostics
1328}
1329
1330pub fn validate_content_strings(program: &Program, source: &str) -> Vec<Diagnostic> {
1335 use shape_runtime::visitor::{Visitor, walk_program};
1336
1337 struct ContentStringValidator<'a> {
1338 source: &'a str,
1339 diagnostics: Vec<Diagnostic>,
1340 }
1341
1342 impl Visitor for ContentStringValidator<'_> {
1343 fn visit_expr(&mut self, expr: &Expr) -> bool {
1344 match expr {
1345 Expr::Literal(Literal::ContentString { value, .. }, span) => {
1346 if value.contains("{}") {
1348 let range = span_to_range(self.source, span);
1349 self.diagnostics.push(Diagnostic {
1350 range,
1351 severity: Some(DiagnosticSeverity::ERROR),
1352 code: Some(NumberOrString::String("E0310".to_string())),
1353 code_description: None,
1354 source: Some("shape".to_string()),
1355 message: "Empty interpolation `{}` in content string. Provide an expression inside the braces.".to_string(),
1356 related_information: None,
1357 tags: None,
1358 data: None,
1359 });
1360 }
1361 }
1362 Expr::MethodCall {
1364 receiver,
1365 method,
1366 args,
1367 span,
1368 ..
1369 } if method == "rgb" => {
1370 if let Expr::Identifier(name, _) = receiver.as_ref() {
1371 if name == "Color" {
1372 for arg in args {
1373 let out_of_range = match arg {
1374 Expr::Literal(Literal::Int(v), _) => *v < 0 || *v > 255,
1375 Expr::Literal(Literal::Number(v), _) => {
1376 (*v as i64) < 0 || (*v as i64) > 255
1377 }
1378 _ => false,
1379 };
1380 if out_of_range {
1381 let val_str = match arg {
1382 Expr::Literal(Literal::Int(v), _) => v.to_string(),
1383 Expr::Literal(Literal::Number(v), _) => v.to_string(),
1384 _ => String::new(),
1385 };
1386 let range = span_to_range(self.source, span);
1387 self.diagnostics.push(Diagnostic {
1388 range,
1389 severity: Some(DiagnosticSeverity::WARNING),
1390 code: Some(NumberOrString::String("W0310".to_string())),
1391 code_description: None,
1392 source: Some("shape".to_string()),
1393 message: format!(
1394 "Color.rgb() component value {} is outside the valid range 0-255.",
1395 val_str
1396 ),
1397 related_information: None,
1398 tags: None,
1399 data: None,
1400 });
1401 }
1402 }
1403 }
1404 }
1405 }
1406 _ => {}
1407 }
1408 true
1409 }
1410 }
1411
1412 let mut validator = ContentStringValidator {
1413 source,
1414 diagnostics: Vec::new(),
1415 };
1416 walk_program(&mut validator, program);
1417 validator.diagnostics
1418}
1419
1420pub fn validate_foreign_function_types(program: &Program, source: &str) -> Vec<Diagnostic> {
1425 let mut diagnostics = Vec::new();
1426
1427 for item in &program.items {
1428 let foreign_fn = match item {
1429 Item::ForeignFunction(f, _) => f,
1430 Item::Export(export, _) => {
1431 if let shape_ast::ast::ExportItem::ForeignFunction(f) = &export.item {
1432 f
1433 } else {
1434 continue;
1435 }
1436 }
1437 _ => continue,
1438 };
1439
1440 for (msg, span) in foreign_fn.validate_type_annotations(true) {
1441 let range = if span.is_dummy() {
1442 span_to_range(source, &foreign_fn.name_span)
1443 } else {
1444 span_to_range(source, &span)
1445 };
1446 diagnostics.push(Diagnostic {
1447 range,
1448 severity: Some(DiagnosticSeverity::ERROR),
1449 code: Some(NumberOrString::String("E0400".to_string())),
1450 code_description: None,
1451 source: Some("shape".to_string()),
1452 message: msg,
1453 related_information: None,
1454 tags: None,
1455 data: None,
1456 });
1457 }
1458 }
1459
1460 diagnostics
1461}
1462
1463pub fn borrow_analysis_to_diagnostics(
1483 analysis: &shape_vm::mir::analysis::BorrowAnalysis,
1484 source: &str,
1485 uri: &Uri,
1486) -> Vec<Diagnostic> {
1487 let mut diagnostics = Vec::new();
1488
1489 for error in &analysis.errors {
1490 let code = error.kind.code();
1491
1492 let primary_range = span_to_range(source, &error.span);
1493
1494 let message = borrow_error_message(&error.kind, code);
1495
1496 let mut related = Vec::new();
1498
1499 let loan_range = span_to_range(source, &error.loan_span);
1501 related.push(DiagnosticRelatedInformation {
1502 location: Location {
1503 uri: uri.clone(),
1504 range: loan_range,
1505 },
1506 message: borrow_origin_note(&error.kind),
1507 });
1508
1509 if let Some(last_use) = error.last_use_span {
1511 let last_use_range = span_to_range(source, &last_use);
1512 related.push(DiagnosticRelatedInformation {
1513 location: Location {
1514 uri: uri.clone(),
1515 range: last_use_range,
1516 },
1517 message: "borrow is still needed here".to_string(),
1518 });
1519 }
1520
1521 let hint = if let Some(repair) = error.repairs.first() {
1523 format!(
1524 "help: {}\nhelp: {}",
1525 borrow_error_hint(&error.kind),
1526 repair.description
1527 )
1528 } else {
1529 format!("help: {}", borrow_error_hint(&error.kind))
1530 };
1531
1532 diagnostics.push(Diagnostic {
1533 range: primary_range,
1534 severity: Some(DiagnosticSeverity::ERROR),
1535 code: Some(NumberOrString::String(code.as_str().to_string())),
1536 code_description: None,
1537 source: Some("shape-borrow".to_string()),
1538 message: format!("{}\n{}", message, hint),
1539 related_information: Some(related),
1540 tags: None,
1541 data: None,
1542 });
1543 }
1544
1545 for error in &analysis.mutability_errors {
1546 let primary_range = span_to_range(source, &error.span);
1547
1548 let binding_kind = if error.is_const {
1549 "const"
1550 } else if error.is_explicit_let {
1551 "let"
1552 } else {
1553 "immutable"
1554 };
1555
1556 let message = format!(
1557 "cannot assign to {} binding '{}'",
1558 binding_kind, error.variable_name
1559 );
1560
1561 let decl_range = span_to_range(source, &error.declaration_span);
1562 let related = vec![DiagnosticRelatedInformation {
1563 location: Location {
1564 uri: uri.clone(),
1565 range: decl_range,
1566 },
1567 message: format!("'{}' declared here", error.variable_name),
1568 }];
1569
1570 diagnostics.push(Diagnostic {
1571 range: primary_range,
1572 severity: Some(DiagnosticSeverity::ERROR),
1573 code: Some(NumberOrString::String("E0384".to_string())),
1574 code_description: None,
1575 source: Some("shape-borrow".to_string()),
1576 message: format!(
1577 "{}\nhelp: consider changing '{}' to 'let mut {}' or 'var {}'",
1578 message, error.variable_name, error.variable_name, error.variable_name
1579 ),
1580 related_information: Some(related),
1581 tags: None,
1582 data: None,
1583 });
1584 }
1585
1586 diagnostics
1587}
1588
1589fn borrow_error_message(
1591 kind: &shape_vm::mir::analysis::BorrowErrorKind,
1592 code: shape_vm::mir::analysis::BorrowErrorCode,
1593) -> String {
1594 use shape_vm::mir::analysis::BorrowErrorKind;
1595 let body = match kind {
1596 BorrowErrorKind::ConflictSharedExclusive => {
1597 "cannot mutably borrow this value while shared borrows are active"
1598 }
1599 BorrowErrorKind::ConflictExclusiveExclusive => {
1600 "cannot mutably borrow this value because it is already borrowed"
1601 }
1602 BorrowErrorKind::ReadWhileExclusivelyBorrowed => {
1603 "cannot read this value while it is mutably borrowed"
1604 }
1605 BorrowErrorKind::WriteWhileBorrowed => {
1606 "cannot write to this value while it is borrowed"
1607 }
1608 BorrowErrorKind::ReferenceEscape => {
1609 "cannot return or store a reference that outlives its owner"
1610 }
1611 BorrowErrorKind::ReferenceStoredInArray => {
1612 "cannot store a reference in an array"
1613 }
1614 BorrowErrorKind::ReferenceStoredInObject => {
1615 "cannot store a reference in an object or struct literal"
1616 }
1617 BorrowErrorKind::ReferenceStoredInEnum => {
1618 "cannot store a reference in an enum payload"
1619 }
1620 BorrowErrorKind::ReferenceEscapeIntoClosure => {
1621 "reference cannot escape into a closure"
1622 }
1623 BorrowErrorKind::UseAfterMove => {
1624 "cannot use this value after it was moved"
1625 }
1626 BorrowErrorKind::ExclusiveRefAcrossTaskBoundary => {
1627 "cannot move an exclusive reference across a task boundary"
1628 }
1629 BorrowErrorKind::SharedRefAcrossDetachedTask => {
1630 "cannot send a shared reference across a detached task boundary"
1631 }
1632 BorrowErrorKind::InconsistentReferenceReturn => {
1633 "reference-returning functions must return a reference on every path from the same borrowed origin and borrow kind"
1634 }
1635 BorrowErrorKind::CallSiteAliasConflict => {
1636 "cannot pass the same variable to multiple parameters that conflict on aliasing"
1637 }
1638 BorrowErrorKind::NonSendableAcrossTaskBoundary => {
1639 "cannot send a non-sendable value across a task boundary"
1640 }
1641 };
1642 format!("[{}] {}", code, body)
1643}
1644
1645fn borrow_error_hint(kind: &shape_vm::mir::analysis::BorrowErrorKind) -> &'static str {
1647 use shape_vm::mir::analysis::BorrowErrorKind;
1648 match kind {
1649 BorrowErrorKind::ConflictSharedExclusive => {
1650 "move the mutable borrow later, or end the shared borrow sooner"
1651 }
1652 BorrowErrorKind::ConflictExclusiveExclusive => {
1653 "end the previous mutable borrow before creating another one"
1654 }
1655 BorrowErrorKind::ReadWhileExclusivelyBorrowed => {
1656 "read through the existing reference, or move the read after the borrow ends"
1657 }
1658 BorrowErrorKind::WriteWhileBorrowed => "move this write after the borrow ends",
1659 BorrowErrorKind::ReferenceEscape => "return an owned value instead of a reference",
1660 BorrowErrorKind::ReferenceStoredInArray
1661 | BorrowErrorKind::ReferenceStoredInObject
1662 | BorrowErrorKind::ReferenceStoredInEnum => {
1663 "store owned values instead of references"
1664 }
1665 BorrowErrorKind::ReferenceEscapeIntoClosure => {
1666 "capture an owned value instead of a reference"
1667 }
1668 BorrowErrorKind::UseAfterMove => {
1669 "clone the value before moving it, or stop using the original after the move"
1670 }
1671 BorrowErrorKind::ExclusiveRefAcrossTaskBoundary => {
1672 "keep the mutable reference within the current task or pass an owned value instead"
1673 }
1674 BorrowErrorKind::SharedRefAcrossDetachedTask => {
1675 "clone the value before sending it to a detached task, or use a structured task instead"
1676 }
1677 BorrowErrorKind::InconsistentReferenceReturn => {
1678 "return a reference from the same borrowed origin on every path, or return owned values instead"
1679 }
1680 BorrowErrorKind::CallSiteAliasConflict => {
1681 "use separate variables for each argument, or clone one of them"
1682 }
1683 BorrowErrorKind::NonSendableAcrossTaskBoundary => {
1684 "clone the captured state or use an owned value that is safe to send across tasks"
1685 }
1686 }
1687}
1688
1689fn borrow_origin_note(kind: &shape_vm::mir::analysis::BorrowErrorKind) -> String {
1691 use shape_vm::mir::analysis::BorrowErrorKind;
1692 match kind {
1693 BorrowErrorKind::ConflictSharedExclusive
1694 | BorrowErrorKind::ConflictExclusiveExclusive
1695 | BorrowErrorKind::ReadWhileExclusivelyBorrowed
1696 | BorrowErrorKind::WriteWhileBorrowed => "conflicting borrow originates here".to_string(),
1697 BorrowErrorKind::ReferenceEscape
1698 | BorrowErrorKind::ReferenceStoredInArray
1699 | BorrowErrorKind::ReferenceStoredInObject
1700 | BorrowErrorKind::ReferenceStoredInEnum
1701 | BorrowErrorKind::ReferenceEscapeIntoClosure
1702 | BorrowErrorKind::ExclusiveRefAcrossTaskBoundary
1703 | BorrowErrorKind::SharedRefAcrossDetachedTask => {
1704 "reference originates here".to_string()
1705 }
1706 BorrowErrorKind::UseAfterMove => "value was moved here".to_string(),
1707 BorrowErrorKind::InconsistentReferenceReturn => {
1708 "borrowed origin on another return path originates here".to_string()
1709 }
1710 BorrowErrorKind::CallSiteAliasConflict => {
1711 "conflicting arguments originate here".to_string()
1712 }
1713 BorrowErrorKind::NonSendableAcrossTaskBoundary => {
1714 "non-sendable value originates here".to_string()
1715 }
1716 }
1717}
1718
1719#[cfg(test)]
1720mod tests {
1721 use super::*;
1722 use crate::util::offset_to_line_col;
1723
1724 #[test]
1725 fn test_location_to_range() {
1726 let loc = SourceLocation::new(5, 10);
1728 let range = location_to_range(Some(&loc));
1729
1730 assert_eq!(range.start.line, 4); assert_eq!(range.start.character, 9); let range = location_to_range(None);
1735 assert_eq!(range.start.line, 0);
1736 assert_eq!(range.start.character, 0);
1737 }
1738
1739 #[test]
1740 fn test_parse_error_diagnostic() {
1741 let error = ShapeError::ParseError {
1742 message: "Expected expression".to_string(),
1743 location: Some(SourceLocation::new(10, 5)),
1744 };
1745
1746 let diagnostics = error_to_diagnostic(&error);
1747 assert_eq!(diagnostics.len(), 1);
1748 assert_eq!(diagnostics[0].message, "Expected expression");
1749 assert_eq!(diagnostics[0].severity, Some(DiagnosticSeverity::ERROR));
1750 assert_eq!(diagnostics[0].source.as_deref(), Some("shape"));
1751 assert_eq!(diagnostics[0].range.start.line, 9); }
1753
1754 #[test]
1755 fn test_semantic_error_diagnostic() {
1756 let error = ShapeError::SemanticError {
1757 message: "Undefined variable 'x'".to_string(),
1758 location: Some(SourceLocation::new(3, 7)),
1759 };
1760
1761 let diagnostics = error_to_diagnostic(&error);
1762 assert_eq!(diagnostics.len(), 1);
1763 assert_eq!(diagnostics[0].message, "Undefined variable 'x'");
1764 assert_eq!(diagnostics[0].severity, Some(DiagnosticSeverity::ERROR));
1765 assert_eq!(diagnostics[0].source.as_deref(), Some("shape"));
1766 }
1767
1768 #[test]
1769 fn test_multi_error_flattening() {
1770 let multi_error = ShapeError::MultiError(vec![
1771 ShapeError::SemanticError {
1772 message: "Undefined variable 'x'".to_string(),
1773 location: Some(SourceLocation::new(1, 1)),
1774 },
1775 ShapeError::SemanticError {
1776 message: "Undefined variable 'y'".to_string(),
1777 location: Some(SourceLocation::new(2, 1)),
1778 },
1779 ]);
1780
1781 let diagnostics = error_to_diagnostic(&multi_error);
1782 assert_eq!(
1783 diagnostics.len(),
1784 2,
1785 "MultiError should flatten into 2 diagnostics"
1786 );
1787 assert!(diagnostics[0].message.contains("x"));
1788 assert!(diagnostics[1].message.contains("y"));
1789 }
1790
1791 #[test]
1792 fn test_multi_error_display() {
1793 let multi_error = ShapeError::MultiError(vec![
1794 ShapeError::SemanticError {
1795 message: "Error one".to_string(),
1796 location: None,
1797 },
1798 ShapeError::SemanticError {
1799 message: "Error two".to_string(),
1800 location: None,
1801 },
1802 ]);
1803
1804 let display = multi_error.to_string();
1805 assert!(
1806 display.contains("Error one"),
1807 "Display should contain first error"
1808 );
1809 assert!(
1810 display.contains("Error two"),
1811 "Display should contain second error"
1812 );
1813 }
1814
1815 #[test]
1816 fn test_offset_to_line_col() {
1817 let source = "line1\nline2\nline3";
1818
1819 assert_eq!(offset_to_line_col(source, 0), (0, 0));
1821
1822 assert_eq!(offset_to_line_col(source, 5), (0, 5));
1824
1825 assert_eq!(offset_to_line_col(source, 6), (1, 0));
1827
1828 assert_eq!(offset_to_line_col(source, 8), (1, 2));
1830 }
1831
1832 #[test]
1833 fn test_validate_annotations_with_defined() {
1834 use shape_ast::parser::parse_program;
1835
1836 let source = r#"
1838annotation my_ann() {
1839 on_define(fn, ctx) {
1840 ctx.registry("items").set(fn.name, fn)
1841 }
1842}
1843
1844@my_ann
1845function my_func(x) {
1846 return x + 1;
1847}
1848"#;
1849
1850 let program = parse_program(source).unwrap();
1851 let mut discovery = AnnotationDiscovery::new();
1852 discovery.discover_from_program(&program);
1853
1854 let diagnostics = validate_annotations(&program, &discovery, source);
1855
1856 assert!(
1858 diagnostics.is_empty(),
1859 "Expected no diagnostics for defined annotation, got: {:?}",
1860 diagnostics
1861 );
1862 }
1863
1864 #[test]
1865 fn test_validate_annotations_with_undefined() {
1866 use shape_ast::parser::parse_program;
1867
1868 let source = r#"
1869@undefined_annotation
1870function my_func() {
1871 return None;
1872}
1873"#;
1874
1875 let program = parse_program(source).unwrap();
1876 let mut discovery = AnnotationDiscovery::new();
1877 discovery.discover_from_program(&program);
1878
1879 let diagnostics = validate_annotations(&program, &discovery, source);
1880
1881 assert_eq!(
1883 diagnostics.len(),
1884 1,
1885 "Expected 1 diagnostic for undefined annotation"
1886 );
1887 assert!(diagnostics[0].message.contains("Undefined annotation"));
1888 assert!(diagnostics[0].message.contains("undefined_annotation"));
1889 }
1890
1891 #[test]
1892 fn test_validate_trait_bounds_missing_method() {
1893 use shape_ast::parser::parse_program;
1894
1895 let source = "trait Queryable {\n filter(pred): any;\n select(cols): any\n}\nimpl Queryable for MyTable {\n method filter(pred) { self }\n}\n";
1896 let program = parse_program(source).unwrap();
1897 let diagnostics = validate_trait_bounds(&program, source);
1898
1899 assert_eq!(
1900 diagnostics.len(),
1901 1,
1902 "Should report 1 missing method error, got: {:?}",
1903 diagnostics
1904 );
1905 assert!(diagnostics[0].message.contains("Missing required method"));
1906 assert!(diagnostics[0].message.contains("select"));
1907 }
1908
1909 #[test]
1910 fn test_validate_trait_bounds_all_implemented() {
1911 use shape_ast::parser::parse_program;
1912
1913 let source = "trait Queryable {\n filter(pred): any;\n select(cols): any\n}\nimpl Queryable for MyTable {\n method filter(pred) { self }\n method select(cols) { self }\n}\n";
1914 let program = parse_program(source).unwrap();
1915 let diagnostics = validate_trait_bounds(&program, source);
1916
1917 assert_eq!(
1918 diagnostics.len(),
1919 0,
1920 "Should report no errors when all methods implemented"
1921 );
1922 }
1923
1924 #[test]
1925 fn test_validate_trait_bounds_undefined_trait_in_bound() {
1926 use shape_ast::parser::parse_program;
1927
1928 let source = "fn foo<T: NonExistent>(x: T) {\n x\n}\n";
1929 let program = parse_program(source).unwrap();
1930 let diagnostics = validate_trait_bounds(&program, source);
1931
1932 assert_eq!(
1933 diagnostics.len(),
1934 1,
1935 "Should report undefined trait in bound"
1936 );
1937 assert!(diagnostics[0].message.contains("NonExistent"));
1938 assert!(diagnostics[0].message.contains("undefined trait"));
1939 }
1940
1941 #[test]
1942 fn test_validate_trait_bounds_valid_bound() {
1943 use shape_ast::parser::parse_program;
1944
1945 let source = "trait Comparable {\n compare(other): number\n}\nfn foo<T: Comparable>(x: T) {\n x\n}\n";
1946 let program = parse_program(source).unwrap();
1947 let diagnostics = validate_trait_bounds(&program, source);
1948
1949 assert_eq!(
1950 diagnostics.len(),
1951 0,
1952 "Should report no errors for valid trait bound"
1953 );
1954 }
1955
1956 #[test]
1957 fn test_validate_async_join_outside_async() {
1958 use shape_ast::parser::parse_program;
1959
1960 let source = "fn foo() {\n let x = await join all {\n 1,\n 2\n }\n}";
1961 let program = parse_program(source).unwrap();
1962 let diagnostics = validate_async_join(&program, source);
1963
1964 assert_eq!(
1965 diagnostics.len(),
1966 1,
1967 "Should report error for join outside async function"
1968 );
1969 assert!(
1970 diagnostics[0].message.contains("async"),
1971 "Error should mention async, got: {}",
1972 diagnostics[0].message
1973 );
1974 }
1975
1976 #[test]
1977 fn test_validate_async_join_inside_async() {
1978 use shape_ast::parser::parse_program;
1979
1980 let source = "async fn foo() {\n let x = await join all {\n 1,\n 2\n }\n}";
1981 let program = parse_program(source).unwrap();
1982 let diagnostics = validate_async_join(&program, source);
1983
1984 assert_eq!(
1985 diagnostics.len(),
1986 0,
1987 "Should not report error for join inside async function"
1988 );
1989 }
1990
1991 #[test]
1992 fn test_validate_async_join_top_level() {
1993 use shape_ast::parser::parse_program;
1994
1995 let source = "let x = await join race {\n 1,\n 2\n}";
1997 let program = parse_program(source).unwrap();
1998 let diagnostics = validate_async_join(&program, source);
1999
2000 assert_eq!(
2001 diagnostics.len(),
2002 1,
2003 "Should report error for join at top level"
2004 );
2005 }
2006
2007 #[test]
2008 fn test_validate_comptime_side_effects_with_print() {
2009 use shape_ast::parser::parse_program;
2010
2011 let source = "comptime {\n print(\"hello\")\n}";
2012 let program = parse_program(source).unwrap();
2013 let diagnostics = validate_comptime_side_effects(&program, source);
2014
2015 assert_eq!(
2016 diagnostics.len(),
2017 1,
2018 "Should warn about print() in comptime block"
2019 );
2020 assert!(
2021 diagnostics[0].message.contains("print"),
2022 "Warning should mention print"
2023 );
2024 assert_eq!(
2025 diagnostics[0].severity,
2026 Some(DiagnosticSeverity::WARNING),
2027 "Should be a warning, not an error"
2028 );
2029 }
2030
2031 #[test]
2032 fn test_validate_comptime_side_effects_clean() {
2033 use shape_ast::parser::parse_program;
2034
2035 let source = "comptime {\n let x = 42\n}";
2036 let program = parse_program(source).unwrap();
2037 let diagnostics = validate_comptime_side_effects(&program, source);
2038
2039 assert_eq!(
2040 diagnostics.len(),
2041 0,
2042 "Pure comptime block should have no warnings"
2043 );
2044 }
2045
2046 #[test]
2047 fn test_validate_comptime_side_effects_nested_in_function() {
2048 use shape_ast::parser::parse_program;
2049
2050 let source = "fn foo() {\n let x = comptime {\n print(\"debug\")\n }\n}\n";
2051 let program = parse_program(source).unwrap();
2052 let diagnostics = validate_comptime_side_effects(&program, source);
2053
2054 assert_eq!(
2055 diagnostics.len(),
2056 1,
2057 "Should warn about print() in nested comptime block, got: {:?}",
2058 diagnostics
2059 );
2060 }
2061
2062 #[test]
2063 fn test_validate_comptime_side_effects_fetch() {
2064 use shape_ast::parser::parse_program;
2065
2066 let source = "comptime {\n let data = fetch(\"http://example.com\")\n}\n";
2067 let program = parse_program(source).unwrap();
2068 let diagnostics = validate_comptime_side_effects(&program, source);
2069
2070 assert_eq!(
2071 diagnostics.len(),
2072 1,
2073 "Should warn about fetch() in comptime block"
2074 );
2075 assert!(diagnostics[0].message.contains("fetch"));
2076 }
2077
2078 #[test]
2079 fn test_validate_comptime_builtins_outside_comptime() {
2080 use shape_ast::parser::parse_program;
2081
2082 let source = r#"let x = implements("Point", "Display")"#;
2083 let program = parse_program(source).unwrap();
2084 let diagnostics = validate_comptime_builtins_context(&program, source);
2085
2086 assert_eq!(
2087 diagnostics.len(),
2088 1,
2089 "Should report error for comptime builtin outside comptime"
2090 );
2091 assert!(diagnostics[0].message.contains("comptime-only"));
2092 assert_eq!(diagnostics[0].severity, Some(DiagnosticSeverity::ERROR));
2093 }
2094
2095 #[test]
2096 fn test_validate_comptime_builtins_inside_comptime_ok() {
2097 use shape_ast::parser::parse_program;
2098
2099 let source = "comptime {\n let has = implements(\"Point\", \"Display\")\n}";
2100 let program = parse_program(source).unwrap();
2101 let diagnostics = validate_comptime_builtins_context(&program, source);
2102
2103 assert_eq!(
2104 diagnostics.len(),
2105 0,
2106 "comptime builtin inside comptime should be allowed"
2107 );
2108 }
2109
2110 #[test]
2111 fn test_validate_comptime_builtins_build_config_outside() {
2112 use shape_ast::parser::parse_program;
2113
2114 let source = "let cfg = build_config()";
2115 let program = parse_program(source).unwrap();
2116 let diagnostics = validate_comptime_builtins_context(&program, source);
2117
2118 assert_eq!(
2119 diagnostics.len(),
2120 1,
2121 "Should report error for build_config() outside comptime"
2122 );
2123 }
2124
2125 #[test]
2126 fn test_validate_async_let_outside_async() {
2127 use shape_ast::parser::parse_program;
2128
2129 let source = "fn foo() {\n async let x = fetch(\"url\")\n}";
2130 let program = parse_program(source).unwrap();
2131 let diagnostics = validate_async_structured_concurrency(&program, source);
2132
2133 assert_eq!(
2134 diagnostics.len(),
2135 1,
2136 "Should report error for async let outside async: {:?}",
2137 diagnostics
2138 );
2139 assert!(diagnostics[0].message.contains("async let"));
2140 assert_eq!(
2141 diagnostics[0].code,
2142 Some(NumberOrString::String("E0201".to_string()))
2143 );
2144 }
2145
2146 #[test]
2147 fn test_validate_async_let_inside_async() {
2148 use shape_ast::parser::parse_program;
2149
2150 let source = "async fn foo() {\n async let x = fetch(\"url\")\n}";
2151 let program = parse_program(source).unwrap();
2152 let diagnostics = validate_async_structured_concurrency(&program, source);
2153
2154 assert!(
2155 diagnostics.is_empty(),
2156 "Should have no errors for async let inside async fn: {:?}",
2157 diagnostics
2158 );
2159 }
2160
2161 #[test]
2162 fn test_validate_async_scope_outside_async() {
2163 use shape_ast::parser::parse_program;
2164
2165 let source = "fn foo() {\n let result = async scope { 42 }\n}";
2166 let program = parse_program(source).unwrap();
2167 let diagnostics = validate_async_structured_concurrency(&program, source);
2168
2169 assert_eq!(
2170 diagnostics.len(),
2171 1,
2172 "Should report error for async scope outside async: {:?}",
2173 diagnostics
2174 );
2175 assert!(diagnostics[0].message.contains("async scope"));
2176 assert_eq!(
2177 diagnostics[0].code,
2178 Some(NumberOrString::String("E0202".to_string()))
2179 );
2180 }
2181
2182 #[test]
2183 fn test_validate_async_scope_inside_async() {
2184 use shape_ast::parser::parse_program;
2185
2186 let source = "async fn foo() {\n let result = async scope { 42 }\n}";
2187 let program = parse_program(source).unwrap();
2188 let diagnostics = validate_async_structured_concurrency(&program, source);
2189
2190 assert!(
2191 diagnostics.is_empty(),
2192 "Should have no errors for async scope inside async fn: {:?}",
2193 diagnostics
2194 );
2195 }
2196
2197 #[test]
2198 fn test_validate_for_await_outside_async() {
2199 use shape_ast::parser::parse_program;
2200
2201 let source = "fn foo() {\n for await x in stream {\n x\n }\n}";
2202 let program = parse_program(source).unwrap();
2203 let diagnostics = validate_async_structured_concurrency(&program, source);
2204
2205 assert_eq!(
2206 diagnostics.len(),
2207 1,
2208 "Should report error for for-await outside async: {:?}",
2209 diagnostics
2210 );
2211 assert!(diagnostics[0].message.contains("for await"));
2212 assert_eq!(
2213 diagnostics[0].code,
2214 Some(NumberOrString::String("E0203".to_string()))
2215 );
2216 }
2217
2218 #[test]
2219 fn test_validate_for_await_inside_async() {
2220 use shape_ast::parser::parse_program;
2221
2222 let source = "async fn foo() {\n for await x in stream {\n x\n }\n}";
2223 let program = parse_program(source).unwrap();
2224 let diagnostics = validate_async_structured_concurrency(&program, source);
2225
2226 assert!(
2227 diagnostics.is_empty(),
2228 "Should have no errors for for-await inside async fn: {:?}",
2229 diagnostics
2230 );
2231 }
2232
2233 #[test]
2234 fn test_validate_interpolation_format_specs_ok() {
2235 use shape_ast::parser::parse_program;
2236
2237 let source = r#"let s = f"value={price:fixed(2)}""#;
2238 let program = parse_program(source).unwrap();
2239 let diagnostics = validate_interpolation_format_specs(&program, source);
2240 assert!(
2241 diagnostics.is_empty(),
2242 "unexpected diagnostics: {:?}",
2243 diagnostics
2244 );
2245 }
2246
2247 #[test]
2248 fn test_validate_interpolation_format_specs_reports_invalid_table_key() {
2249 use shape_ast::parser::parse_program;
2250
2251 let source = r#"let s = f"{rows:table(foo=1)}""#;
2252 let program = parse_program(source).unwrap();
2253 let diagnostics = validate_interpolation_format_specs(&program, source);
2254 assert_eq!(diagnostics.len(), 1, "expected a single diagnostic");
2255 assert!(
2256 diagnostics[0].message.contains("Unknown table format key"),
2257 "unexpected diagnostic message: {}",
2258 diagnostics[0].message
2259 );
2260 assert_eq!(
2261 diagnostics[0].code,
2262 Some(NumberOrString::String("E0300".to_string()))
2263 );
2264 assert_eq!(
2265 diagnostics[0].range.start.line, 0,
2266 "diagnostic should point to formatted string line"
2267 );
2268 }
2269
2270 #[test]
2271 fn test_validate_content_strings_empty_interpolation() {
2272 use shape_ast::parser::parse_program;
2273
2274 let source = r#"let x = c"hello {}""#;
2275 let program = parse_program(source).unwrap();
2276 let diagnostics = validate_content_strings(&program, source);
2277
2278 assert_eq!(
2279 diagnostics.len(),
2280 1,
2281 "expected 1 diagnostic for empty interpolation, got: {:?}",
2282 diagnostics
2283 );
2284 assert!(
2285 diagnostics[0].message.contains("Empty interpolation"),
2286 "unexpected message: {}",
2287 diagnostics[0].message
2288 );
2289 assert_eq!(
2290 diagnostics[0].code,
2291 Some(NumberOrString::String("E0310".to_string()))
2292 );
2293 }
2294
2295 #[test]
2296 fn test_validate_content_strings_valid_interpolation_ok() {
2297 use shape_ast::parser::parse_program;
2298
2299 let source = r#"let x = c"hello {name}""#;
2300 let program = parse_program(source).unwrap();
2301 let diagnostics = validate_content_strings(&program, source);
2302
2303 assert!(
2304 diagnostics.is_empty(),
2305 "valid content string should produce no diagnostics: {:?}",
2306 diagnostics
2307 );
2308 }
2309
2310 #[test]
2311 fn test_validate_color_rgb_out_of_range() {
2312 use shape_ast::parser::parse_program;
2313
2314 let source = "let c = Color.rgb(300, 100, 256)";
2315 let program = parse_program(source).unwrap();
2316 let diagnostics = validate_content_strings(&program, source);
2317
2318 assert_eq!(
2319 diagnostics.len(),
2320 2,
2321 "expected 2 diagnostics for out-of-range RGB values (300 and 256), got: {:?}",
2322 diagnostics
2323 );
2324 assert!(diagnostics[0].message.contains("300"));
2325 assert!(diagnostics[1].message.contains("256"));
2326 assert_eq!(
2327 diagnostics[0].code,
2328 Some(NumberOrString::String("W0310".to_string()))
2329 );
2330 assert_eq!(diagnostics[0].severity, Some(DiagnosticSeverity::WARNING));
2331 }
2332
2333 #[test]
2334 fn test_validate_color_rgb_valid_range_ok() {
2335 use shape_ast::parser::parse_program;
2336
2337 let source = "let c = Color.rgb(255, 128, 0)";
2338 let program = parse_program(source).unwrap();
2339 let diagnostics = validate_content_strings(&program, source);
2340
2341 assert!(
2342 diagnostics.is_empty(),
2343 "valid Color.rgb should produce no diagnostics: {:?}",
2344 diagnostics
2345 );
2346 }
2347
2348 #[test]
2349 fn test_borrow_analysis_to_diagnostics_empty() {
2350 let analysis = shape_vm::mir::analysis::BorrowAnalysis::empty();
2351 let uri = Uri::from_file_path("/tmp/test.shape").unwrap();
2352 let diagnostics = borrow_analysis_to_diagnostics(&analysis, "", &uri);
2353 assert!(
2354 diagnostics.is_empty(),
2355 "Empty analysis should produce no diagnostics"
2356 );
2357 }
2358
2359 #[test]
2360 fn test_borrow_analysis_to_diagnostics_with_error() {
2361 use shape_vm::mir::analysis::*;
2362 use shape_vm::mir::types::*;
2363
2364 let mut analysis = BorrowAnalysis::empty();
2365 analysis.errors.push(BorrowError {
2366 kind: BorrowErrorKind::ConflictExclusiveExclusive,
2367 span: Span { start: 10, end: 20 },
2368 conflicting_loan: LoanId(0),
2369 loan_span: Span { start: 0, end: 5 },
2370 last_use_span: Some(Span { start: 25, end: 30 }),
2371 repairs: Vec::new(),
2372 });
2373
2374 let source = "let mut x = 10\nlet m1 = &mut x\nlet m2 = &mut x\nprint(m1)\nprint(m2)";
2375 let uri = Uri::from_file_path("/tmp/test.shape").unwrap();
2376 let diagnostics = borrow_analysis_to_diagnostics(&analysis, source, &uri);
2377
2378 assert_eq!(diagnostics.len(), 1, "Should produce one diagnostic");
2379 let diag = &diagnostics[0];
2380 assert_eq!(diag.severity, Some(DiagnosticSeverity::ERROR));
2381 assert_eq!(
2382 diag.code,
2383 Some(NumberOrString::String("B0001".to_string()))
2384 );
2385 assert_eq!(diag.source.as_deref(), Some("shape-borrow"));
2386 assert!(
2387 diag.message.contains("cannot mutably borrow"),
2388 "Message should describe the conflict: {}",
2389 diag.message
2390 );
2391 let related = diag.related_information.as_ref().unwrap();
2393 assert_eq!(
2394 related.len(),
2395 2,
2396 "Should have loan origin + last use entries"
2397 );
2398 assert!(related[0].message.contains("conflicting borrow"));
2399 assert!(related[1].message.contains("still needed"));
2400 }
2401
2402 #[test]
2403 fn test_borrow_analysis_to_diagnostics_mutability_error() {
2404 use shape_vm::mir::analysis::*;
2405
2406 let mut analysis = BorrowAnalysis::empty();
2407 analysis.mutability_errors.push(MutabilityError {
2408 span: Span { start: 10, end: 15 },
2409 variable_name: "x".to_string(),
2410 declaration_span: Span { start: 0, end: 5 },
2411 is_explicit_let: true,
2412 is_const: false,
2413 });
2414
2415 let source = "let x = 42\nx = 100\n";
2416 let uri = Uri::from_file_path("/tmp/test.shape").unwrap();
2417 let diagnostics = borrow_analysis_to_diagnostics(&analysis, source, &uri);
2418
2419 assert_eq!(diagnostics.len(), 1);
2420 let diag = &diagnostics[0];
2421 assert!(diag.message.contains("cannot assign to let binding"));
2422 assert_eq!(
2423 diag.code,
2424 Some(NumberOrString::String("E0384".to_string()))
2425 );
2426 let related = diag.related_information.as_ref().unwrap();
2427 assert_eq!(related.len(), 1);
2428 assert!(related[0].message.contains("declared here"));
2429 }
2430}