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