Skip to main content

shape_lsp/
diagnostics.rs

1//! Diagnostics conversion from Shape errors to LSP diagnostics
2
3use 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
16/// LSP Error Renderer - converts structured errors to LSP Diagnostics
17pub struct LspErrorRenderer {
18    /// URI of the document being validated
19    uri: Uri,
20}
21
22impl LspErrorRenderer {
23    pub fn new(uri: Uri) -> Self {
24        Self { uri }
25    }
26
27    /// Convert a structured error to an LSP diagnostic
28    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        // Convert location to range
37        let range = self.structured_location_to_range(error);
38
39        // Build the main message
40        let message = format_structured_message(&error.kind, &error.suggestions);
41
42        // Build related information from related locations
43        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            // Default to a reasonable span
95            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
136/// Format the error message with suggestions for LSP display
137fn 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    // Add suggestions as hints in the message
150    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
159/// Convert Shape errors to LSP diagnostics
160pub fn error_to_diagnostic(error: &ShapeError) -> Vec<Diagnostic> {
161    error_to_diagnostic_with_uri(error, None)
162}
163
164/// Convert Shape errors to LSP diagnostics with optional URI for structured errors
165pub fn error_to_diagnostic_with_uri(error: &ShapeError, uri: Option<Uri>) -> Vec<Diagnostic> {
166    // Get error code if available
167    let error_code = error.error_code().map(|c| c.as_str());
168
169    match error {
170        ShapeError::StructuredParse(structured) => {
171            // Use the LSP renderer for structured errors if we have a URI
172            if let Some(uri) = uri {
173                let renderer = LspErrorRenderer::new(uri);
174                renderer.render(structured)
175            } else {
176                // Fallback: convert to a basic diagnostic
177                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            // Type errors may have location information
224            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            // Flatten MultiError into individual diagnostics
289            errors
290                .iter()
291                .flat_map(|e| error_to_diagnostic_with_uri(e, uri.clone()))
292                .collect()
293        }
294
295        // Other error types get generic diagnostic
296        _ => vec![create_diagnostic_with_code(
297            &error.to_string(),
298            None,
299            DiagnosticSeverity::ERROR,
300            "shape",
301            error_code,
302        )],
303    }
304}
305
306/// Create a diagnostic from error information
307fn 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    // Build message with hints
316    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    // Build related information from notes and cross-file locations
333    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 the error originated in a different file, add a related info entry
341        // pointing to the actual source location so the user can navigate there
342        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, // Error code is set per-error type
379        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
388/// Create a diagnostic with error code
389fn 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
403/// Convert ErrorNotes to LSP DiagnosticRelatedInformation
404fn notes_to_related_info(
405    notes: &[ErrorNote],
406    default_file: Option<&str>,
407) -> Vec<DiagnosticRelatedInformation> {
408    notes
409        .iter()
410        .filter_map(|note| {
411            // Try to create a valid URI for the location
412            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
447/// Convert SourceLocation to LSP Range
448fn location_to_range(location: Option<&SourceLocation>) -> Range {
449    // For missing or synthetic locations, highlight the full first line
450    // so the diagnostic is visible rather than a zero-width invisible marker
451    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        // Shape uses 1-based line/column, LSP uses 0-based
468        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        // If we have length, calculate end position
477        let end = if let Some(len) = loc.length {
478            Position {
479                line,
480                character: column + len as u32,
481            }
482        } else {
483            // Default to highlighting the whole line if no length
484            Position {
485                line,
486                character: column + 100, // Reasonable default
487            }
488        };
489
490        Range { start, end }
491    } else {
492        full_first_line
493    }
494}
495
496/// Validate annotations in a program and return diagnostics for any issues
497///
498/// Checks:
499/// - Whether annotations are defined (in local file or imports)
500/// - Whether annotation arguments are valid
501pub 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
525/// Validate a single annotation usage
526fn 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    // Check if annotation is defined
535    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        // Calculate position from span
553        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    // Check argument count if annotation is defined
569    if let Some(ann_info) = annotation_discovery.get(name) {
570        let expected = ann_info.params.len();
571        let actual = annotation.args.len();
572
573        // Allow 0 args for optional-arg annotations, or exact match
574        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
594/// Validate async join usage: `await join` must be inside an async function.
595///
596/// Walks the AST to find `Expr::Join` nodes that appear outside async function bodies.
597pub 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
654/// Validate structured concurrency constructs (`async let`, `async scope`, `for await`)
655/// are only used inside async function bodies.
656pub 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
771/// Validate formatted interpolation specs in `f"..."` string literals.
772///
773/// This catches invalid typed specs (unknown keys, invalid enum values, malformed
774/// spec calls) in the editor without waiting for a full compile pass.
775pub 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
818/// Validate type alias overrides: only comptime fields can be overridden.
819///
820/// Checks `type EUR = Currency { symbol: "EUR" }` and reports an error if `symbol`
821/// is not a comptime field of `Currency`.
822pub fn validate_comptime_overrides(program: &Program, source: &str) -> Vec<Diagnostic> {
823    use std::collections::HashMap;
824
825    let mut diagnostics = Vec::new();
826
827    // Collect struct type definitions with their comptime field names
828    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    // Check type alias overrides
842    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                // Get the base type name from the type annotation
846                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                            // Find the override position within the span for better error location
855                            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
881/// Warn when a comptime block contains side-effecting expressions.
882///
883/// Comptime blocks run at compile time, so side effects (print, I/O) are
884/// unexpected and likely mistakes.
885pub fn validate_comptime_side_effects(program: &Program, source: &str) -> Vec<Diagnostic> {
886    let mut diagnostics = Vec::new();
887
888    // Walk top-level items for Item::Comptime and expressions containing Expr::Comptime
889    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
903/// Known side-effecting function names that should not appear in comptime blocks.
904const 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            // Recurse into args
977            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            // Nested comptime — still check
986            check_stmts_for_side_effects(stmts, span, source, diagnostics);
987        }
988        _ => {}
989    }
990}
991
992/// Walk an item's expressions looking for Expr::Comptime blocks to validate.
993fn visit_item_exprs(item: &Item, source: &str, diagnostics: &mut Vec<Diagnostic>) {
994    // We only need to find Expr::Comptime inside function bodies, variable decls, etc.
995    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
1087/// Diagnose comptime-only builtins called outside a `comptime { }` block.
1088pub 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                // Inside comptime — everything is allowed
1095            }
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            // Inside comptime block — builtins are allowed
1170            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
1231/// Validate trait bound satisfaction in impl blocks and function type parameters.
1232///
1233/// Checks:
1234/// - Functions with bounded type params: `fn foo<T: Comparable>(x: T)` — the trait must exist
1235/// - Impl blocks: all required trait methods are implemented
1236pub fn validate_trait_bounds(program: &Program, source: &str) -> Vec<Diagnostic> {
1237    let mut diagnostics = Vec::new();
1238
1239    // Collect known trait definitions and their required method names
1240    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    // Check function type parameter trait bounds reference existing traits
1261    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    // Check impl blocks: all required methods are implemented
1290    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
1330/// Validate content string usage and Content API calls.
1331///
1332/// - Error on empty interpolation `{}` in c-strings
1333/// - Warn on `Color.rgb()` with values outside 0-255
1334pub 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                    // Check for empty interpolation `{}`
1347                    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                // Check Color.rgb(r, g, b) for out-of-range values
1363                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
1420/// Validate that foreign function parameters and return types are explicitly annotated.
1421///
1422/// Foreign function bodies are opaque — the type system cannot infer types from them.
1423/// Uses `ForeignFunctionDef::validate_type_annotations()` (shared with the compiler).
1424pub 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
1463// ─── MIR borrow analysis → LSP diagnostics ─────────────────────────────────
1464//
1465// The compiler already converts MIR `BorrowError`/`MutabilityError` into
1466// `ShapeError::SemanticError` which flows through `error_to_diagnostic`.
1467// This module provides an *alternative* path that produces richer LSP
1468// diagnostics directly from the structured analysis data (proper error
1469// codes, `DiagnosticRelatedInformation` for borrow origins, etc.).
1470//
1471// Usage: call `borrow_analysis_to_diagnostics` after a successful or
1472// recovered compilation to surface borrow warnings that the compiler's
1473// `RecoverAll` mode collected but did not promote to hard errors.
1474
1475/// Convert structured MIR borrow errors into LSP diagnostics.
1476///
1477/// This produces higher-fidelity diagnostics than the `error_to_diagnostic`
1478/// path because it has direct access to `BorrowError` fields:
1479/// - Sets `code` to the unified `BorrowErrorCode` (`B0001`..`B0007`).
1480/// - Attaches `DiagnosticRelatedInformation` for the loan origin span
1481///   and the last-use span (if available).
1482pub 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        // Build related-information entries.
1497        let mut related = Vec::new();
1498
1499        // 1. Where the conflicting loan was created.
1500        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        // 2. Where the loan is still needed (last use).
1510        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        // Build hint text from repair suggestions.
1522        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
1589/// Human-readable message for a borrow error kind (with code prefix).
1590fn 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
1645/// Hint text for a borrow error kind.
1646fn 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
1689/// Note text for the related-information entry pointing at the loan origin.
1690fn 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        // Test with location
1727        let loc = SourceLocation::new(5, 10);
1728        let range = location_to_range(Some(&loc));
1729
1730        assert_eq!(range.start.line, 4); // 0-based
1731        assert_eq!(range.start.character, 9); // 0-based
1732
1733        // Test without location
1734        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); // 0-based
1752    }
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        // Start of file
1820        assert_eq!(offset_to_line_col(source, 0), (0, 0));
1821
1822        // End of first line
1823        assert_eq!(offset_to_line_col(source, 5), (0, 5));
1824
1825        // Start of second line
1826        assert_eq!(offset_to_line_col(source, 6), (1, 0));
1827
1828        // Middle of second line
1829        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        // Define @my_ann locally, then use it on a function
1837        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        // @my_ann is defined locally, so no errors
1857        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        // @undefined_annotation is not defined anywhere
1882        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        // Join at top level (not inside any function) should be an error
1996        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        // Should have related information (loan origin + last use)
2392        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}