1use adam_lang::{AdamAstParser, TypeRegistry, check_sheet};
7use cel_parser::{CELError, SourceSpan};
8use lsp_types::{Diagnostic, DiagnosticSeverity, Position, Range};
9
10pub fn diagnostics_for_source(source: &str) -> Vec<Diagnostic> {
32 let mut parser = AdamAstParser::new();
33 let sheet = match parser.parse_str(source) {
34 Ok(sheet) => sheet,
35 Err(e) => return vec![to_diagnostic(e.into())],
36 };
37 let mut types = TypeRegistry::new();
38 cel_euclid::register_adam_types(&mut types);
39 let mut lookup = cel_parser::OpLookup::new();
40 cel_std::install(&mut lookup);
41 cel_euclid::install(&mut lookup);
42 let type_errors = check_sheet(&sheet, &types, &lookup);
43 sheet
44 .errors
45 .into_iter()
46 .chain(type_errors)
47 .map(|e| to_diagnostic(e.into()))
48 .collect()
49}
50
51fn to_diagnostic(error: CELError) -> Diagnostic {
53 Diagnostic {
54 range: to_range(error.span()),
55 severity: Some(DiagnosticSeverity::ERROR),
56 message: error.message().to_string(),
57 ..Default::default()
58 }
59}
60
61fn to_range(span: SourceSpan) -> Range {
68 Range {
69 start: Position {
70 line: (span.start.line - 1) as u32,
71 character: span.start.column as u32,
72 },
73 end: Position {
74 line: (span.end.line - 1) as u32,
75 character: span.end.column as u32,
76 },
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 #[test]
86 fn lsp_geometry_fields_and_members_have_matching_diagnostics() {
87 let valid = "sheet s { out x := (|p: Point2D| p.x)(Point2D { y: 2.0, x: 1.0 }); out n := min(3.0, 4.0); }";
88 assert!(
89 diagnostics_for_source(valid).is_empty(),
90 "{:?}",
91 diagnostics_for_source(valid)
92 );
93 let invalid = "sheet s { out p := Point2D { x: true, y: 1.0 }; }";
94 let diagnostics = diagnostics_for_source(invalid);
95 assert_eq!(diagnostics.len(), 1, "{diagnostics:?}");
96 let invalid = "sheet s { out p := Point2D { x: 1.0, y: 1.0 }.translate(1.0); }";
97 assert!(!diagnostics_for_source(invalid).is_empty());
98 for actual in [
99 include_str!("../../cel-euclid/examples/crop_straighten.adm2"),
100 include_str!("../../cel-euclid/examples/pan_zoom.adm2"),
101 ] {
102 let diagnostics = diagnostics_for_source(actual);
103 assert!(diagnostics.is_empty(), "{diagnostics:?}");
104 }
105 }
106
107 #[test]
109 fn unknown_record_and_child_errors_reach_lsp_diagnostics() {
110 let diagnostics =
111 diagnostics_for_source("sheet s {\n out value := Missing { x: true + 1 }.x;\n}");
112 assert_eq!(diagnostics.len(), 2);
113 let missing = diagnostics
114 .iter()
115 .find(|d| d.message.contains("Missing"))
116 .unwrap();
117 assert_eq!(missing.range.start, Position::new(1, 17));
118 assert_eq!(missing.range.end, Position::new(1, 24));
119 assert!(diagnostics.iter().any(|d| d.message.contains("operand")));
120 }
121
122 #[test]
123 fn clean_sheet_has_no_diagnostics() {
124 assert!(diagnostics_for_source("sheet s { cell x: i32 = 1; }").is_empty());
125 }
126
127 #[test]
128 fn type_mismatched_cell_initializer_is_a_diagnostic() {
129 let diags = diagnostics_for_source("sheet s { cell x: i32 = 1.0; }");
132 assert_eq!(diags.len(), 1);
133 assert_eq!(diags[0].severity, Some(DiagnosticSeverity::ERROR));
134 }
135
136 #[test]
137 fn recovered_syntax_error_is_a_diagnostic() {
138 assert_eq!(
141 diagnostics_for_source("sheet s { cell x unknown_syntax }").len(),
142 1
143 );
144 }
145
146 #[test]
147 fn unrecoverable_parse_failure_is_a_single_diagnostic() {
148 assert_eq!(diagnostics_for_source("not a sheet at all").len(), 1);
151 }
152
153 #[test]
154 fn filter_clause_with_matching_types_has_no_diagnostics() {
155 assert!(diagnostics_for_source("sheet s { cell a: i32 = 1 filter _; }").is_empty());
156 }
157
158 #[test]
159 fn filter_clause_without_underscore_is_a_diagnostic() {
160 let diags = diagnostics_for_source("sheet s { cell a: i32 = 1 filter 1; }");
161 assert_eq!(diags.len(), 1);
162 }
163}