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