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