1use std::path::Path;
32
33use serde::Serialize;
34
35use crate::cst;
36use crate::diag::{OpyError, Position, Span};
37use crate::hir;
38use crate::hir::types::{
39 Declaration, Define, Expr as HirExpr, RuleEntry, SourceFile, Stmt as HirStmt,
40};
41use crate::preprocess::{FileRecord, PreprocessOutcome, PreprocessWarning};
42
43#[derive(Debug, Clone)]
50pub struct CheckOutcome {
51 pub diagnostics: Vec<Diagnostic>,
52 pub model: Option<SemanticModel>,
53 pub files: Vec<FileRecord>,
54 pub post_compile_hook: Option<crate::preprocess::PostCompileHook>,
63}
64
65impl CheckOutcome {
66 pub fn is_clean(&self) -> bool {
68 self.diagnostics
69 .iter()
70 .all(|diagnostic| diagnostic.severity != DiagnosticSeverity::Error)
71 }
72}
73
74pub fn check(source: &str, main_path: &str, root: &Path) -> CheckOutcome {
78 check_with_overlay(source, main_path, root, &std::collections::BTreeMap::new())
79}
80
81pub fn check_with_overlay(
84 source: &str,
85 main_path: &str,
86 root: &Path,
87 overlay: &std::collections::BTreeMap<String, String>,
88) -> CheckOutcome {
89 let PreprocessOutcome {
90 result,
91 files,
92 warnings,
93 } = crate::preprocess::preprocess_with_overlay_outcome(source, main_path, root, overlay);
94 let preprocessed = match result {
95 Ok((preprocessed, _)) => preprocessed,
96 Err(error) => {
97 let mut diagnostics = warnings
98 .iter()
99 .map(|warning| Diagnostic::from_warning(warning, &files))
100 .collect::<Vec<_>>();
101 diagnostics.push(Diagnostic::from_error(error, &files));
102 return CheckOutcome {
103 diagnostics,
104 model: None,
105 files,
106 post_compile_hook: None,
107 };
108 }
109 };
110 let parsed = crate::parser::parse_with_options(
111 &preprocessed.tokens,
112 preprocessed.preprocessing.allow_macro_redeclaration,
113 );
114 let Some(mut program) = parsed.program else {
115 let mut diagnostics = preprocessed
118 .warnings
119 .iter()
120 .map(|warning| Diagnostic::from_warning(warning, &files))
121 .collect::<Vec<_>>();
122 diagnostics.extend(
123 parsed
124 .errors
125 .iter()
126 .map(|error| Diagnostic::from_error(error.clone(), &files)),
127 );
128 return CheckOutcome {
129 diagnostics,
130 model: None,
131 files,
132 post_compile_hook: None,
133 };
134 };
135 if let Some(block) = &preprocessed.settings {
138 match crate::settings::parse_block(block) {
139 Ok(parsed_settings) => program.settings = Some(parsed_settings),
140 Err(error) => {
141 let mut diagnostics = preprocessed
142 .warnings
143 .iter()
144 .map(|warning| Diagnostic::from_warning(warning, &files))
145 .collect::<Vec<_>>();
146 diagnostics.push(Diagnostic::from_error(error, &files));
147 return CheckOutcome {
148 diagnostics,
149 model: None,
150 files,
151 post_compile_hook: None,
152 };
153 }
154 }
155 }
156 let defines = preprocessed
157 .defines
158 .iter()
159 .map(|define| Define {
160 name: define.name.clone(),
161 is_function: define.is_function,
162 is_member: define.is_member,
163 span: define.span.map(Into::into),
164 })
165 .collect();
166 let hir_files = files
167 .iter()
168 .map(|file| hir::types::SourceFile {
169 id: file.id,
170 path: file.path.clone(),
171 })
172 .collect();
173 match crate::lower::lower_with_preprocessing(
174 &program,
175 hir_files,
176 defines,
177 &preprocessed.preprocessing,
178 ) {
179 Ok(mut hir) => {
180 hir.preprocessing = preprocessed.preprocessing;
181 CheckOutcome {
182 diagnostics: preprocessed
183 .warnings
184 .iter()
185 .map(|warning| Diagnostic::from_warning(warning, &files))
186 .collect(),
187 model: Some(SemanticModel::build(hir, &program)),
188 files,
189 post_compile_hook: preprocessed.post_compile_hook,
194 }
195 }
196 Err(error) => {
197 let mut diagnostics = preprocessed
198 .warnings
199 .iter()
200 .map(|warning| Diagnostic::from_warning(warning, &files))
201 .collect::<Vec<_>>();
202 diagnostics.push(Diagnostic::from_error(error, &files));
203 CheckOutcome {
204 diagnostics,
205 model: None,
206 files,
207 post_compile_hook: None,
208 }
209 }
210 }
211}
212
213#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
219pub struct Diagnostic {
220 pub severity: DiagnosticSeverity,
221 pub code: String,
222 pub message: String,
223 pub span: Option<SourceLocation>,
224}
225
226impl Diagnostic {
227 fn from_warning(warning: &PreprocessWarning, files: &[FileRecord]) -> Diagnostic {
228 Diagnostic {
229 severity: DiagnosticSeverity::Warning,
230 code: warning.code.clone(),
231 message: warning.message.clone(),
232 span: resolve_record_span(warning.span, files),
233 }
234 }
235
236 fn from_error(error: OpyError, files: &[FileRecord]) -> Diagnostic {
237 Diagnostic {
238 severity: DiagnosticSeverity::Error,
239 code: error.code,
240 message: error.message,
241 span: error.span.and_then(|span| resolve_record_span(span, files)),
242 }
243 }
244}
245
246#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
248#[serde(rename_all = "camelCase")]
249pub enum DiagnosticSeverity {
250 Error,
251 Warning,
252}
253
254impl DiagnosticSeverity {
255 pub fn as_str(&self) -> &'static str {
256 match self {
257 DiagnosticSeverity::Error => "error",
258 DiagnosticSeverity::Warning => "warning",
259 }
260 }
261}
262
263#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
266pub struct SourceLocation {
267 pub file_id: u32,
268 pub path: String,
269 pub start: Position,
270 pub end: Position,
271}
272
273impl SourceLocation {
274 pub fn to_span(&self) -> Span {
276 Span::new(self.file_id, self.start, self.end)
277 }
278}
279
280fn resolve_span(span: Span, files: &[SourceFile]) -> Option<SourceLocation> {
281 let path = files.iter().find(|file| file.id == span.file)?.path.clone();
282 Some(SourceLocation {
283 file_id: span.file,
284 path,
285 start: span.start,
286 end: span.end,
287 })
288}
289
290fn resolve_record_span(span: Span, files: &[FileRecord]) -> Option<SourceLocation> {
293 let path = files.iter().find(|file| file.id == span.file)?.path.clone();
294 Some(SourceLocation {
295 file_id: span.file,
296 path,
297 start: span.start,
298 end: span.end,
299 })
300}
301
302fn to_frontend_span(span: hir::types::Span) -> Span {
306 Span::new(
307 span.file,
308 Position::new(span.start.line, span.start.col),
309 Position::new(span.end.line, span.end.col),
310 )
311}
312
313#[derive(Debug, Clone, Serialize)]
320pub struct SemanticModel {
321 pub hir: hir::Program,
322 pub enums: Vec<EnumDecl>,
323 pub symbols: Vec<Symbol>,
324}
325
326impl SemanticModel {
327 pub fn build(hir: hir::Program, cst: &cst::Program) -> SemanticModel {
330 let enums = cst
331 .declarations
332 .iter()
333 .filter_map(|decl| match decl {
334 cst::Decl::Enum { name, members, .. } => Some(EnumDecl {
335 name: name.clone(),
336 members: members
337 .iter()
338 .map(|(member, span)| EnumMember {
339 name: member.clone(),
340 span: resolve_span(*span, &hir.files)
341 .expect("every token span resolves through the file registry"),
342 })
343 .collect(),
344 }),
345 _ => None,
346 })
347 .collect();
348 let mut model = SemanticModel {
349 hir,
350 enums,
351 symbols: Vec::new(),
352 };
353 model.index_symbols();
354 model
355 }
356
357 pub fn declarations(&self) -> &[Declaration] {
360 &self.hir.declarations
361 }
362
363 pub fn rules(&self) -> &[RuleEntry] {
365 &self.hir.rules
366 }
367
368 pub fn defines(&self) -> &[Define] {
370 &self.hir.defines
371 }
372
373 pub fn enums(&self) -> &[EnumDecl] {
375 &self.enums
376 }
377
378 pub fn symbols(&self) -> &[Symbol] {
381 &self.symbols
382 }
383
384 pub fn symbol(&self, name: &str) -> Option<&Symbol> {
387 self.symbols.iter().find(|symbol| symbol.name == name)
388 }
389
390 pub fn symbol_at(&self, span: Span) -> Option<&Symbol> {
393 self.symbols.iter().find(|symbol| {
394 span_contains(symbol.declaration.to_span(), span)
395 || symbol
396 .references
397 .iter()
398 .any(|reference| span_contains(reference.to_span(), span))
399 })
400 }
401
402 pub fn provenance(&self, span: Span) -> Option<SourceLocation> {
405 resolve_span(span, &self.hir.files)
406 }
407
408 pub fn file(&self, id: u32) -> Option<&str> {
410 self.hir
411 .files
412 .iter()
413 .find(|file| file.id == id)
414 .map(|file| file.path.as_str())
415 }
416
417 fn index_symbols(&mut self) {
420 for decl in &self.hir.declarations {
421 let (kind, name, span) = match decl {
422 Declaration::GlobalVariable {
423 name,
424 name_span,
425 span,
426 ..
427 } => (SymbolKind::Global, name, name_span.or(*span)),
428 Declaration::PlayerVariable {
429 name,
430 name_span,
431 span,
432 ..
433 } => (SymbolKind::Player, name, name_span.or(*span)),
434 Declaration::Subroutine {
435 name,
436 name_span,
437 span,
438 ..
439 } => (SymbolKind::Subroutine, name, name_span.or(*span)),
440 Declaration::Constant { name, span, .. } => (SymbolKind::Constant, name, *span),
441 Declaration::Macro { name, span, .. } => (SymbolKind::Macro, name, *span),
442 };
443 let Some(span) = span.map(to_frontend_span) else {
444 continue;
447 };
448 let Some(declaration) = resolve_span(span, &self.hir.files) else {
449 continue;
450 };
451 self.symbols.push(Symbol {
452 name: name.clone(),
453 kind,
454 declaration,
455 references: Vec::new(),
456 });
457 }
458 for entry in &self.hir.rules {
459 let RuleEntry::SubroutineDef {
460 name,
461 source_name,
462 name_span,
463 span,
464 ..
465 } = entry
466 else {
467 continue;
468 };
469 let Some(span) = name_span.or(*span).map(to_frontend_span) else {
470 continue;
471 };
472 let Some(declaration) = resolve_span(span, &self.hir.files) else {
473 continue;
474 };
475 self.symbols.push(Symbol {
476 name: if source_name.is_empty() {
477 name.clone()
478 } else {
479 source_name.clone()
480 },
481 kind: SymbolKind::Def,
482 declaration,
483 references: Vec::new(),
484 });
485 }
486
487 let mut sites: Vec<(SymbolKind, String, Span)> = Vec::new();
488 for decl in &self.hir.declarations {
489 match decl {
490 Declaration::GlobalVariable {
491 initializer: Some(initializer),
492 ..
493 }
494 | Declaration::PlayerVariable {
495 initializer: Some(initializer),
496 ..
497 } => Self::collect_expr(initializer, &mut sites),
498 Declaration::Constant { value, .. } => Self::collect_expr(value, &mut sites),
499 Declaration::Macro { body, .. } => {
500 for stmt in body {
501 Self::collect_stmt(stmt, &mut sites);
502 }
503 }
504 _ => {}
505 }
506 }
507 for entry in &self.hir.rules {
508 match entry {
509 RuleEntry::Rule(rule) => {
510 for arg in &rule.event.args {
511 Self::collect_expr(arg, &mut sites);
512 }
513 for condition in &rule.conditions {
514 Self::collect_expr(condition, &mut sites);
515 }
516 for stmt in &rule.actions {
517 Self::collect_stmt(stmt, &mut sites);
518 }
519 }
520 RuleEntry::SubroutineDef { body, .. } => {
521 for stmt in body {
522 Self::collect_stmt(stmt, &mut sites);
523 }
524 }
525 }
526 }
527 for (kind, name, span) in sites {
528 self.attach_reference(kind, &name, span);
529 }
530 }
531
532 fn attach_reference(&mut self, kind: SymbolKind, name: &str, span: Span) {
536 let Some(location) = resolve_span(span, &self.hir.files) else {
537 return;
538 };
539 if let Some(index) = self
540 .symbols
541 .iter()
542 .position(|symbol| symbol.kind == kind && symbol.name == name)
543 {
544 self.symbols[index].references.push(location);
545 }
546 }
547
548 fn collect_expr(expr: &HirExpr, sites: &mut Vec<(SymbolKind, String, Span)>) {
549 match expr {
550 HirExpr::Number { .. }
551 | HirExpr::String { .. }
552 | HirExpr::Bool { .. }
553 | HirExpr::Null { .. }
554 | HirExpr::Enum { .. }
555 | HirExpr::EventPlayer { .. }
556 | HirExpr::HostPlayer { .. }
557 | HirExpr::MacroParam { .. }
558 | HirExpr::StringModifier { .. }
559 | HirExpr::Local { .. } => {}
560 HirExpr::Type { args, .. } => {
561 for arg in args {
562 Self::collect_expr(arg, sites);
563 }
564 }
565 HirExpr::GlobalVar { name, span } | HirExpr::Constant { name, span } => {
566 let kind = if matches!(expr, HirExpr::GlobalVar { .. }) {
567 SymbolKind::Global
568 } else {
569 SymbolKind::Constant
570 };
571 if let Some(span) = span {
572 sites.push((kind, name.clone(), to_frontend_span(*span)));
573 }
574 }
575 HirExpr::PlayerVar {
576 name,
577 member_span,
578 span,
579 ..
580 } => {
581 if let Some(span) = member_span.as_ref().or(span.as_ref()) {
582 sites.push((SymbolKind::Player, name.clone(), to_frontend_span(*span)));
583 }
584 }
585 HirExpr::Member { receiver, .. } => Self::collect_expr(receiver, sites),
586 HirExpr::Array { elements, .. } => {
587 for element in elements {
588 Self::collect_expr(element, sites);
589 }
590 }
591 HirExpr::Dict { entries, .. } => {
592 for entry in entries {
593 Self::collect_expr(&entry.key, sites);
594 Self::collect_expr(&entry.value, sites);
595 }
596 }
597 HirExpr::Comprehension {
598 element,
599 iterable,
600 condition,
601 ..
602 } => {
603 Self::collect_expr(iterable, sites);
604 Self::collect_expr(element, sites);
605 if let Some(condition) = condition {
606 Self::collect_expr(condition, sites);
607 }
608 }
609 HirExpr::Lambda { body, .. } => Self::collect_expr(body, sites),
610 HirExpr::Vector { x, y, z, .. } => {
611 Self::collect_expr(x, sites);
612 Self::collect_expr(y, sites);
613 Self::collect_expr(z, sites);
614 }
615 HirExpr::Call { name, span, args } => {
616 if let Some(span) = span {
620 sites.push((
621 SymbolKind::Subroutine,
622 name.clone(),
623 to_frontend_span(*span),
624 ));
625 sites.push((SymbolKind::Def, name.clone(), to_frontend_span(*span)));
626 }
627 for arg in args {
628 Self::collect_expr(arg, sites);
629 }
630 }
631 HirExpr::MacroCall { name, span, args } => {
632 if let Some(span) = span {
633 sites.push((SymbolKind::Macro, name.clone(), to_frontend_span(*span)));
634 }
635 for arg in args {
636 Self::collect_expr(arg, sites);
637 }
638 }
639 HirExpr::ReceiverCall { receiver, args, .. } => {
640 Self::collect_expr(receiver, sites);
644 for arg in args {
645 Self::collect_expr(arg, sites);
646 }
647 }
648 HirExpr::Binary { left, right, .. } => {
649 Self::collect_expr(left, sites);
650 Self::collect_expr(right, sites);
651 }
652 HirExpr::Conditional {
653 then_value,
654 condition,
655 else_value,
656 ..
657 } => {
658 Self::collect_expr(then_value, sites);
659 Self::collect_expr(condition, sites);
660 Self::collect_expr(else_value, sites);
661 }
662 HirExpr::Unary { operand, .. } => Self::collect_expr(operand, sites),
663 HirExpr::Index { array, index, .. } => {
664 Self::collect_expr(array, sites);
665 Self::collect_expr(index, sites);
666 }
667 HirExpr::Format { args, .. } => {
668 for arg in args {
669 Self::collect_expr(arg, sites);
670 }
671 }
672 }
673 }
674
675 fn collect_stmt(stmt: &HirStmt, sites: &mut Vec<(SymbolKind, String, Span)>) {
676 match stmt {
677 HirStmt::Expr { expr, .. } => Self::collect_expr(expr, sites),
678 HirStmt::Assign { target, value, .. } => {
679 Self::collect_expr(target, sites);
680 Self::collect_expr(value, sites);
681 }
682 HirStmt::Delete { target, .. } => Self::collect_expr(target, sites),
683 HirStmt::If {
684 branches, r#else, ..
685 } => {
686 for branch in branches {
687 Self::collect_expr(&branch.condition, sites);
688 for stmt in &branch.body {
689 Self::collect_stmt(stmt, sites);
690 }
691 }
692 if let Some(r#else) = r#else {
693 for stmt in r#else {
694 Self::collect_stmt(stmt, sites);
695 }
696 }
697 }
698 HirStmt::For {
699 variable,
700 iterable,
701 body,
702 ..
703 } => {
704 Self::collect_expr(variable, sites);
705 Self::collect_expr(iterable, sites);
706 for stmt in body {
707 Self::collect_stmt(stmt, sites);
708 }
709 }
710 HirStmt::While {
711 condition, body, ..
712 } => {
713 Self::collect_expr(condition, sites);
714 for stmt in body {
715 Self::collect_stmt(stmt, sites);
716 }
717 }
718 HirStmt::DoWhile {
719 condition, body, ..
720 } => {
721 Self::collect_expr(condition, sites);
722 for stmt in body {
723 Self::collect_stmt(stmt, sites);
724 }
725 }
726 HirStmt::Switch { value, arms, .. } => {
727 Self::collect_expr(value, sites);
728 for arm in arms {
729 match arm {
730 hir::SwitchArm::Case { value, body, .. } => {
731 Self::collect_expr(value, sites);
732 for stmt in body {
733 Self::collect_stmt(stmt, sites);
734 }
735 }
736 hir::SwitchArm::Default { body, .. } => {
737 for stmt in body {
738 Self::collect_stmt(stmt, sites);
739 }
740 }
741 }
742 }
743 }
744 HirStmt::Break { .. } => {}
745 HirStmt::Return { .. } => {}
746 HirStmt::Continue { .. } | HirStmt::Label { .. } => {}
747 HirStmt::Goto { offset, .. } => {
748 if let Some(offset) = offset {
749 Self::collect_expr(offset, sites);
750 }
751 }
752 HirStmt::CallSubroutine { name, span } => {
753 if let Some(span) = span {
754 sites.push((
755 SymbolKind::Subroutine,
756 name.clone(),
757 to_frontend_span(*span),
758 ));
759 sites.push((SymbolKind::Def, name.clone(), to_frontend_span(*span)));
760 }
761 }
762 HirStmt::Pass { .. } => {}
763 }
764 }
765}
766
767#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
770pub struct EnumDecl {
771 pub name: String,
772 pub members: Vec<EnumMember>,
773}
774
775#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
777pub struct EnumMember {
778 pub name: String,
779 pub span: SourceLocation,
780}
781
782#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
784#[serde(rename_all = "camelCase")]
785pub enum SymbolKind {
786 Global,
787 Player,
788 Subroutine,
789 Def,
790 Constant,
791 Macro,
792}
793
794#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
797pub struct Symbol {
798 pub name: String,
799 pub kind: SymbolKind,
800 pub declaration: SourceLocation,
801 pub references: Vec<SourceLocation>,
802}
803
804fn span_contains(outer: Span, inner: Span) -> bool {
807 position_leq(outer.start, inner.start) && position_leq(inner.end, outer.end)
808}
809
810fn position_leq(a: Position, b: Position) -> bool {
811 a.line < b.line || (a.line == b.line && a.col <= b.col)
812}
813
814#[cfg(test)]
815mod tests {
816 use super::*;
817
818 fn check_source(source: &str) -> CheckOutcome {
819 check(source, "main.opy", Path::new(""))
820 }
821
822 #[test]
823 fn clean_project_has_no_diagnostics_and_a_model() {
824 let outcome = check_source(
825 "globalvar total = 0\nrule \"r\":\n @Event global\n total += 1\n debug(total)\n",
826 );
827 assert!(
828 outcome.is_clean(),
829 "unexpected diagnostics: {:?}",
830 outcome.diagnostics
831 );
832 let model = outcome.model.expect("a clean project resolves");
833 assert_eq!(outcome.files.len(), 1);
834 assert_eq!(model.declarations().len(), 1);
835 assert_eq!(model.rules().len(), 1);
836 }
837
838 #[test]
839 fn symbols_index_declarations_and_references() {
840 let outcome = check_source(
841 "globalvar total\nplayervar P\nsubroutine reset\nmacro double(x):\n x + x\nrule \"r\":\n @Event eachPlayer\n total = 1\n eventPlayer.P = total\n reset()\n double(2)\n",
842 );
843 let model = outcome.model.expect("clean project");
844 let names: Vec<(&str, SymbolKind)> = model
845 .symbols()
846 .iter()
847 .map(|symbol| (symbol.name.as_str(), symbol.kind))
848 .collect();
849 assert_eq!(
850 names,
851 vec![
852 ("total", SymbolKind::Global),
853 ("P", SymbolKind::Player),
854 ("reset", SymbolKind::Subroutine),
855 ("double", SymbolKind::Macro),
856 ]
857 );
858 assert_eq!(model.symbol("total").expect("symbol").references.len(), 2);
859 assert_eq!(model.symbol("P").expect("symbol").references.len(), 1);
860 let reset = model.symbol("reset").expect("symbol");
861 assert_eq!(reset.references.len(), 1);
862 assert_eq!(reset.references[0].path, "main.opy");
863 assert_eq!(model.symbol("double").expect("symbol").references.len(), 1);
864 }
865
866 #[test]
867 fn symbol_lookup_by_name_and_span() {
868 let outcome =
869 check_source("globalvar total\nrule \"r\":\n @Event global\n total = 1\n");
870 let model = outcome.model.expect("clean project");
871 let total = model.symbol("total").expect("symbol by name");
872 assert_eq!(total.kind, SymbolKind::Global);
873 let at_decl = model
875 .symbol_at(total.declaration.to_span())
876 .expect("symbol at declaration span");
877 assert_eq!(at_decl.name, "total");
878 let at_ref = model
880 .symbol_at(total.references[0].to_span())
881 .expect("symbol at reference span");
882 assert_eq!(at_ref.name, "total");
883 assert!(
884 model
885 .symbol_at(Span::new(99, Position::new(1, 1), Position::new(1, 1)))
886 .is_none()
887 );
888 }
889
890 #[test]
891 fn provenance_resolves_through_the_file_registry() {
892 let outcome =
893 check_source("globalvar total\nrule \"r\":\n @Event global\n total = 1\n");
894 let model = outcome.model.expect("clean project");
895 let total = model.symbol("total").expect("symbol");
896 let provenance = model
897 .provenance(total.references[0].to_span())
898 .expect("provenance");
899 assert_eq!(provenance.file_id, 0);
900 assert_eq!(provenance.path, "main.opy");
901 assert_eq!(provenance.start.line, 4);
902 assert_eq!(model.file(0), Some("main.opy"));
903 assert_eq!(model.file(1), None);
904 }
905
906 #[test]
907 fn custom_enums_are_queried_from_the_model() {
908 let outcome = check_source(
909 "globalvar x\nenum Direction:\n NORTH\n SOUTH\nrule \"r\":\n @Event global\n x = Direction.SOUTH\n",
910 );
911 let model = outcome.model.expect("clean project");
912 assert_eq!(model.enums().len(), 1);
913 let direction = &model.enums()[0];
914 assert_eq!(direction.name, "Direction");
915 let members: Vec<&str> = direction
916 .members
917 .iter()
918 .map(|member| member.name.as_str())
919 .collect();
920 assert_eq!(members, vec!["NORTH", "SOUTH"]);
921 assert!(direction.members[0].span.path.ends_with("main.opy"));
922 }
923
924 #[test]
925 fn check_reports_every_parse_error() {
926 let outcome = check_source("rule \"a\"\n @Event global\nrule \"b\"\n");
931 assert!(!outcome.is_clean());
932 assert!(outcome.model.is_none());
933 assert_eq!(outcome.diagnostics.len(), 3);
934 assert!(
935 outcome
936 .diagnostics
937 .iter()
938 .all(|diagnostic| diagnostic.code == "parse-error")
939 );
940 }
941
942 #[test]
943 fn diagnostics_carry_severity_code_and_span() {
944 let outcome = check_source("rule \"r\":\n @Event global\n frobnicate()\n");
945 let diagnostic = &outcome.diagnostics[0];
946 assert_eq!(diagnostic.severity, DiagnosticSeverity::Error);
947 assert_eq!(diagnostic.code, "unknown-action");
948 let span = diagnostic.span.as_ref().expect("source-located");
949 assert_eq!(span.path, "main.opy");
950 assert_eq!(span.start.line, 3);
951 }
952}