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 CodeDescription, Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag,
13 Location, NumberOrString, Position, Range, Uri,
14};
15
16const BOOK_CODE_DESCRIPTION_BASE: &str = "https://book.shape-lang.dev/appendix/error-codes/";
33
34pub fn code_description_for(code: &str) -> Option<CodeDescription> {
43 if code.is_empty() {
44 return None;
45 }
46 use std::str::FromStr;
47 let href_str = format!("{}#{}", BOOK_CODE_DESCRIPTION_BASE, code);
48 Uri::from_str(&href_str)
49 .ok()
50 .map(|href| CodeDescription { href })
51}
52
53pub fn diagnostic_tags_for(code: Option<&str>, message: &str) -> Option<Vec<DiagnosticTag>> {
61 let mut tags = Vec::new();
62 let is_unused = matches!(code, Some("W0102") | Some("W0103"))
68 || message.contains("unused import")
69 || message.contains("unused variable");
70 if is_unused {
71 tags.push(DiagnosticTag::UNNECESSARY);
72 }
73 if tags.is_empty() { None } else { Some(tags) }
74}
75
76pub fn enrich_diagnostics_with_code_metadata(diagnostics: &mut [Diagnostic]) {
87 for diag in diagnostics.iter_mut() {
88 if diag.code_description.is_none() {
89 if let Some(NumberOrString::String(code)) = &diag.code {
90 diag.code_description = code_description_for(code);
91 }
92 }
93 if diag.tags.is_none() {
94 let code_str = match &diag.code {
95 Some(NumberOrString::String(c)) => Some(c.as_str()),
96 _ => None,
97 };
98 diag.tags = diagnostic_tags_for(code_str, &diag.message);
99 }
100 }
101}
102
103pub struct LspErrorRenderer {
105 uri: Uri,
107}
108
109impl LspErrorRenderer {
110 pub fn new(uri: Uri) -> Self {
111 Self { uri }
112 }
113
114 pub fn structured_error_to_diagnostic(&self, error: &StructuredParseError) -> Diagnostic {
116 let severity = match error.severity {
117 ErrorSeverity::Error => DiagnosticSeverity::ERROR,
118 ErrorSeverity::Warning => DiagnosticSeverity::WARNING,
119 ErrorSeverity::Info => DiagnosticSeverity::INFORMATION,
120 ErrorSeverity::Hint => DiagnosticSeverity::HINT,
121 };
122
123 let range = self.structured_location_to_range(error);
125
126 let message = format_structured_message(&error.kind, &error.suggestions);
128
129 let related_information = if !error.related.is_empty() {
131 Some(
132 error
133 .related
134 .iter()
135 .map(|rel| DiagnosticRelatedInformation {
136 location: Location {
137 uri: self.uri.clone(),
138 range: self.source_location_to_range(&rel.location),
139 },
140 message: rel.message.clone(),
141 })
142 .collect(),
143 )
144 } else {
145 None
146 };
147
148 let code_str = error.code.as_str().to_string();
149 let code_description = code_description_for(&code_str);
152 let tags = diagnostic_tags_for(Some(&code_str), &message);
153
154 Diagnostic {
155 range,
156 severity: Some(severity),
157 code: Some(NumberOrString::String(code_str)),
158 code_description,
159 source: Some("shape".to_string()),
160 message,
161 related_information,
162 tags,
163 data: None,
164 }
165 }
166
167 fn structured_location_to_range(&self, error: &StructuredParseError) -> Range {
168 let line = error.location.line.saturating_sub(1) as u32;
169 let column = error.location.column.saturating_sub(1) as u32;
170
171 let start = Position {
172 line,
173 character: column,
174 };
175
176 let end = if let Some((end_line, end_col)) = error.span_end {
177 Position {
178 line: end_line.saturating_sub(1) as u32,
179 character: end_col.saturating_sub(1) as u32,
180 }
181 } else if let Some(len) = error.location.length {
182 Position {
183 line,
184 character: column + len as u32,
185 }
186 } else {
187 Position {
189 line,
190 character: column + 1,
191 }
192 };
193
194 Range { start, end }
195 }
196
197 fn source_location_to_range(&self, location: &SourceLocation) -> Range {
198 let line = location.line.saturating_sub(1) as u32;
199 let column = location.column.saturating_sub(1) as u32;
200
201 Range {
202 start: Position {
203 line,
204 character: column,
205 },
206 end: Position {
207 line,
208 character: column + location.length.unwrap_or(1) as u32,
209 },
210 }
211 }
212}
213
214impl ErrorRenderer for LspErrorRenderer {
215 type Output = Vec<Diagnostic>;
216
217 fn render(&self, error: &StructuredParseError) -> Self::Output {
218 vec![self.structured_error_to_diagnostic(error)]
219 }
220
221 fn render_all(&self, errors: &[StructuredParseError]) -> Self::Output {
222 errors
223 .iter()
224 .map(|e| self.structured_error_to_diagnostic(e))
225 .collect()
226 }
227}
228
229fn format_structured_message(
231 kind: &ParseErrorKind,
232 suggestions: &[shape_ast::error::Suggestion],
233) -> String {
234 use shape_ast::error::parse_error::format_error_message;
235
236 let base_message = format_error_message(kind);
237
238 if suggestions.is_empty() {
239 return base_message;
240 }
241
242 let suggestion_text: Vec<String> = suggestions.iter().map(|s| s.message.clone()).collect();
244
245 if suggestion_text.is_empty() {
246 base_message
247 } else {
248 format!("{}\n\n{}", base_message, suggestion_text.join("\n"))
249 }
250}
251
252pub fn error_to_diagnostic(error: &ShapeError) -> Vec<Diagnostic> {
254 error_to_diagnostic_with_uri(error, None)
255}
256
257pub fn error_to_diagnostic_with_uri(error: &ShapeError, uri: Option<Uri>) -> Vec<Diagnostic> {
259 let error_code = error.error_code().map(|c| c.as_str());
261
262 match error {
263 ShapeError::StructuredParse(structured) => {
264 if let Some(uri) = uri {
266 let renderer = LspErrorRenderer::new(uri);
267 renderer.render(structured)
268 } else {
269 vec![create_diagnostic_with_code(
271 &structured.to_string(),
272 Some(&structured.location),
273 DiagnosticSeverity::ERROR,
274 "shape",
275 Some(structured.code.as_str()),
276 )]
277 }
278 }
279 ShapeError::ParseError { message, location } => {
280 vec![create_diagnostic_with_code(
281 message,
282 location.as_ref(),
283 DiagnosticSeverity::ERROR,
284 "shape",
285 error_code,
286 )]
287 }
288 ShapeError::LexError { message, location } => {
289 vec![create_diagnostic_with_code(
290 message,
291 location.as_ref(),
292 DiagnosticSeverity::ERROR,
293 "shape",
294 error_code,
295 )]
296 }
297 ShapeError::SemanticError { message, location } => {
298 vec![create_diagnostic_with_code(
299 message,
300 location.as_ref(),
301 DiagnosticSeverity::ERROR,
302 "shape",
303 error_code,
304 )]
305 }
306 ShapeError::RuntimeError { message, location } => {
307 vec![create_diagnostic_with_code(
308 message,
309 location.as_ref(),
310 DiagnosticSeverity::WARNING,
311 "shape",
312 error_code,
313 )]
314 }
315 ShapeError::TypeError(type_error) => {
316 vec![create_diagnostic_with_code(
318 &type_error.to_string(),
319 None,
320 DiagnosticSeverity::ERROR,
321 "shape",
322 error_code,
323 )]
324 }
325 ShapeError::PatternError {
326 message,
327 pattern_name,
328 } => {
329 let msg = if let Some(name) = pattern_name {
330 format!("Pattern '{}': {}", name, message)
331 } else {
332 message.clone()
333 };
334 vec![create_diagnostic_with_code(
335 &msg,
336 None,
337 DiagnosticSeverity::ERROR,
338 "shape",
339 error_code,
340 )]
341 }
342 ShapeError::DataError {
343 message,
344 symbol,
345 timeframe,
346 } => {
347 let mut msg = message.clone();
348 if let Some(sym) = symbol {
349 msg.push_str(&format!(" (symbol: {})", sym));
350 }
351 if let Some(tf) = timeframe {
352 msg.push_str(&format!(" (timeframe: {})", tf));
353 }
354 vec![create_diagnostic_with_code(
355 &msg,
356 None,
357 DiagnosticSeverity::WARNING,
358 "shape",
359 error_code,
360 )]
361 }
362 ShapeError::ModuleError {
363 message,
364 module_path,
365 } => {
366 let msg = if let Some(path) = module_path {
367 format!("{}: {}", path.display(), message)
368 } else {
369 message.clone()
370 };
371 vec![create_diagnostic_with_code(
372 &msg,
373 None,
374 DiagnosticSeverity::ERROR,
375 "shape",
376 error_code,
377 )]
378 }
379
380 ShapeError::MultiError(errors) => {
381 errors
383 .iter()
384 .flat_map(|e| error_to_diagnostic_with_uri(e, uri.clone()))
385 .collect()
386 }
387
388 _ => vec![create_diagnostic_with_code(
390 &error.to_string(),
391 None,
392 DiagnosticSeverity::ERROR,
393 "shape",
394 error_code,
395 )],
396 }
397}
398
399fn create_diagnostic(
401 message: &str,
402 location: Option<&SourceLocation>,
403 severity: DiagnosticSeverity,
404 source: &str,
405) -> Diagnostic {
406 let range = location_to_range(location);
407
408 let full_message = if let Some(loc) = location {
410 if !loc.hints.is_empty() {
411 let hints = loc
412 .hints
413 .iter()
414 .map(|h| format!("help: {}", h))
415 .collect::<Vec<_>>()
416 .join("\n");
417 format!("{}\n{}", message, hints)
418 } else {
419 message.to_string()
420 }
421 } else {
422 message.to_string()
423 };
424
425 let related_information = if let Some(loc) = location {
427 let mut related = if !loc.notes.is_empty() {
428 notes_to_related_info(&loc.notes, loc.file.as_deref())
429 } else {
430 Vec::new()
431 };
432
433 if let Some(ref file) = loc.file {
436 if let Some(file_uri) = Uri::from_file_path(file) {
437 let line = if loc.line > 0 { loc.line - 1 } else { 0 } as u32;
438 let col = if loc.column > 0 { loc.column - 1 } else { 0 } as u32;
439 let end_char = loc.length.map(|l| col + l as u32).unwrap_or(col + 1);
440 related.push(DiagnosticRelatedInformation {
441 location: Location {
442 uri: file_uri,
443 range: Range {
444 start: Position {
445 line,
446 character: col,
447 },
448 end: Position {
449 line,
450 character: end_char,
451 },
452 },
453 },
454 message: format!("error originates in {}", file),
455 });
456 }
457 }
458
459 if related.is_empty() {
460 None
461 } else {
462 Some(related)
463 }
464 } else {
465 None
466 };
467
468 Diagnostic {
469 range,
470 severity: Some(severity),
471 code: None, code_description: None,
473 source: Some(source.to_string()),
474 message: full_message,
475 related_information,
476 tags: None,
477 data: None,
478 }
479}
480
481fn create_diagnostic_with_code(
483 message: &str,
484 location: Option<&SourceLocation>,
485 severity: DiagnosticSeverity,
486 source: &str,
487 error_code: Option<&str>,
488) -> Diagnostic {
489 let mut diag = create_diagnostic(message, location, severity, source);
490 if let Some(code) = error_code {
491 diag.code = Some(NumberOrString::String(code.to_string()));
492 diag.code_description = code_description_for(code);
494 }
495 diag.tags = diagnostic_tags_for(error_code, &diag.message);
497 diag
498}
499
500fn notes_to_related_info(
502 notes: &[ErrorNote],
503 default_file: Option<&str>,
504) -> Vec<DiagnosticRelatedInformation> {
505 notes
506 .iter()
507 .filter_map(|note| {
508 let location = note.location.as_ref()?;
510 let file = location.file.as_deref().or(default_file)?;
511 let uri = Uri::from_file_path(file)?;
512
513 let line = if location.line > 0 {
514 location.line - 1
515 } else {
516 0
517 } as u32;
518 let column = if location.column > 0 {
519 location.column - 1
520 } else {
521 0
522 } as u32;
523
524 Some(DiagnosticRelatedInformation {
525 location: Location {
526 uri,
527 range: Range {
528 start: Position {
529 line,
530 character: column,
531 },
532 end: Position {
533 line,
534 character: column + 1,
535 },
536 },
537 },
538 message: note.message.clone(),
539 })
540 })
541 .collect()
542}
543
544fn location_to_range(location: Option<&SourceLocation>) -> Range {
546 let full_first_line = Range {
549 start: Position {
550 line: 0,
551 character: 0,
552 },
553 end: Position {
554 line: 0,
555 character: 1000,
556 },
557 };
558
559 if let Some(loc) = location {
560 if loc.is_synthetic {
561 return full_first_line;
562 }
563
564 let line = if loc.line > 0 { loc.line - 1 } else { 0 } as u32;
566 let column = if loc.column > 0 { loc.column - 1 } else { 0 } as u32;
567
568 let start = Position {
569 line,
570 character: column,
571 };
572
573 let end = if let Some(len) = loc.length {
575 Position {
576 line,
577 character: column + len as u32,
578 }
579 } else {
580 Position {
582 line,
583 character: column + 100, }
585 };
586
587 Range { start, end }
588 } else {
589 full_first_line
590 }
591}
592
593pub fn validate_annotations(
599 program: &Program,
600 annotation_discovery: &AnnotationDiscovery,
601 source: &str,
602) -> Vec<Diagnostic> {
603 let mut diagnostics = Vec::new();
604
605 for item in &program.items {
606 let (annotations, span) = match item {
607 Item::Function(func, span) => (&func.annotations, span),
608 Item::ForeignFunction(foreign_fn, span) => (&foreign_fn.annotations, span),
609 _ => continue,
610 };
611 for annotation in annotations {
612 if let Some(diag) = validate_annotation(annotation, span, annotation_discovery, source)
613 {
614 diagnostics.push(diag);
615 }
616 }
617 }
618
619 diagnostics
620}
621
622fn validate_annotation(
624 annotation: &Annotation,
625 item_span: &Span,
626 annotation_discovery: &AnnotationDiscovery,
627 source: &str,
628) -> Option<Diagnostic> {
629 let name = &annotation.name;
630
631 if !annotation_discovery.is_defined(name) {
633 let available: Vec<_> = annotation_discovery
634 .all_annotations()
635 .iter()
636 .map(|a| format!("@{}", a.name))
637 .collect();
638
639 let message = if available.is_empty() {
640 format!("Undefined annotation: @{}", name)
641 } else {
642 format!(
643 "Undefined annotation: @{}. Available: {}",
644 name,
645 available.join(", ")
646 )
647 };
648
649 let range = span_to_range(source, item_span);
651
652 return Some(Diagnostic {
653 range,
654 severity: Some(DiagnosticSeverity::ERROR),
655 code: Some(NumberOrString::String("E0100".to_string())),
656 code_description: None,
657 source: Some("shape".to_string()),
658 message,
659 related_information: None,
660 tags: None,
661 data: None,
662 });
663 }
664
665 if let Some(ann_info) = annotation_discovery.get(name) {
667 let expected = ann_info.params.len();
668 let actual = annotation.args.len();
669
670 if actual > expected || (actual < expected && expected > 0 && actual > 0) {
672 let range = span_to_range(source, item_span);
673
674 return Some(Diagnostic {
675 range,
676 severity: Some(DiagnosticSeverity::WARNING),
677 code: Some(NumberOrString::String("W0101".to_string())),
678 code_description: None,
679 source: Some("shape".to_string()),
680 message: format!("@{} expects {} argument(s), got {}", name, expected, actual),
681 related_information: None,
682 tags: None,
683 data: None,
684 });
685 }
686 }
687
688 None
689}
690
691pub fn validate_async_join(program: &Program, source: &str) -> Vec<Diagnostic> {
695 use shape_ast::ast::Expr;
696 use shape_runtime::visitor::{Visitor, walk_program};
697
698 struct AsyncJoinValidator<'a> {
699 source: &'a str,
700 async_depth_stack: Vec<bool>,
701 diagnostics: Vec<Diagnostic>,
702 }
703
704 impl AsyncJoinValidator<'_> {
705 fn is_in_async(&self) -> bool {
706 self.async_depth_stack.last().copied().unwrap_or(false)
707 }
708 }
709
710 impl Visitor for AsyncJoinValidator<'_> {
711 fn visit_function(&mut self, func: &shape_ast::ast::FunctionDef) -> bool {
712 self.async_depth_stack.push(func.is_async);
713 true
714 }
715
716 fn leave_function(&mut self, _func: &shape_ast::ast::FunctionDef) {
717 self.async_depth_stack.pop();
718 }
719
720 fn visit_expr(&mut self, expr: &Expr) -> bool {
721 if let Expr::Join(_, span) = expr {
722 if !self.is_in_async() {
723 let range = span_to_range(self.source, span);
724 self.diagnostics.push(Diagnostic {
725 range,
726 severity: Some(DiagnosticSeverity::ERROR),
727 code: Some(NumberOrString::String("E0200".to_string())),
728 code_description: None,
729 source: Some("shape".to_string()),
730 message: "`await join` can only be used inside an async function"
731 .to_string(),
732 related_information: None,
733 tags: None,
734 data: None,
735 });
736 }
737 }
738 true
739 }
740 }
741
742 let mut validator = AsyncJoinValidator {
743 source,
744 async_depth_stack: Vec::new(),
745 diagnostics: Vec::new(),
746 };
747 walk_program(&mut validator, program);
748 validator.diagnostics
749}
750
751pub fn validate_async_structured_concurrency(program: &Program, source: &str) -> Vec<Diagnostic> {
754 use shape_ast::ast::Expr;
755 use shape_runtime::visitor::{Visitor, walk_program};
756
757 struct AsyncStructuredValidator<'a> {
758 source: &'a str,
759 async_depth_stack: Vec<bool>,
760 diagnostics: Vec<Diagnostic>,
761 }
762
763 impl AsyncStructuredValidator<'_> {
764 fn is_in_async(&self) -> bool {
765 self.async_depth_stack.last().copied().unwrap_or(false)
766 }
767 }
768
769 impl Visitor for AsyncStructuredValidator<'_> {
770 fn visit_function(&mut self, func: &shape_ast::ast::FunctionDef) -> bool {
771 self.async_depth_stack.push(func.is_async);
772 true
773 }
774
775 fn leave_function(&mut self, _func: &shape_ast::ast::FunctionDef) {
776 self.async_depth_stack.pop();
777 }
778
779 fn visit_expr(&mut self, expr: &Expr) -> bool {
780 match expr {
781 Expr::AsyncLet(_, span) => {
782 if !self.is_in_async() {
783 let range = span_to_range(self.source, span);
784 self.diagnostics.push(Diagnostic {
785 range,
786 severity: Some(DiagnosticSeverity::ERROR),
787 code: Some(NumberOrString::String("E0201".to_string())),
788 code_description: None,
789 source: Some("shape".to_string()),
790 message: "`async let` can only be used inside an async function"
791 .to_string(),
792 related_information: None,
793 tags: None,
794 data: None,
795 });
796 }
797 }
798 Expr::AsyncScope(_, span) => {
799 if !self.is_in_async() {
800 let range = span_to_range(self.source, span);
801 self.diagnostics.push(Diagnostic {
802 range,
803 severity: Some(DiagnosticSeverity::ERROR),
804 code: Some(NumberOrString::String("E0202".to_string())),
805 code_description: None,
806 source: Some("shape".to_string()),
807 message: "`async scope` can only be used inside an async function"
808 .to_string(),
809 related_information: None,
810 tags: None,
811 data: None,
812 });
813 }
814 }
815 Expr::For(for_expr, span) if for_expr.is_async => {
816 if !self.is_in_async() {
817 let range = span_to_range(self.source, span);
818 self.diagnostics.push(Diagnostic {
819 range,
820 severity: Some(DiagnosticSeverity::ERROR),
821 code: Some(NumberOrString::String("E0203".to_string())),
822 code_description: None,
823 source: Some("shape".to_string()),
824 message: "`for await` can only be used inside an async function"
825 .to_string(),
826 related_information: None,
827 tags: None,
828 data: None,
829 });
830 }
831 }
832 _ => {}
833 }
834 true
835 }
836
837 fn visit_stmt(&mut self, stmt: &shape_ast::ast::Statement) -> bool {
838 if let shape_ast::ast::Statement::For(for_loop, span) = stmt {
839 if for_loop.is_async && !self.is_in_async() {
840 let range = span_to_range(self.source, span);
841 self.diagnostics.push(Diagnostic {
842 range,
843 severity: Some(DiagnosticSeverity::ERROR),
844 code: Some(NumberOrString::String("E0203".to_string())),
845 code_description: None,
846 source: Some("shape".to_string()),
847 message: "`for await` can only be used inside an async function"
848 .to_string(),
849 related_information: None,
850 tags: None,
851 data: None,
852 });
853 }
854 }
855 true
856 }
857 }
858
859 let mut validator = AsyncStructuredValidator {
860 source,
861 async_depth_stack: Vec::new(),
862 diagnostics: Vec::new(),
863 };
864 walk_program(&mut validator, program);
865 validator.diagnostics
866}
867
868pub fn validate_interpolation_format_specs(program: &Program, source: &str) -> Vec<Diagnostic> {
873 use shape_ast::interpolation::parse_interpolation_with_mode;
874 use shape_runtime::visitor::{Visitor, walk_program};
875
876 struct InterpolationFormatSpecValidator<'a> {
877 source: &'a str,
878 diagnostics: Vec<Diagnostic>,
879 }
880
881 impl Visitor for InterpolationFormatSpecValidator<'_> {
882 fn visit_expr(&mut self, expr: &Expr) -> bool {
883 if let Expr::Literal(Literal::FormattedString { value, mode }, span) = expr {
884 if let Err(err) = parse_interpolation_with_mode(value, *mode) {
885 let range = span_to_range(self.source, span);
886 self.diagnostics.push(Diagnostic {
887 range,
888 severity: Some(DiagnosticSeverity::ERROR),
889 code: Some(NumberOrString::String("E0300".to_string())),
890 code_description: None,
891 source: Some("shape".to_string()),
892 message: format!("Invalid interpolation format spec: {}", err),
893 related_information: None,
894 tags: None,
895 data: None,
896 });
897 }
898 }
899 true
900 }
901 }
902
903 let mut validator = InterpolationFormatSpecValidator {
904 source,
905 diagnostics: Vec::new(),
906 };
907 walk_program(&mut validator, program);
908 validator.diagnostics
909}
910
911pub fn validate_comptime_overrides(program: &Program, source: &str) -> Vec<Diagnostic> {
916 use std::collections::HashMap;
917
918 let mut diagnostics = Vec::new();
919
920 let mut struct_comptime_fields: HashMap<String, Vec<String>> = HashMap::new();
922 for item in &program.items {
923 if let Item::StructType(struct_def, _) = item {
924 let comptime_names: Vec<String> = struct_def
925 .fields
926 .iter()
927 .filter(|f| f.is_comptime)
928 .map(|f| f.name.clone())
929 .collect();
930 struct_comptime_fields.insert(struct_def.name.clone(), comptime_names);
931 }
932 }
933
934 for item in &program.items {
936 if let Item::TypeAlias(alias_def, span) = item {
937 if let Some(overrides) = &alias_def.meta_param_overrides {
938 let base_type = match &alias_def.type_annotation {
940 shape_ast::ast::TypeAnnotation::Basic(name) => name.clone(),
941 _ => continue,
942 };
943
944 if let Some(comptime_fields) = struct_comptime_fields.get(&base_type) {
945 for (field_name, _value) in overrides {
946 if !comptime_fields.contains(field_name) {
947 let range = span_to_range(source, span);
949 diagnostics.push(Diagnostic {
950 range,
951 severity: Some(DiagnosticSeverity::ERROR),
952 code: Some(NumberOrString::String("E0300".to_string())),
953 code_description: None,
954 source: Some("shape".to_string()),
955 message: format!(
956 "Cannot override field '{}': only comptime fields can be overridden in type alias. \
957 '{}' is not a comptime field of '{}'.",
958 field_name, field_name, base_type
959 ),
960 related_information: None,
961 tags: None,
962 data: None,
963 });
964 }
965 }
966 }
967 }
968 }
969 }
970
971 diagnostics
972}
973
974pub fn validate_comptime_side_effects(program: &Program, source: &str) -> Vec<Diagnostic> {
979 let mut diagnostics = Vec::new();
980
981 for item in &program.items {
983 match item {
984 Item::Comptime(stmts, span) => {
985 check_stmts_for_side_effects(stmts, span, source, &mut diagnostics);
986 }
987 _ => {
988 visit_item_exprs(item, source, &mut diagnostics);
989 }
990 }
991 }
992
993 diagnostics
994}
995
996const SIDE_EFFECT_FNS: &[&str] = &["print", "println", "debug", "log", "write", "fetch"];
998
999fn check_stmts_for_side_effects(
1000 stmts: &[Statement],
1001 _block_span: &Span,
1002 source: &str,
1003 diagnostics: &mut Vec<Diagnostic>,
1004) {
1005 for stmt in stmts {
1006 check_stmt_for_side_effects(stmt, source, diagnostics);
1007 }
1008}
1009
1010fn check_stmt_for_side_effects(stmt: &Statement, source: &str, diagnostics: &mut Vec<Diagnostic>) {
1011 match stmt {
1012 Statement::Expression(expr, _) => check_expr_for_side_effects(expr, source, diagnostics),
1013 Statement::VariableDecl(decl, _) => {
1014 if let Some(init) = &decl.value {
1015 check_expr_for_side_effects(init, source, diagnostics);
1016 }
1017 }
1018 Statement::Return(Some(expr), _) => check_expr_for_side_effects(expr, source, diagnostics),
1019 Statement::For(for_loop, _) => {
1020 for s in &for_loop.body {
1021 check_stmt_for_side_effects(s, source, diagnostics);
1022 }
1023 }
1024 Statement::While(while_loop, _) => {
1025 for s in &while_loop.body {
1026 check_stmt_for_side_effects(s, source, diagnostics);
1027 }
1028 }
1029 Statement::If(if_stmt, _) => {
1030 for s in &if_stmt.then_body {
1031 check_stmt_for_side_effects(s, source, diagnostics);
1032 }
1033 if let Some(else_body) = &if_stmt.else_body {
1034 for s in else_body {
1035 check_stmt_for_side_effects(s, source, diagnostics);
1036 }
1037 }
1038 }
1039 _ => {}
1040 }
1041}
1042
1043fn check_expr_for_side_effects(expr: &Expr, source: &str, diagnostics: &mut Vec<Diagnostic>) {
1044 match expr {
1045 Expr::FunctionCall {
1046 name,
1047 span,
1048 args,
1049 named_args,
1050 } => {
1051 if SIDE_EFFECT_FNS.contains(&name.as_str()) {
1052 let range = span_to_range(source, span);
1053 diagnostics.push(Diagnostic {
1054 range,
1055 severity: Some(DiagnosticSeverity::WARNING),
1056 code: Some(NumberOrString::String("W0100".to_string())),
1057 code_description: None,
1058 source: Some("shape".to_string()),
1059 message: format!(
1060 "Side effect in comptime block: `{}()` performs I/O at compile time. \
1061 Consider removing or using a comptime-safe alternative.",
1062 name
1063 ),
1064 related_information: None,
1065 tags: None,
1066 data: None,
1067 });
1068 }
1069 for arg in args {
1071 check_expr_for_side_effects(arg, source, diagnostics);
1072 }
1073 for (_, arg) in named_args {
1074 check_expr_for_side_effects(arg, source, diagnostics);
1075 }
1076 }
1077 Expr::Comptime(stmts, span) => {
1078 check_stmts_for_side_effects(stmts, span, source, diagnostics);
1080 }
1081 _ => {}
1082 }
1083}
1084
1085fn visit_item_exprs(item: &Item, source: &str, diagnostics: &mut Vec<Diagnostic>) {
1087 match item {
1089 Item::Function(func_def, _) => {
1090 for stmt in &func_def.body {
1091 visit_stmt_for_comptime(stmt, source, diagnostics);
1092 }
1093 }
1094 Item::VariableDecl(decl, _) => {
1095 if let Some(init) = &decl.value {
1096 visit_expr_for_comptime(init, source, diagnostics);
1097 }
1098 }
1099 Item::Expression(expr, _) => {
1100 visit_expr_for_comptime(expr, source, diagnostics);
1101 }
1102 Item::Statement(stmt, _) => {
1103 visit_stmt_for_comptime(stmt, source, diagnostics);
1104 }
1105 _ => {}
1106 }
1107}
1108
1109fn visit_stmt_for_comptime(stmt: &Statement, source: &str, diagnostics: &mut Vec<Diagnostic>) {
1110 match stmt {
1111 Statement::Expression(expr, _) => visit_expr_for_comptime(expr, source, diagnostics),
1112 Statement::VariableDecl(decl, _) => {
1113 if let Some(init) = &decl.value {
1114 visit_expr_for_comptime(init, source, diagnostics);
1115 }
1116 }
1117 Statement::Return(Some(expr), _) => visit_expr_for_comptime(expr, source, diagnostics),
1118 Statement::For(for_loop, _) => {
1119 for s in &for_loop.body {
1120 visit_stmt_for_comptime(s, source, diagnostics);
1121 }
1122 }
1123 Statement::While(while_loop, _) => {
1124 for s in &while_loop.body {
1125 visit_stmt_for_comptime(s, source, diagnostics);
1126 }
1127 }
1128 Statement::If(if_stmt, _) => {
1129 for s in &if_stmt.then_body {
1130 visit_stmt_for_comptime(s, source, diagnostics);
1131 }
1132 if let Some(else_body) = &if_stmt.else_body {
1133 for s in else_body {
1134 visit_stmt_for_comptime(s, source, diagnostics);
1135 }
1136 }
1137 }
1138 _ => {}
1139 }
1140}
1141
1142fn visit_expr_for_comptime(expr: &Expr, source: &str, diagnostics: &mut Vec<Diagnostic>) {
1143 match expr {
1144 Expr::Comptime(stmts, span) => {
1145 check_stmts_for_side_effects(stmts, span, source, diagnostics);
1146 }
1147 Expr::FunctionCall {
1148 args, named_args, ..
1149 } => {
1150 for arg in args {
1151 visit_expr_for_comptime(arg, source, diagnostics);
1152 }
1153 for (_, arg) in named_args {
1154 visit_expr_for_comptime(arg, source, diagnostics);
1155 }
1156 }
1157 Expr::Conditional {
1158 condition,
1159 then_expr,
1160 else_expr,
1161 ..
1162 } => {
1163 visit_expr_for_comptime(condition, source, diagnostics);
1164 visit_expr_for_comptime(then_expr, source, diagnostics);
1165 if let Some(e) = else_expr {
1166 visit_expr_for_comptime(e, source, diagnostics);
1167 }
1168 }
1169 Expr::BinaryOp { left, right, .. } => {
1170 visit_expr_for_comptime(left, source, diagnostics);
1171 visit_expr_for_comptime(right, source, diagnostics);
1172 }
1173 Expr::UnaryOp { operand, .. } => {
1174 visit_expr_for_comptime(operand, source, diagnostics);
1175 }
1176 _ => {}
1177 }
1178}
1179
1180pub fn validate_comptime_builtins_context(program: &Program, source: &str) -> Vec<Diagnostic> {
1182 let mut diagnostics = Vec::new();
1183
1184 for item in &program.items {
1185 match item {
1186 Item::Comptime(_, _) => {
1187 }
1189 Item::Function(func_def, _) => {
1190 for stmt in &func_def.body {
1191 check_stmt_comptime_only(stmt, false, source, &mut diagnostics);
1192 }
1193 }
1194 Item::VariableDecl(decl, _) => {
1195 if let Some(init) = &decl.value {
1196 check_expr_comptime_only(init, false, source, &mut diagnostics);
1197 }
1198 }
1199 Item::Expression(expr, _) => {
1200 check_expr_comptime_only(expr, false, source, &mut diagnostics);
1201 }
1202 Item::Statement(stmt, _) => {
1203 check_stmt_comptime_only(stmt, false, source, &mut diagnostics);
1204 }
1205 _ => {}
1206 }
1207 }
1208
1209 diagnostics
1210}
1211
1212fn check_stmt_comptime_only(
1213 stmt: &Statement,
1214 in_comptime: bool,
1215 source: &str,
1216 diagnostics: &mut Vec<Diagnostic>,
1217) {
1218 match stmt {
1219 Statement::Expression(expr, _) => {
1220 check_expr_comptime_only(expr, in_comptime, source, diagnostics);
1221 }
1222 Statement::VariableDecl(decl, _) => {
1223 if let Some(init) = &decl.value {
1224 check_expr_comptime_only(init, in_comptime, source, diagnostics);
1225 }
1226 }
1227 Statement::Return(Some(expr), _) => {
1228 check_expr_comptime_only(expr, in_comptime, source, diagnostics);
1229 }
1230 Statement::For(for_loop, _) => {
1231 for s in &for_loop.body {
1232 check_stmt_comptime_only(s, in_comptime, source, diagnostics);
1233 }
1234 }
1235 Statement::While(while_loop, _) => {
1236 for s in &while_loop.body {
1237 check_stmt_comptime_only(s, in_comptime, source, diagnostics);
1238 }
1239 }
1240 Statement::If(if_stmt, _) => {
1241 for s in &if_stmt.then_body {
1242 check_stmt_comptime_only(s, in_comptime, source, diagnostics);
1243 }
1244 if let Some(else_body) = &if_stmt.else_body {
1245 for s in else_body {
1246 check_stmt_comptime_only(s, in_comptime, source, diagnostics);
1247 }
1248 }
1249 }
1250 _ => {}
1251 }
1252}
1253
1254fn check_expr_comptime_only(
1255 expr: &Expr,
1256 in_comptime: bool,
1257 source: &str,
1258 diagnostics: &mut Vec<Diagnostic>,
1259) {
1260 match expr {
1261 Expr::Comptime(stmts, _) => {
1262 for stmt in stmts {
1264 check_stmt_comptime_only(stmt, true, source, diagnostics);
1265 }
1266 }
1267 Expr::FunctionCall {
1268 name,
1269 span,
1270 args,
1271 named_args,
1272 } => {
1273 let is_comptime_only = unified_metadata()
1274 .get_function(name)
1275 .map(|f| f.comptime_only)
1276 .unwrap_or(false);
1277 if !in_comptime && is_comptime_only {
1278 let range = span_to_range(source, span);
1279 diagnostics.push(Diagnostic {
1280 range,
1281 severity: Some(DiagnosticSeverity::ERROR),
1282 code: Some(NumberOrString::String("E0301".to_string())),
1283 code_description: None,
1284 source: Some("shape".to_string()),
1285 message: format!(
1286 "`{}()` is a comptime-only builtin and can only be called inside a `comptime {{ }}` block.",
1287 name
1288 ),
1289 related_information: None,
1290 tags: None,
1291 data: None,
1292 });
1293 }
1294 for arg in args {
1295 check_expr_comptime_only(arg, in_comptime, source, diagnostics);
1296 }
1297 for (_, arg) in named_args {
1298 check_expr_comptime_only(arg, in_comptime, source, diagnostics);
1299 }
1300 }
1301 Expr::Conditional {
1302 condition,
1303 then_expr,
1304 else_expr,
1305 ..
1306 } => {
1307 check_expr_comptime_only(condition, in_comptime, source, diagnostics);
1308 check_expr_comptime_only(then_expr, in_comptime, source, diagnostics);
1309 if let Some(e) = else_expr {
1310 check_expr_comptime_only(e, in_comptime, source, diagnostics);
1311 }
1312 }
1313 Expr::BinaryOp { left, right, .. } => {
1314 check_expr_comptime_only(left, in_comptime, source, diagnostics);
1315 check_expr_comptime_only(right, in_comptime, source, diagnostics);
1316 }
1317 Expr::UnaryOp { operand, .. } => {
1318 check_expr_comptime_only(operand, in_comptime, source, diagnostics);
1319 }
1320 _ => {}
1321 }
1322}
1323
1324pub fn validate_trait_bounds(program: &Program, source: &str) -> Vec<Diagnostic> {
1330 let mut diagnostics = Vec::new();
1331
1332 let mut trait_methods: std::collections::HashMap<String, Vec<String>> =
1334 std::collections::HashMap::new();
1335 let mut trait_spans: std::collections::HashMap<String, Span> = std::collections::HashMap::new();
1336 for item in &program.items {
1337 if let Item::Trait(trait_def, span) = item {
1338 let required: Vec<String> = trait_def
1339 .members
1340 .iter()
1341 .filter_map(|m| match m {
1342 shape_ast::ast::TraitMember::Required(
1343 shape_ast::ast::TraitMemberSignature::Method { name, .. },
1344 ) => Some(name.clone()),
1345 _ => None,
1346 })
1347 .collect();
1348 trait_methods.insert(trait_def.name.clone(), required);
1349 trait_spans.insert(trait_def.name.clone(), *span);
1350 }
1351 }
1352
1353 for item in &program.items {
1355 if let Item::Function(func, span) = item {
1356 if let Some(type_params) = &func.type_params {
1357 for tp in type_params {
1358 for bound in tp.trait_bounds() {
1363 if !trait_methods.contains_key(bound.as_str()) {
1364 let range = span_to_range(source, span);
1365 diagnostics.push(Diagnostic {
1366 range,
1367 severity: Some(DiagnosticSeverity::ERROR),
1368 code: Some(NumberOrString::String("E0400".to_string())),
1369 code_description: None,
1370 source: Some("shape".to_string()),
1371 message: format!(
1372 "Trait bound '{}' on type parameter '{}' refers to an undefined trait.",
1373 bound, tp.name()
1374 ),
1375 related_information: None,
1376 tags: None,
1377 data: None,
1378 });
1379 }
1380 }
1381 }
1382 }
1383 }
1384 }
1385
1386 for item in &program.items {
1388 if let Item::Impl(impl_block, span) = item {
1389 let trait_name = match &impl_block.trait_name {
1390 shape_ast::ast::TypeName::Simple(n) => n.to_string(),
1391 shape_ast::ast::TypeName::Generic { name, .. } => name.to_string(),
1392 };
1393 let target_type = match &impl_block.target_type {
1394 shape_ast::ast::TypeName::Simple(n) => n.to_string(),
1395 shape_ast::ast::TypeName::Generic { name, .. } => name.to_string(),
1396 };
1397
1398 if let Some(required_methods) = trait_methods.get(&trait_name) {
1399 let implemented: Vec<String> =
1400 impl_block.methods.iter().map(|m| m.name.clone()).collect();
1401 for required in required_methods {
1402 if !implemented.contains(required) {
1403 let range = span_to_range(source, span);
1404 diagnostics.push(Diagnostic {
1405 range,
1406 severity: Some(DiagnosticSeverity::ERROR),
1407 code: Some(NumberOrString::String("E0401".to_string())),
1408 code_description: None,
1409 source: Some("shape".to_string()),
1410 message: format!(
1411 "Missing required method '{}' in impl {} for {}.",
1412 required, trait_name, target_type
1413 ),
1414 related_information: None,
1415 tags: None,
1416 data: None,
1417 });
1418 }
1419 }
1420 }
1421 }
1422 }
1423
1424 diagnostics
1425}
1426
1427pub fn validate_color_rgb_range(program: &Program, source: &str) -> Vec<Diagnostic> {
1431 use shape_runtime::visitor::{Visitor, walk_program};
1432
1433 struct ColorRgbValidator<'a> {
1434 source: &'a str,
1435 diagnostics: Vec<Diagnostic>,
1436 }
1437
1438 impl Visitor for ColorRgbValidator<'_> {
1439 fn visit_expr(&mut self, expr: &Expr) -> bool {
1440 if let Expr::MethodCall {
1442 receiver,
1443 method,
1444 args,
1445 span,
1446 ..
1447 } = expr
1448 {
1449 if method == "rgb" {
1450 if let Expr::Identifier(name, _) = receiver.as_ref() {
1451 if name == "Color" {
1452 for arg in args {
1453 let out_of_range = match arg {
1454 Expr::Literal(Literal::Int(v), _) => *v < 0 || *v > 255,
1455 Expr::Literal(Literal::Number(v), _) => {
1456 (*v as i64) < 0 || (*v as i64) > 255
1457 }
1458 _ => false,
1459 };
1460 if out_of_range {
1461 let val_str = match arg {
1462 Expr::Literal(Literal::Int(v), _) => v.to_string(),
1463 Expr::Literal(Literal::Number(v), _) => v.to_string(),
1464 _ => String::new(),
1465 };
1466 let range = span_to_range(self.source, span);
1467 self.diagnostics.push(Diagnostic {
1468 range,
1469 severity: Some(DiagnosticSeverity::WARNING),
1470 code: Some(NumberOrString::String("W0310".to_string())),
1471 code_description: None,
1472 source: Some("shape".to_string()),
1473 message: format!(
1474 "Color.rgb() component value {} is outside the valid range 0-255.",
1475 val_str
1476 ),
1477 related_information: None,
1478 tags: None,
1479 data: None,
1480 });
1481 }
1482 }
1483 }
1484 }
1485 }
1486 }
1487 true
1488 }
1489 }
1490
1491 let mut validator = ColorRgbValidator {
1492 source,
1493 diagnostics: Vec::new(),
1494 };
1495 walk_program(&mut validator, program);
1496 validator.diagnostics
1497}
1498
1499pub fn validate_foreign_function_types(program: &Program, source: &str) -> Vec<Diagnostic> {
1504 let mut diagnostics = Vec::new();
1505
1506 for item in &program.items {
1507 let foreign_fn = match item {
1508 Item::ForeignFunction(f, _) => f,
1509 Item::Export(export, _) => {
1510 if let shape_ast::ast::ExportItem::ForeignFunction(f) = &export.item {
1511 f
1512 } else {
1513 continue;
1514 }
1515 }
1516 _ => continue,
1517 };
1518
1519 for (msg, span) in foreign_fn.validate_type_annotations(true) {
1520 let range = if span.is_dummy() {
1521 span_to_range(source, &foreign_fn.name_span)
1522 } else {
1523 span_to_range(source, &span)
1524 };
1525 diagnostics.push(Diagnostic {
1526 range,
1527 severity: Some(DiagnosticSeverity::ERROR),
1528 code: Some(NumberOrString::String("E0400".to_string())),
1529 code_description: None,
1530 source: Some("shape".to_string()),
1531 message: msg,
1532 related_information: None,
1533 tags: None,
1534 data: None,
1535 });
1536 }
1537 }
1538
1539 diagnostics
1540}
1541
1542pub fn borrow_analysis_to_diagnostics(
1562 analysis: &shape_vm::mir::analysis::BorrowAnalysis,
1563 source: &str,
1564 uri: &Uri,
1565) -> Vec<Diagnostic> {
1566 let mut diagnostics = Vec::new();
1567
1568 for error in &analysis.errors {
1569 let code = error.kind.code();
1570
1571 let primary_range = span_to_range(source, &error.span);
1572
1573 let message = borrow_error_message(&error.kind, code);
1574
1575 let mut related = Vec::new();
1577
1578 let loan_range = span_to_range(source, &error.loan_span);
1580 related.push(DiagnosticRelatedInformation {
1581 location: Location {
1582 uri: uri.clone(),
1583 range: loan_range,
1584 },
1585 message: borrow_origin_note(&error.kind),
1586 });
1587
1588 if let Some(last_use) = error.last_use_span {
1590 let last_use_range = span_to_range(source, &last_use);
1591 related.push(DiagnosticRelatedInformation {
1592 location: Location {
1593 uri: uri.clone(),
1594 range: last_use_range,
1595 },
1596 message: "borrow is still needed here".to_string(),
1597 });
1598 }
1599
1600 let hint = if let Some(repair) = error.repairs.first() {
1602 format!(
1603 "help: {}\nhelp: {}",
1604 borrow_error_hint(&error.kind),
1605 repair.description
1606 )
1607 } else {
1608 format!("help: {}", borrow_error_hint(&error.kind))
1609 };
1610
1611 diagnostics.push(Diagnostic {
1612 range: primary_range,
1613 severity: Some(DiagnosticSeverity::ERROR),
1614 code: Some(NumberOrString::String(code.as_str().to_string())),
1615 code_description: None,
1616 source: Some("shape-borrow".to_string()),
1617 message: format!("{}\n{}", message, hint),
1618 related_information: Some(related),
1619 tags: None,
1620 data: None,
1621 });
1622 }
1623
1624 for error in &analysis.mutability_errors {
1625 let primary_range = span_to_range(source, &error.span);
1626
1627 let binding_kind = if error.is_const {
1628 "const"
1629 } else if error.is_explicit_let {
1630 "let"
1631 } else {
1632 "immutable"
1633 };
1634
1635 let message = format!(
1636 "cannot assign to {} binding '{}'",
1637 binding_kind, error.variable_name
1638 );
1639
1640 let decl_range = span_to_range(source, &error.declaration_span);
1641 let related = vec![DiagnosticRelatedInformation {
1642 location: Location {
1643 uri: uri.clone(),
1644 range: decl_range,
1645 },
1646 message: format!("'{}' declared here", error.variable_name),
1647 }];
1648
1649 diagnostics.push(Diagnostic {
1650 range: primary_range,
1651 severity: Some(DiagnosticSeverity::ERROR),
1652 code: Some(NumberOrString::String("E0384".to_string())),
1653 code_description: None,
1654 source: Some("shape-borrow".to_string()),
1655 message: format!(
1656 "{}\nhelp: consider changing '{}' to 'let mut {}' or 'var {}'",
1657 message, error.variable_name, error.variable_name, error.variable_name
1658 ),
1659 related_information: Some(related),
1660 tags: None,
1661 data: None,
1662 });
1663 }
1664
1665 diagnostics
1666}
1667
1668fn borrow_error_message(
1670 kind: &shape_vm::mir::analysis::BorrowErrorKind,
1671 code: shape_vm::mir::analysis::BorrowErrorCode,
1672) -> String {
1673 use shape_vm::mir::analysis::BorrowErrorKind;
1674 let body = match kind {
1675 BorrowErrorKind::ConflictSharedExclusive => {
1676 "cannot mutably borrow this value while shared borrows are active"
1677 }
1678 BorrowErrorKind::ConflictExclusiveExclusive => {
1679 "cannot mutably borrow this value because it is already borrowed"
1680 }
1681 BorrowErrorKind::ReadWhileExclusivelyBorrowed => {
1682 "cannot read this value while it is mutably borrowed"
1683 }
1684 BorrowErrorKind::WriteWhileBorrowed => {
1685 "cannot write to this value while it is borrowed"
1686 }
1687 BorrowErrorKind::ReferenceEscape => {
1688 "cannot return or store a reference that outlives its owner"
1689 }
1690 BorrowErrorKind::ReferenceStoredInArray => {
1691 "cannot store a reference in an array"
1692 }
1693 BorrowErrorKind::ReferenceStoredInObject => {
1694 "cannot store a reference in an object or struct literal"
1695 }
1696 BorrowErrorKind::ReferenceStoredInEnum => {
1697 "cannot store a reference in an enum payload"
1698 }
1699 BorrowErrorKind::ReferenceEscapeIntoClosure => {
1700 "reference cannot escape into a closure"
1701 }
1702 BorrowErrorKind::UseAfterMove => {
1703 "cannot use this value after it was moved"
1704 }
1705 BorrowErrorKind::ExclusiveRefAcrossTaskBoundary => {
1706 "cannot move an exclusive reference across a task boundary"
1707 }
1708 BorrowErrorKind::SharedRefAcrossDetachedTask => {
1709 "cannot send a shared reference across a detached task boundary"
1710 }
1711 BorrowErrorKind::InconsistentReferenceReturn => {
1712 "reference-returning functions must return a reference on every path from the same borrowed origin and borrow kind"
1713 }
1714 BorrowErrorKind::CallSiteAliasConflict => {
1715 "cannot pass the same variable to multiple parameters that conflict on aliasing"
1716 }
1717 BorrowErrorKind::NonSendableAcrossTaskBoundary => {
1718 "cannot send a non-sendable value across a task boundary"
1719 }
1720 };
1721 format!("[{}] {}", code, body)
1722}
1723
1724fn borrow_error_hint(kind: &shape_vm::mir::analysis::BorrowErrorKind) -> &'static str {
1726 use shape_vm::mir::analysis::BorrowErrorKind;
1727 match kind {
1728 BorrowErrorKind::ConflictSharedExclusive => {
1729 "move the mutable borrow later, or end the shared borrow sooner"
1730 }
1731 BorrowErrorKind::ConflictExclusiveExclusive => {
1732 "end the previous mutable borrow before creating another one"
1733 }
1734 BorrowErrorKind::ReadWhileExclusivelyBorrowed => {
1735 "read through the existing reference, or move the read after the borrow ends"
1736 }
1737 BorrowErrorKind::WriteWhileBorrowed => "move this write after the borrow ends",
1738 BorrowErrorKind::ReferenceEscape => "return an owned value instead of a reference",
1739 BorrowErrorKind::ReferenceStoredInArray
1740 | BorrowErrorKind::ReferenceStoredInObject
1741 | BorrowErrorKind::ReferenceStoredInEnum => {
1742 "store owned values instead of references"
1743 }
1744 BorrowErrorKind::ReferenceEscapeIntoClosure => {
1745 "capture an owned value instead of a reference"
1746 }
1747 BorrowErrorKind::UseAfterMove => {
1748 "clone the value before moving it, or stop using the original after the move"
1749 }
1750 BorrowErrorKind::ExclusiveRefAcrossTaskBoundary => {
1751 "keep the mutable reference within the current task or pass an owned value instead"
1752 }
1753 BorrowErrorKind::SharedRefAcrossDetachedTask => {
1754 "clone the value before sending it to a detached task, or use a structured task instead"
1755 }
1756 BorrowErrorKind::InconsistentReferenceReturn => {
1757 "return a reference from the same borrowed origin on every path, or return owned values instead"
1758 }
1759 BorrowErrorKind::CallSiteAliasConflict => {
1760 "use separate variables for each argument, or clone one of them"
1761 }
1762 BorrowErrorKind::NonSendableAcrossTaskBoundary => {
1763 "clone the captured state or use an owned value that is safe to send across tasks"
1764 }
1765 }
1766}
1767
1768fn borrow_origin_note(kind: &shape_vm::mir::analysis::BorrowErrorKind) -> String {
1770 use shape_vm::mir::analysis::BorrowErrorKind;
1771 match kind {
1772 BorrowErrorKind::ConflictSharedExclusive
1773 | BorrowErrorKind::ConflictExclusiveExclusive
1774 | BorrowErrorKind::ReadWhileExclusivelyBorrowed
1775 | BorrowErrorKind::WriteWhileBorrowed => "conflicting borrow originates here".to_string(),
1776 BorrowErrorKind::ReferenceEscape
1777 | BorrowErrorKind::ReferenceStoredInArray
1778 | BorrowErrorKind::ReferenceStoredInObject
1779 | BorrowErrorKind::ReferenceStoredInEnum
1780 | BorrowErrorKind::ReferenceEscapeIntoClosure
1781 | BorrowErrorKind::ExclusiveRefAcrossTaskBoundary
1782 | BorrowErrorKind::SharedRefAcrossDetachedTask => {
1783 "reference originates here".to_string()
1784 }
1785 BorrowErrorKind::UseAfterMove => "value was moved here".to_string(),
1786 BorrowErrorKind::InconsistentReferenceReturn => {
1787 "borrowed origin on another return path originates here".to_string()
1788 }
1789 BorrowErrorKind::CallSiteAliasConflict => {
1790 "conflicting arguments originate here".to_string()
1791 }
1792 BorrowErrorKind::NonSendableAcrossTaskBoundary => {
1793 "non-sendable value originates here".to_string()
1794 }
1795 }
1796}
1797
1798pub fn validate_unused_imports(program: &Program, source: &str) -> Vec<Diagnostic> {
1827 use shape_runtime::visitor::{Visitor, walk_program};
1828 use std::collections::HashSet;
1829
1830 struct ImportInfo {
1831 local_name: String,
1832 span: Span,
1833 }
1834
1835 let mut imports: Vec<ImportInfo> = Vec::new();
1836 for item in &program.items {
1837 if let Item::Import(import_stmt, span) = item {
1838 if let shape_ast::ast::ImportItems::Named(specs) = &import_stmt.items {
1839 for spec in specs {
1840 if spec.is_annotation {
1841 continue;
1842 }
1843 let local = spec.alias.clone().unwrap_or_else(|| spec.name.clone());
1844 if local.starts_with('_') {
1845 continue;
1846 }
1847 imports.push(ImportInfo {
1848 local_name: local,
1849 span: *span,
1850 });
1851 }
1852 }
1853 }
1854 }
1855
1856 if imports.is_empty() {
1857 return Vec::new();
1858 }
1859
1860 struct IdentCollector {
1861 names: HashSet<String>,
1862 }
1863 impl Visitor for IdentCollector {
1864 fn visit_expr(&mut self, expr: &Expr) -> bool {
1865 match expr {
1866 Expr::Identifier(name, _) => {
1867 self.names.insert(name.clone());
1868 }
1869 Expr::FunctionCall { name, .. } => {
1870 self.names.insert(name.clone());
1871 }
1872 Expr::MethodCall { method, .. } => {
1873 self.names.insert(method.clone());
1874 }
1875 Expr::PropertyAccess { property, .. } => {
1876 self.names.insert(property.clone());
1877 }
1878 _ => {}
1879 }
1880 true
1881 }
1882 }
1883
1884 let mut collector = IdentCollector {
1885 names: HashSet::new(),
1886 };
1887 walk_program(&mut collector, program);
1888
1889 let mut diagnostics = Vec::new();
1890 for info in &imports {
1891 if collector.names.contains(&info.local_name) {
1892 continue;
1893 }
1894 if source_references_name_outside_span(source, &info.local_name, info.span) {
1895 continue;
1896 }
1897 let range = span_to_range(source, &info.span);
1898 let diag = Diagnostic {
1899 range,
1900 severity: Some(DiagnosticSeverity::WARNING),
1901 code: Some(NumberOrString::String("W0102".to_string())),
1902 code_description: code_description_for("W0102"),
1903 source: Some("shape".to_string()),
1904 message: format!(
1905 "unused import '{}': the imported symbol is never referenced in this file",
1906 info.local_name
1907 ),
1908 related_information: None,
1909 tags: Some(vec![DiagnosticTag::UNNECESSARY]),
1910 data: None,
1911 };
1912 diagnostics.push(diag);
1913 }
1914 diagnostics
1915}
1916
1917fn source_references_name_outside_span(source: &str, name: &str, exclude_span: Span) -> bool {
1923 if name.is_empty() {
1924 return false;
1925 }
1926 let bytes = source.as_bytes();
1927 let name_bytes = name.as_bytes();
1928 let mut i = 0usize;
1929 while i + name_bytes.len() <= bytes.len() {
1930 if i >= exclude_span.start && i < exclude_span.end {
1931 i = exclude_span.end;
1932 continue;
1933 }
1934 let prev_is_word = i > 0 && is_ident_byte(bytes[i - 1]);
1935 let next_idx = i + name_bytes.len();
1936 let next_is_word = next_idx < bytes.len() && is_ident_byte(bytes[next_idx]);
1937 if !prev_is_word && !next_is_word && &bytes[i..next_idx] == name_bytes {
1938 return true;
1939 }
1940 i += 1;
1941 }
1942 false
1943}
1944
1945fn is_ident_byte(b: u8) -> bool {
1946 b.is_ascii_alphanumeric() || b == b'_'
1947}
1948
1949#[cfg(test)]
1950mod tests {
1951 use super::*;
1952 use crate::util::offset_to_line_col;
1953
1954 #[test]
1955 fn test_location_to_range() {
1956 let loc = SourceLocation::new(5, 10);
1958 let range = location_to_range(Some(&loc));
1959
1960 assert_eq!(range.start.line, 4); assert_eq!(range.start.character, 9); let range = location_to_range(None);
1965 assert_eq!(range.start.line, 0);
1966 assert_eq!(range.start.character, 0);
1967 }
1968
1969 #[test]
1970 fn test_parse_error_diagnostic() {
1971 let error = ShapeError::ParseError {
1972 message: "Expected expression".to_string(),
1973 location: Some(SourceLocation::new(10, 5)),
1974 };
1975
1976 let diagnostics = error_to_diagnostic(&error);
1977 assert_eq!(diagnostics.len(), 1);
1978 assert_eq!(diagnostics[0].message, "Expected expression");
1979 assert_eq!(diagnostics[0].severity, Some(DiagnosticSeverity::ERROR));
1980 assert_eq!(diagnostics[0].source.as_deref(), Some("shape"));
1981 assert_eq!(diagnostics[0].range.start.line, 9); }
1983
1984 #[test]
1985 fn test_semantic_error_diagnostic() {
1986 let error = ShapeError::SemanticError {
1987 message: "Undefined variable 'x'".to_string(),
1988 location: Some(SourceLocation::new(3, 7)),
1989 };
1990
1991 let diagnostics = error_to_diagnostic(&error);
1992 assert_eq!(diagnostics.len(), 1);
1993 assert_eq!(diagnostics[0].message, "Undefined variable 'x'");
1994 assert_eq!(diagnostics[0].severity, Some(DiagnosticSeverity::ERROR));
1995 assert_eq!(diagnostics[0].source.as_deref(), Some("shape"));
1996 }
1997
1998 #[test]
1999 fn test_multi_error_flattening() {
2000 let multi_error = ShapeError::MultiError(vec![
2001 ShapeError::SemanticError {
2002 message: "Undefined variable 'x'".to_string(),
2003 location: Some(SourceLocation::new(1, 1)),
2004 },
2005 ShapeError::SemanticError {
2006 message: "Undefined variable 'y'".to_string(),
2007 location: Some(SourceLocation::new(2, 1)),
2008 },
2009 ]);
2010
2011 let diagnostics = error_to_diagnostic(&multi_error);
2012 assert_eq!(
2013 diagnostics.len(),
2014 2,
2015 "MultiError should flatten into 2 diagnostics"
2016 );
2017 assert!(diagnostics[0].message.contains("x"));
2018 assert!(diagnostics[1].message.contains("y"));
2019 }
2020
2021 #[test]
2022 fn test_multi_error_display() {
2023 let multi_error = ShapeError::MultiError(vec![
2024 ShapeError::SemanticError {
2025 message: "Error one".to_string(),
2026 location: None,
2027 },
2028 ShapeError::SemanticError {
2029 message: "Error two".to_string(),
2030 location: None,
2031 },
2032 ]);
2033
2034 let display = multi_error.to_string();
2035 assert!(
2036 display.contains("Error one"),
2037 "Display should contain first error"
2038 );
2039 assert!(
2040 display.contains("Error two"),
2041 "Display should contain second error"
2042 );
2043 }
2044
2045 #[test]
2046 fn test_offset_to_line_col() {
2047 let source = "line1\nline2\nline3";
2048
2049 assert_eq!(offset_to_line_col(source, 0), (0, 0));
2051
2052 assert_eq!(offset_to_line_col(source, 5), (0, 5));
2054
2055 assert_eq!(offset_to_line_col(source, 6), (1, 0));
2057
2058 assert_eq!(offset_to_line_col(source, 8), (1, 2));
2060 }
2061
2062 #[test]
2063 fn test_validate_annotations_with_defined() {
2064 use shape_ast::parser::parse_program;
2065
2066 let source = r#"
2068annotation my_ann() {
2069 on_define(fn, ctx) {
2070 ctx.registry("items").set(fn.name, fn)
2071 }
2072}
2073
2074@my_ann
2075function my_func(x) {
2076 return x + 1;
2077}
2078"#;
2079
2080 let program = parse_program(source).unwrap();
2081 let mut discovery = AnnotationDiscovery::new();
2082 discovery.discover_from_program(&program);
2083
2084 let diagnostics = validate_annotations(&program, &discovery, source);
2085
2086 assert!(
2088 diagnostics.is_empty(),
2089 "Expected no diagnostics for defined annotation, got: {:?}",
2090 diagnostics
2091 );
2092 }
2093
2094 #[test]
2095 fn test_validate_annotations_with_undefined() {
2096 use shape_ast::parser::parse_program;
2097
2098 let source = r#"
2099@undefined_annotation
2100function my_func() {
2101 return None;
2102}
2103"#;
2104
2105 let program = parse_program(source).unwrap();
2106 let mut discovery = AnnotationDiscovery::new();
2107 discovery.discover_from_program(&program);
2108
2109 let diagnostics = validate_annotations(&program, &discovery, source);
2110
2111 assert_eq!(
2113 diagnostics.len(),
2114 1,
2115 "Expected 1 diagnostic for undefined annotation"
2116 );
2117 assert!(diagnostics[0].message.contains("Undefined annotation"));
2118 assert!(diagnostics[0].message.contains("undefined_annotation"));
2119 }
2120
2121 #[test]
2122 fn test_validate_trait_bounds_missing_method() {
2123 use shape_ast::parser::parse_program;
2124
2125 let source = "trait Queryable {\n method filter(self, pred) -> any;\n method select(self, cols) -> any;\n}\nimpl Queryable for MyTable {\n method filter(pred) { self }\n}\n";
2127 let program = parse_program(source).unwrap();
2128 let diagnostics = validate_trait_bounds(&program, source);
2129
2130 assert_eq!(
2131 diagnostics.len(),
2132 1,
2133 "Should report 1 missing method error, got: {:?}",
2134 diagnostics
2135 );
2136 assert!(diagnostics[0].message.contains("Missing required method"));
2137 assert!(diagnostics[0].message.contains("select"));
2138 }
2139
2140 #[test]
2141 fn test_validate_trait_bounds_all_implemented() {
2142 use shape_ast::parser::parse_program;
2143
2144 let source = "trait Queryable {\n method filter(self, pred) -> any;\n method select(self, cols) -> any;\n}\nimpl Queryable for MyTable {\n method filter(pred) { self }\n method select(cols) { self }\n}\n";
2146 let program = parse_program(source).unwrap();
2147 let diagnostics = validate_trait_bounds(&program, source);
2148
2149 assert_eq!(
2150 diagnostics.len(),
2151 0,
2152 "Should report no errors when all methods implemented"
2153 );
2154 }
2155
2156 #[test]
2157 fn test_validate_trait_bounds_undefined_trait_in_bound() {
2158 use shape_ast::parser::parse_program;
2159
2160 let source = "fn foo<T: NonExistent>(x: T) {\n x\n}\n";
2161 let program = parse_program(source).unwrap();
2162 let diagnostics = validate_trait_bounds(&program, source);
2163
2164 assert_eq!(
2165 diagnostics.len(),
2166 1,
2167 "Should report undefined trait in bound"
2168 );
2169 assert!(diagnostics[0].message.contains("NonExistent"));
2170 assert!(diagnostics[0].message.contains("undefined trait"));
2171 }
2172
2173 #[test]
2174 fn test_validate_trait_bounds_valid_bound() {
2175 use shape_ast::parser::parse_program;
2176
2177 let source = "trait Comparable {\n method compare(self, other) -> number;\n}\nfn foo<T: Comparable>(x: T) {\n x\n}\n";
2179 let program = parse_program(source).unwrap();
2180 let diagnostics = validate_trait_bounds(&program, source);
2181
2182 assert_eq!(
2183 diagnostics.len(),
2184 0,
2185 "Should report no errors for valid trait bound"
2186 );
2187 }
2188
2189 #[test]
2190 fn test_validate_async_join_outside_async() {
2191 use shape_ast::parser::parse_program;
2192
2193 let source = "fn foo() {\n let x = await join all {\n 1,\n 2\n }\n}";
2194 let program = parse_program(source).unwrap();
2195 let diagnostics = validate_async_join(&program, source);
2196
2197 assert_eq!(
2198 diagnostics.len(),
2199 1,
2200 "Should report error for join outside async function"
2201 );
2202 assert!(
2203 diagnostics[0].message.contains("async"),
2204 "Error should mention async, got: {}",
2205 diagnostics[0].message
2206 );
2207 }
2208
2209 #[test]
2210 fn test_validate_async_join_inside_async() {
2211 use shape_ast::parser::parse_program;
2212
2213 let source = "async fn foo() {\n let x = await join all {\n 1,\n 2\n }\n}";
2214 let program = parse_program(source).unwrap();
2215 let diagnostics = validate_async_join(&program, source);
2216
2217 assert_eq!(
2218 diagnostics.len(),
2219 0,
2220 "Should not report error for join inside async function"
2221 );
2222 }
2223
2224 #[test]
2225 fn test_validate_async_join_top_level() {
2226 use shape_ast::parser::parse_program;
2227
2228 let source = "let x = await join race {\n 1,\n 2\n}";
2230 let program = parse_program(source).unwrap();
2231 let diagnostics = validate_async_join(&program, source);
2232
2233 assert_eq!(
2234 diagnostics.len(),
2235 1,
2236 "Should report error for join at top level"
2237 );
2238 }
2239
2240 #[test]
2241 fn test_validate_comptime_side_effects_with_print() {
2242 use shape_ast::parser::parse_program;
2243
2244 let source = "comptime {\n print(\"hello\")\n}";
2245 let program = parse_program(source).unwrap();
2246 let diagnostics = validate_comptime_side_effects(&program, source);
2247
2248 assert_eq!(
2249 diagnostics.len(),
2250 1,
2251 "Should warn about print() in comptime block"
2252 );
2253 assert!(
2254 diagnostics[0].message.contains("print"),
2255 "Warning should mention print"
2256 );
2257 assert_eq!(
2258 diagnostics[0].severity,
2259 Some(DiagnosticSeverity::WARNING),
2260 "Should be a warning, not an error"
2261 );
2262 }
2263
2264 #[test]
2265 fn test_validate_comptime_side_effects_clean() {
2266 use shape_ast::parser::parse_program;
2267
2268 let source = "comptime {\n let x = 42\n}";
2269 let program = parse_program(source).unwrap();
2270 let diagnostics = validate_comptime_side_effects(&program, source);
2271
2272 assert_eq!(
2273 diagnostics.len(),
2274 0,
2275 "Pure comptime block should have no warnings"
2276 );
2277 }
2278
2279 #[test]
2280 fn test_validate_comptime_side_effects_nested_in_function() {
2281 use shape_ast::parser::parse_program;
2282
2283 let source = "fn foo() {\n let x = comptime {\n print(\"debug\")\n }\n}\n";
2284 let program = parse_program(source).unwrap();
2285 let diagnostics = validate_comptime_side_effects(&program, source);
2286
2287 assert_eq!(
2288 diagnostics.len(),
2289 1,
2290 "Should warn about print() in nested comptime block, got: {:?}",
2291 diagnostics
2292 );
2293 }
2294
2295 #[test]
2296 fn test_validate_comptime_side_effects_fetch() {
2297 use shape_ast::parser::parse_program;
2298
2299 let source = "comptime {\n let data = fetch(\"http://example.com\")\n}\n";
2300 let program = parse_program(source).unwrap();
2301 let diagnostics = validate_comptime_side_effects(&program, source);
2302
2303 assert_eq!(
2304 diagnostics.len(),
2305 1,
2306 "Should warn about fetch() in comptime block"
2307 );
2308 assert!(diagnostics[0].message.contains("fetch"));
2309 }
2310
2311 #[test]
2312 fn test_validate_comptime_builtins_outside_comptime() {
2313 use shape_ast::parser::parse_program;
2314
2315 let source = r#"let x = implements("Point", "Display")"#;
2316 let program = parse_program(source).unwrap();
2317 let diagnostics = validate_comptime_builtins_context(&program, source);
2318
2319 assert_eq!(
2320 diagnostics.len(),
2321 1,
2322 "Should report error for comptime builtin outside comptime"
2323 );
2324 assert!(diagnostics[0].message.contains("comptime-only"));
2325 assert_eq!(diagnostics[0].severity, Some(DiagnosticSeverity::ERROR));
2326 }
2327
2328 #[test]
2329 fn test_validate_comptime_builtins_inside_comptime_ok() {
2330 use shape_ast::parser::parse_program;
2331
2332 let source = "comptime {\n let has = implements(\"Point\", \"Display\")\n}";
2333 let program = parse_program(source).unwrap();
2334 let diagnostics = validate_comptime_builtins_context(&program, source);
2335
2336 assert_eq!(
2337 diagnostics.len(),
2338 0,
2339 "comptime builtin inside comptime should be allowed"
2340 );
2341 }
2342
2343 #[test]
2344 fn test_validate_comptime_builtins_build_config_outside() {
2345 use shape_ast::parser::parse_program;
2346
2347 let source = "let cfg = build_config()";
2348 let program = parse_program(source).unwrap();
2349 let diagnostics = validate_comptime_builtins_context(&program, source);
2350
2351 assert_eq!(
2352 diagnostics.len(),
2353 1,
2354 "Should report error for build_config() outside comptime"
2355 );
2356 }
2357
2358 #[test]
2359 fn test_validate_async_let_outside_async() {
2360 use shape_ast::parser::parse_program;
2361
2362 let source = "fn foo() {\n async let x = fetch(\"url\")\n}";
2363 let program = parse_program(source).unwrap();
2364 let diagnostics = validate_async_structured_concurrency(&program, source);
2365
2366 assert_eq!(
2367 diagnostics.len(),
2368 1,
2369 "Should report error for async let outside async: {:?}",
2370 diagnostics
2371 );
2372 assert!(diagnostics[0].message.contains("async let"));
2373 assert_eq!(
2374 diagnostics[0].code,
2375 Some(NumberOrString::String("E0201".to_string()))
2376 );
2377 }
2378
2379 #[test]
2380 fn test_validate_async_let_inside_async() {
2381 use shape_ast::parser::parse_program;
2382
2383 let source = "async fn foo() {\n async let x = fetch(\"url\")\n}";
2384 let program = parse_program(source).unwrap();
2385 let diagnostics = validate_async_structured_concurrency(&program, source);
2386
2387 assert!(
2388 diagnostics.is_empty(),
2389 "Should have no errors for async let inside async fn: {:?}",
2390 diagnostics
2391 );
2392 }
2393
2394 #[test]
2395 fn test_validate_async_scope_outside_async() {
2396 use shape_ast::parser::parse_program;
2397
2398 let source = "fn foo() {\n let result = async scope { 42 }\n}";
2399 let program = parse_program(source).unwrap();
2400 let diagnostics = validate_async_structured_concurrency(&program, source);
2401
2402 assert_eq!(
2403 diagnostics.len(),
2404 1,
2405 "Should report error for async scope outside async: {:?}",
2406 diagnostics
2407 );
2408 assert!(diagnostics[0].message.contains("async scope"));
2409 assert_eq!(
2410 diagnostics[0].code,
2411 Some(NumberOrString::String("E0202".to_string()))
2412 );
2413 }
2414
2415 #[test]
2416 fn test_validate_async_scope_inside_async() {
2417 use shape_ast::parser::parse_program;
2418
2419 let source = "async fn foo() {\n let result = async scope { 42 }\n}";
2420 let program = parse_program(source).unwrap();
2421 let diagnostics = validate_async_structured_concurrency(&program, source);
2422
2423 assert!(
2424 diagnostics.is_empty(),
2425 "Should have no errors for async scope inside async fn: {:?}",
2426 diagnostics
2427 );
2428 }
2429
2430 #[test]
2431 fn test_validate_for_await_outside_async() {
2432 use shape_ast::parser::parse_program;
2433
2434 let source = "fn foo() {\n for await x in stream {\n x\n }\n}";
2435 let program = parse_program(source).unwrap();
2436 let diagnostics = validate_async_structured_concurrency(&program, source);
2437
2438 assert_eq!(
2439 diagnostics.len(),
2440 1,
2441 "Should report error for for-await outside async: {:?}",
2442 diagnostics
2443 );
2444 assert!(diagnostics[0].message.contains("for await"));
2445 assert_eq!(
2446 diagnostics[0].code,
2447 Some(NumberOrString::String("E0203".to_string()))
2448 );
2449 }
2450
2451 #[test]
2452 fn test_validate_for_await_inside_async() {
2453 use shape_ast::parser::parse_program;
2454
2455 let source = "async fn foo() {\n for await x in stream {\n x\n }\n}";
2456 let program = parse_program(source).unwrap();
2457 let diagnostics = validate_async_structured_concurrency(&program, source);
2458
2459 assert!(
2460 diagnostics.is_empty(),
2461 "Should have no errors for for-await inside async fn: {:?}",
2462 diagnostics
2463 );
2464 }
2465
2466 #[test]
2467 fn test_validate_interpolation_format_specs_ok() {
2468 use shape_ast::parser::parse_program;
2469
2470 let source = r#"let s = f"value={price:fixed(2)}""#;
2471 let program = parse_program(source).unwrap();
2472 let diagnostics = validate_interpolation_format_specs(&program, source);
2473 assert!(
2474 diagnostics.is_empty(),
2475 "unexpected diagnostics: {:?}",
2476 diagnostics
2477 );
2478 }
2479
2480 #[test]
2481 fn test_validate_interpolation_format_specs_reports_invalid_table_key() {
2482 use shape_ast::parser::parse_program;
2483
2484 let source = r#"let s = f"{rows:table(foo=1)}""#;
2485 let program = parse_program(source).unwrap();
2486 let diagnostics = validate_interpolation_format_specs(&program, source);
2487 assert_eq!(diagnostics.len(), 1, "expected a single diagnostic");
2488 assert!(
2489 diagnostics[0].message.contains("Unknown table format key"),
2490 "unexpected diagnostic message: {}",
2491 diagnostics[0].message
2492 );
2493 assert_eq!(
2494 diagnostics[0].code,
2495 Some(NumberOrString::String("E0300".to_string()))
2496 );
2497 assert_eq!(
2498 diagnostics[0].range.start.line, 0,
2499 "diagnostic should point to formatted string line"
2500 );
2501 }
2502
2503 #[test]
2504 fn test_validate_color_rgb_out_of_range() {
2505 use shape_ast::parser::parse_program;
2506
2507 let source = "let c = Color.rgb(300, 100, 256)";
2508 let program = parse_program(source).unwrap();
2509 let diagnostics = validate_color_rgb_range(&program, source);
2510
2511 assert_eq!(
2512 diagnostics.len(),
2513 2,
2514 "expected 2 diagnostics for out-of-range RGB values (300 and 256), got: {:?}",
2515 diagnostics
2516 );
2517 assert!(diagnostics[0].message.contains("300"));
2518 assert!(diagnostics[1].message.contains("256"));
2519 assert_eq!(
2520 diagnostics[0].code,
2521 Some(NumberOrString::String("W0310".to_string()))
2522 );
2523 assert_eq!(diagnostics[0].severity, Some(DiagnosticSeverity::WARNING));
2524 }
2525
2526 #[test]
2527 fn test_validate_color_rgb_valid_range_ok() {
2528 use shape_ast::parser::parse_program;
2529
2530 let source = "let c = Color.rgb(255, 128, 0)";
2531 let program = parse_program(source).unwrap();
2532 let diagnostics = validate_color_rgb_range(&program, source);
2533
2534 assert!(
2535 diagnostics.is_empty(),
2536 "valid Color.rgb should produce no diagnostics: {:?}",
2537 diagnostics
2538 );
2539 }
2540
2541 #[test]
2542 fn test_borrow_analysis_to_diagnostics_empty() {
2543 let analysis = shape_vm::mir::analysis::BorrowAnalysis::empty();
2544 let uri = Uri::from_file_path("/tmp/test.shape").unwrap();
2545 let diagnostics = borrow_analysis_to_diagnostics(&analysis, "", &uri);
2546 assert!(
2547 diagnostics.is_empty(),
2548 "Empty analysis should produce no diagnostics"
2549 );
2550 }
2551
2552 #[test]
2553 fn test_borrow_analysis_to_diagnostics_with_error() {
2554 use shape_vm::mir::analysis::*;
2555 use shape_vm::mir::types::*;
2556
2557 let mut analysis = BorrowAnalysis::empty();
2558 analysis.errors.push(BorrowError {
2559 kind: BorrowErrorKind::ConflictExclusiveExclusive,
2560 span: Span { start: 10, end: 20 },
2561 conflicting_loan: LoanId(0),
2562 loan_span: Span { start: 0, end: 5 },
2563 last_use_span: Some(Span { start: 25, end: 30 }),
2564 repairs: Vec::new(),
2565 });
2566
2567 let source = "let mut x = 10\nlet m1 = &mut x\nlet m2 = &mut x\nprint(m1)\nprint(m2)";
2568 let uri = Uri::from_file_path("/tmp/test.shape").unwrap();
2569 let diagnostics = borrow_analysis_to_diagnostics(&analysis, source, &uri);
2570
2571 assert_eq!(diagnostics.len(), 1, "Should produce one diagnostic");
2572 let diag = &diagnostics[0];
2573 assert_eq!(diag.severity, Some(DiagnosticSeverity::ERROR));
2574 assert_eq!(
2575 diag.code,
2576 Some(NumberOrString::String("B0001".to_string()))
2577 );
2578 assert_eq!(diag.source.as_deref(), Some("shape-borrow"));
2579 assert!(
2580 diag.message.contains("cannot mutably borrow"),
2581 "Message should describe the conflict: {}",
2582 diag.message
2583 );
2584 let related = diag.related_information.as_ref().unwrap();
2586 assert_eq!(
2587 related.len(),
2588 2,
2589 "Should have loan origin + last use entries"
2590 );
2591 assert!(related[0].message.contains("conflicting borrow"));
2592 assert!(related[1].message.contains("still needed"));
2593 }
2594
2595 #[test]
2596 fn test_borrow_analysis_to_diagnostics_mutability_error() {
2597 use shape_vm::mir::analysis::*;
2598
2599 let mut analysis = BorrowAnalysis::empty();
2600 analysis.mutability_errors.push(MutabilityError {
2601 span: Span { start: 10, end: 15 },
2602 variable_name: "x".to_string(),
2603 declaration_span: Span { start: 0, end: 5 },
2604 is_explicit_let: true,
2605 is_const: false,
2606 });
2607
2608 let source = "let x = 42\nx = 100\n";
2609 let uri = Uri::from_file_path("/tmp/test.shape").unwrap();
2610 let diagnostics = borrow_analysis_to_diagnostics(&analysis, source, &uri);
2611
2612 assert_eq!(diagnostics.len(), 1);
2613 let diag = &diagnostics[0];
2614 assert!(diag.message.contains("cannot assign to let binding"));
2615 assert_eq!(
2616 diag.code,
2617 Some(NumberOrString::String("E0384".to_string()))
2618 );
2619 let related = diag.related_information.as_ref().unwrap();
2620 assert_eq!(related.len(), 1);
2621 assert!(related[0].message.contains("declared here"));
2622 }
2623
2624 #[test]
2626 fn test_code_description_for_known_code() {
2627 let result = code_description_for("E0001");
2628 assert!(result.is_some(), "expected code description for E0001");
2629 }
2630
2631 #[test]
2632 fn test_code_description_for_empty_returns_none() {
2633 let result = code_description_for("");
2634 assert!(result.is_none());
2635 }
2636
2637 #[test]
2638 fn test_diagnostic_tags_for_unused_import_by_code() {
2639 let tags = diagnostic_tags_for(Some("W0102"), "unused import 'foo'");
2640 assert!(tags.is_some());
2641 let tags = tags.unwrap();
2642 assert!(tags.contains(&DiagnosticTag::UNNECESSARY));
2643 }
2644
2645 #[test]
2646 fn test_diagnostic_tags_for_unused_variable_by_message() {
2647 let tags = diagnostic_tags_for(None, "unused variable 'x'");
2648 assert!(tags.is_some());
2649 let tags = tags.unwrap();
2650 assert!(tags.contains(&DiagnosticTag::UNNECESSARY));
2651 }
2652
2653 #[test]
2654 fn test_diagnostic_tags_for_other_returns_none() {
2655 let tags = diagnostic_tags_for(Some("E0001"), "Expected expression");
2656 assert!(tags.is_none(), "non-unused diagnostics should have no tags");
2657 }
2658
2659 #[test]
2660 fn test_enrich_diagnostics_with_code_metadata_backfills_description() {
2661 let mut diagnostics = vec![Diagnostic {
2662 range: Range::default(),
2663 severity: Some(DiagnosticSeverity::WARNING),
2664 code: Some(NumberOrString::String("W0102".to_string())),
2665 code_description: None,
2666 tags: None,
2667 source: Some("shape".to_string()),
2668 message: "unused import 'x'".to_string(),
2669 ..Default::default()
2670 }];
2671 enrich_diagnostics_with_code_metadata(&mut diagnostics);
2672 assert!(diagnostics[0].code_description.is_some());
2673 assert!(diagnostics[0].tags.is_some());
2674 let tags = diagnostics[0].tags.as_ref().unwrap();
2675 assert!(tags.contains(&DiagnosticTag::UNNECESSARY));
2676 }
2677
2678 #[test]
2679 fn test_enrich_diagnostics_preserves_existing_metadata() {
2680 use std::str::FromStr;
2681 let existing_desc = CodeDescription {
2682 href: Uri::from_str("https://example.com/x").unwrap(),
2683 };
2684 let mut diagnostics = vec![Diagnostic {
2685 range: Range::default(),
2686 severity: Some(DiagnosticSeverity::ERROR),
2687 code: Some(NumberOrString::String("E0001".to_string())),
2688 code_description: Some(existing_desc.clone()),
2689 tags: None,
2690 source: Some("shape".to_string()),
2691 message: "Existing".to_string(),
2692 ..Default::default()
2693 }];
2694 enrich_diagnostics_with_code_metadata(&mut diagnostics);
2695 assert!(diagnostics[0].code_description.is_some());
2697 }
2698
2699 #[test]
2700 fn test_parse_error_diagnostic_no_location_defaults_to_origin() {
2701 let error = ShapeError::ParseError {
2702 message: "no loc".to_string(),
2703 location: None,
2704 };
2705 let diagnostics = error_to_diagnostic(&error);
2706 assert_eq!(diagnostics.len(), 1);
2707 assert_eq!(diagnostics[0].range.start.line, 0);
2708 assert_eq!(diagnostics[0].range.start.character, 0);
2709 }
2710
2711 #[test]
2712 fn test_validate_unused_imports_finds_unused() {
2713 let source = "from std::core::math use { abs }\nlet x = 5\n";
2714 let program = shape_ast::parser::parse_program(source).unwrap();
2715 let diagnostics = validate_unused_imports(&program, source);
2716 assert!(
2718 diagnostics
2719 .iter()
2720 .any(|d| d.message.contains("unused") || d.message.contains("abs")),
2721 "expected unused-import diagnostic; got: {:?}",
2722 diagnostics.iter().map(|d| &d.message).collect::<Vec<_>>()
2723 );
2724 }
2725
2726 #[test]
2727 fn test_validate_unused_imports_clean_when_used() {
2728 let source = "from std::core::math use { abs }\nlet x = abs(-5)\n";
2729 let program = shape_ast::parser::parse_program(source).unwrap();
2730 let diagnostics = validate_unused_imports(&program, source);
2731 assert!(
2732 diagnostics.is_empty(),
2733 "expected no diagnostics when import is used; got: {:?}",
2734 diagnostics.iter().map(|d| &d.message).collect::<Vec<_>>()
2735 );
2736 }
2737}