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 fn failure(diagnostics: Vec<Diagnostic>, files: Vec<FileRecord>) -> Self {
77 Self {
78 diagnostics,
79 model: None,
80 files,
81 post_compile_hook: None,
82 }
83 }
84
85 pub fn is_clean(&self) -> bool {
87 self.diagnostics
88 .iter()
89 .all(|diagnostic| diagnostic.severity != DiagnosticSeverity::Error)
90 }
91}
92
93pub fn check(source: &str, main_path: &str, root: &Path) -> CheckOutcome {
97 check_with_overlay(source, main_path, root, &std::collections::BTreeMap::new())
98}
99
100pub fn check_with_overlay(
103 source: &str,
104 main_path: &str,
105 root: &Path,
106 overlay: &std::collections::BTreeMap<String, String>,
107) -> CheckOutcome {
108 let PreprocessOutcome {
109 result,
110 files,
111 warnings,
112 } = crate::preprocess::preprocess_with_overlay_outcome(source, main_path, root, overlay);
113 let preprocessed = match result {
114 Ok((preprocessed, _)) => preprocessed,
115 Err(error) => {
116 let mut diagnostics = warnings
117 .iter()
118 .map(|warning| Diagnostic::from_warning(warning, &files))
119 .collect::<Vec<_>>();
120 diagnostics.push(Diagnostic::from_error(error, &files));
121 return CheckOutcome::failure(diagnostics, files);
122 }
123 };
124 let parsed = crate::parser::parse_with_options(
125 &preprocessed.tokens,
126 preprocessed.preprocessing.allow_macro_redeclaration,
127 );
128 let Some(mut program) = parsed.program else {
129 let mut diagnostics = visible_warnings(&preprocessed)
132 .map(|warning| Diagnostic::from_warning(warning, &files))
133 .collect::<Vec<_>>();
134 diagnostics.extend(
135 parsed
136 .errors
137 .iter()
138 .map(|error| Diagnostic::from_error(error.clone(), &files)),
139 );
140 return CheckOutcome::failure(diagnostics, files);
141 };
142 if let Some(block) = &preprocessed.settings {
146 match crate::settings::parse_block(block) {
147 Ok(parsed_settings) => program.settings = Some(parsed_settings),
148 Err(error) => {
149 let mut diagnostics = visible_warnings(&preprocessed)
150 .map(|warning| Diagnostic::from_warning(warning, &files))
151 .collect::<Vec<_>>();
152 diagnostics.push(Diagnostic::from_error(error, &files));
153 return CheckOutcome::failure(diagnostics, files);
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 let frontend_warnings = visible_warnings(&preprocessed)
182 .map(|warning| Diagnostic::from_warning(warning, &files))
183 .collect::<Vec<_>>();
184 hir.preprocessing = preprocessed.preprocessing;
185 if let Err(error) = crate::settings::resolve_hir_settings(&mut hir, &program) {
186 let mut diagnostics = frontend_warnings;
187 diagnostics.push(Diagnostic::from_error(error, &files));
188 return CheckOutcome::failure(diagnostics, files);
189 }
190 CheckOutcome {
191 diagnostics: frontend_warnings,
192 model: Some(SemanticModel::build(hir, &program)),
193 files,
194 post_compile_hook: preprocessed.post_compile_hook,
199 }
200 }
201 Err(error) => {
202 let mut diagnostics = visible_warnings(&preprocessed)
203 .map(|warning| Diagnostic::from_warning(warning, &files))
204 .collect::<Vec<_>>();
205 diagnostics.push(Diagnostic::from_error(error, &files));
206 CheckOutcome::failure(diagnostics, files)
207 }
208 }
209}
210
211#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
217pub struct Diagnostic {
218 pub severity: DiagnosticSeverity,
219 pub code: String,
220 pub message: String,
221 pub span: Option<SourceLocation>,
222}
223
224impl Diagnostic {
225 fn from_warning(warning: &PreprocessWarning, files: &[FileRecord]) -> Diagnostic {
226 Diagnostic {
227 severity: DiagnosticSeverity::Warning,
228 code: warning.code.clone(),
229 message: warning.message.clone(),
230 span: resolve_record_span(warning.span, files),
231 }
232 }
233
234 fn from_error(error: OpyError, files: &[FileRecord]) -> Diagnostic {
235 Diagnostic {
236 severity: DiagnosticSeverity::Error,
237 code: error.code,
238 message: error.message,
239 span: error.span.and_then(|span| resolve_record_span(span, files)),
240 }
241 }
242}
243
244#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
246#[serde(rename_all = "camelCase")]
247pub enum DiagnosticSeverity {
248 Error,
249 Warning,
250}
251
252impl DiagnosticSeverity {
253 pub fn as_str(&self) -> &'static str {
254 match self {
255 DiagnosticSeverity::Error => "error",
256 DiagnosticSeverity::Warning => "warning",
257 }
258 }
259}
260
261#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
264pub struct SourceLocation {
265 pub file_id: u32,
266 pub path: String,
267 pub start: Position,
268 pub end: Position,
269}
270
271impl SourceLocation {
272 pub fn to_span(&self) -> Span {
274 Span::new(self.file_id, self.start, self.end)
275 }
276}
277
278fn resolve_span(span: Span, files: &[SourceFile]) -> Option<SourceLocation> {
279 let path = files.iter().find(|file| file.id == span.file)?.path.clone();
280 Some(SourceLocation {
281 file_id: span.file,
282 path,
283 start: span.start,
284 end: span.end,
285 })
286}
287
288fn resolve_record_span(span: Span, files: &[FileRecord]) -> Option<SourceLocation> {
291 let path = files.iter().find(|file| file.id == span.file)?.path.clone();
292 Some(SourceLocation {
293 file_id: span.file,
294 path,
295 start: span.start,
296 end: span.end,
297 })
298}
299
300fn to_frontend_span(span: hir::types::Span) -> Span {
304 Span::new(
305 span.file,
306 Position::new(span.start.line, span.start.col),
307 Position::new(span.end.line, span.end.col),
308 )
309}
310
311#[derive(Debug, Clone, Serialize)]
318pub struct SemanticModel {
319 pub hir: hir::Program,
320 pub enums: Vec<EnumDecl>,
321 pub symbols: Vec<Symbol>,
322}
323
324impl SemanticModel {
325 pub fn build(hir: hir::Program, cst: &cst::Program) -> SemanticModel {
328 let enums = cst
329 .declarations
330 .iter()
331 .filter_map(|decl| match decl {
332 cst::Decl::Enum { name, members, .. } => Some(EnumDecl {
333 name: name.clone(),
334 members: members
335 .iter()
336 .map(|(member, span)| EnumMember {
337 name: member.clone(),
338 span: resolve_span(*span, &hir.files)
339 .expect("every token span resolves through the file registry"),
340 })
341 .collect(),
342 }),
343 _ => None,
344 })
345 .collect();
346 let mut model = SemanticModel {
347 hir,
348 enums,
349 symbols: Vec::new(),
350 };
351 model.index_symbols();
352 model
353 }
354
355 pub fn declarations(&self) -> &[Declaration] {
358 &self.hir.declarations
359 }
360
361 pub fn rules(&self) -> &[RuleEntry] {
363 &self.hir.rules
364 }
365
366 pub fn defines(&self) -> &[Define] {
368 &self.hir.defines
369 }
370
371 pub fn enums(&self) -> &[EnumDecl] {
373 &self.enums
374 }
375
376 pub fn symbols(&self) -> &[Symbol] {
379 &self.symbols
380 }
381
382 pub fn symbol(&self, name: &str) -> Option<&Symbol> {
385 self.symbols.iter().find(|symbol| symbol.name == name)
386 }
387
388 pub fn symbol_at(&self, span: Span) -> Option<&Symbol> {
391 self.symbols.iter().find(|symbol| {
392 span_contains(symbol.declaration.to_span(), span)
393 || symbol
394 .references
395 .iter()
396 .any(|reference| span_contains(reference.to_span(), span))
397 })
398 }
399
400 pub fn provenance(&self, span: Span) -> Option<SourceLocation> {
403 resolve_span(span, &self.hir.files)
404 }
405
406 pub fn file(&self, id: u32) -> Option<&str> {
408 self.hir
409 .files
410 .iter()
411 .find(|file| file.id == id)
412 .map(|file| file.path.as_str())
413 }
414
415 fn index_symbols(&mut self) {
418 for decl in &self.hir.declarations {
419 let (kind, name, span) = match decl {
420 Declaration::GlobalVariable {
421 name,
422 name_span,
423 span,
424 ..
425 } => (SymbolKind::Global, name, name_span.or(*span)),
426 Declaration::PlayerVariable {
427 name,
428 name_span,
429 span,
430 ..
431 } => (SymbolKind::Player, name, name_span.or(*span)),
432 Declaration::Subroutine {
433 name,
434 name_span,
435 span,
436 ..
437 } => (SymbolKind::Subroutine, name, name_span.or(*span)),
438 Declaration::Constant { name, span, .. } => (SymbolKind::Constant, name, *span),
439 Declaration::Macro { name, span, .. } => (SymbolKind::Macro, name, *span),
440 };
441 let Some(span) = span.map(to_frontend_span) else {
442 continue;
445 };
446 let Some(declaration) = resolve_span(span, &self.hir.files) else {
447 continue;
448 };
449 self.symbols.push(Symbol {
450 name: name.clone(),
451 kind,
452 declaration,
453 references: Vec::new(),
454 });
455 }
456 for entry in &self.hir.rules {
457 let RuleEntry::SubroutineDef {
458 name,
459 source_name,
460 name_span,
461 span,
462 ..
463 } = entry
464 else {
465 continue;
466 };
467 let Some(span) = name_span.or(*span).map(to_frontend_span) else {
468 continue;
469 };
470 let Some(declaration) = resolve_span(span, &self.hir.files) else {
471 continue;
472 };
473 self.symbols.push(Symbol {
474 name: if source_name.is_empty() {
475 name.clone()
476 } else {
477 source_name.clone()
478 },
479 kind: SymbolKind::Def,
480 declaration,
481 references: Vec::new(),
482 });
483 }
484
485 let mut collector = ReferenceSiteCollector { sites: Vec::new() };
486 hir::visit::walk_program(&mut collector, &self.hir);
487 for (kind, name, span) in collector.sites {
488 self.attach_reference(kind, &name, span);
489 }
490 }
491
492 fn attach_reference(&mut self, kind: SymbolKind, name: &str, span: Span) {
495 let Some(location) = resolve_span(span, &self.hir.files) else {
496 return;
497 };
498 if let Some(index) = self
499 .symbols
500 .iter()
501 .position(|symbol| symbol.kind == kind && symbol.name == name)
502 {
503 self.symbols[index].references.push(location);
504 }
505 }
506}
507
508struct ReferenceSiteCollector {
509 sites: Vec<(SymbolKind, String, Span)>,
510}
511
512impl hir::visit::Visitor for ReferenceSiteCollector {
513 fn visit_expr(&mut self, expression: &HirExpr) {
514 match expression {
515 HirExpr::GlobalVar {
516 name,
517 span: Some(span),
518 } => {
519 self.sites
520 .push((SymbolKind::Global, name.clone(), to_frontend_span(*span)));
521 }
522 HirExpr::Constant {
523 name,
524 span: Some(span),
525 } => {
526 self.sites
527 .push((SymbolKind::Constant, name.clone(), to_frontend_span(*span)));
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 self.sites
537 .push((SymbolKind::Player, name.clone(), to_frontend_span(*span)));
538 }
539 }
540 HirExpr::Call {
541 name,
542 span: Some(span),
543 ..
544 } => {
545 let span = to_frontend_span(*span);
546 self.sites
547 .push((SymbolKind::Subroutine, name.clone(), span));
548 self.sites.push((SymbolKind::Def, name.clone(), span));
549 }
550 HirExpr::MacroCall {
551 name,
552 span: Some(span),
553 ..
554 } => {
555 self.sites
556 .push((SymbolKind::Macro, name.clone(), to_frontend_span(*span)));
557 }
558 _ => {}
559 }
560 hir::visit::walk_expr(self, expression);
561 }
562
563 fn visit_comprehension(
564 &mut self,
565 element: &HirExpr,
566 iterable: &HirExpr,
567 condition: Option<&HirExpr>,
568 ) {
569 hir::visit::Visitor::visit_expr(self, iterable);
570 hir::visit::Visitor::visit_expr(self, element);
571 if let Some(condition) = condition {
572 hir::visit::Visitor::visit_expr(self, condition);
573 }
574 }
575
576 fn visit_stmt(&mut self, statement: &HirStmt) {
577 if let HirStmt::CallSubroutine {
578 name,
579 span: Some(span),
580 } = statement
581 {
582 let span = to_frontend_span(*span);
583 self.sites
584 .push((SymbolKind::Subroutine, name.clone(), span));
585 self.sites.push((SymbolKind::Def, name.clone(), span));
586 }
587 hir::visit::walk_stmt(self, statement);
588 }
589}
590
591#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
594pub struct EnumDecl {
595 pub name: String,
596 pub members: Vec<EnumMember>,
597}
598
599#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
601pub struct EnumMember {
602 pub name: String,
603 pub span: SourceLocation,
604}
605
606#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
608#[serde(rename_all = "camelCase")]
609pub enum SymbolKind {
610 Global,
611 Player,
612 Subroutine,
613 Def,
614 Constant,
615 Macro,
616}
617
618#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
621pub struct Symbol {
622 pub name: String,
623 pub kind: SymbolKind,
624 pub declaration: SourceLocation,
625 pub references: Vec<SourceLocation>,
626}
627
628fn span_contains(outer: Span, inner: Span) -> bool {
631 position_leq(outer.start, inner.start) && position_leq(inner.end, outer.end)
632}
633
634fn position_leq(a: Position, b: Position) -> bool {
635 a.line < b.line || (a.line == b.line && a.col <= b.col)
636}
637
638#[cfg(test)]
639mod tests {
640 use super::*;
641
642 fn check_source(source: &str) -> CheckOutcome {
643 check(source, "main.opy", Path::new(""))
644 }
645
646 #[test]
647 fn clean_project_has_no_diagnostics_and_a_model() {
648 let outcome = check_source(
649 "globalvar total = 0\nrule \"r\":\n @Event global\n total += 1\n debug(total)\n",
650 );
651 assert!(
652 outcome.is_clean(),
653 "unexpected diagnostics: {:?}",
654 outcome.diagnostics
655 );
656 let model = outcome.model.expect("a clean project resolves");
657 assert_eq!(outcome.files.len(), 1);
658 assert_eq!(model.declarations().len(), 1);
659 assert_eq!(model.rules().len(), 1);
660 }
661
662 #[test]
663 fn symbols_index_declarations_and_references() {
664 let outcome = check_source(
665 "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",
666 );
667 let model = outcome.model.expect("clean project");
668 let names: Vec<(&str, SymbolKind)> = model
669 .symbols()
670 .iter()
671 .map(|symbol| (symbol.name.as_str(), symbol.kind))
672 .collect();
673 assert_eq!(
674 names,
675 vec![
676 ("total", SymbolKind::Global),
677 ("P", SymbolKind::Player),
678 ("reset", SymbolKind::Subroutine),
679 ("double", SymbolKind::Macro),
680 ]
681 );
682 assert_eq!(model.symbol("total").expect("symbol").references.len(), 2);
683 assert_eq!(model.symbol("P").expect("symbol").references.len(), 1);
684 let reset = model.symbol("reset").expect("symbol");
685 assert_eq!(reset.references.len(), 1);
686 assert_eq!(reset.references[0].path, "main.opy");
687 assert_eq!(model.symbol("double").expect("symbol").references.len(), 1);
688 }
689
690 #[test]
691 fn symbol_lookup_by_name_and_span() {
692 let outcome =
693 check_source("globalvar total\nrule \"r\":\n @Event global\n total = 1\n");
694 let model = outcome.model.expect("clean project");
695 let total = model.symbol("total").expect("symbol by name");
696 assert_eq!(total.kind, SymbolKind::Global);
697 let at_decl = model
699 .symbol_at(total.declaration.to_span())
700 .expect("symbol at declaration span");
701 assert_eq!(at_decl.name, "total");
702 let at_ref = model
704 .symbol_at(total.references[0].to_span())
705 .expect("symbol at reference span");
706 assert_eq!(at_ref.name, "total");
707 assert!(
708 model
709 .symbol_at(Span::new(99, Position::new(1, 1), Position::new(1, 1)))
710 .is_none()
711 );
712 }
713
714 #[test]
715 fn provenance_resolves_through_the_file_registry() {
716 let outcome =
717 check_source("globalvar total\nrule \"r\":\n @Event global\n total = 1\n");
718 let model = outcome.model.expect("clean project");
719 let total = model.symbol("total").expect("symbol");
720 let provenance = model
721 .provenance(total.references[0].to_span())
722 .expect("provenance");
723 assert_eq!(provenance.file_id, 0);
724 assert_eq!(provenance.path, "main.opy");
725 assert_eq!(provenance.start.line, 4);
726 assert_eq!(model.file(0), Some("main.opy"));
727 assert_eq!(model.file(1), None);
728 }
729
730 #[test]
731 fn custom_enums_are_queried_from_the_model() {
732 let outcome = check_source(
733 "globalvar x\nenum Direction:\n NORTH\n SOUTH\nrule \"r\":\n @Event global\n x = Direction.SOUTH\n",
734 );
735 let model = outcome.model.expect("clean project");
736 assert_eq!(model.enums().len(), 1);
737 let direction = &model.enums()[0];
738 assert_eq!(direction.name, "Direction");
739 let members: Vec<&str> = direction
740 .members
741 .iter()
742 .map(|member| member.name.as_str())
743 .collect();
744 assert_eq!(members, vec!["NORTH", "SOUTH"]);
745 assert!(direction.members[0].span.path.ends_with("main.opy"));
746 }
747
748 #[test]
749 fn check_reports_every_parse_error() {
750 let outcome = check_source("rule \"a\"\n @Event global\nrule \"b\"\n");
755 assert!(!outcome.is_clean());
756 assert!(outcome.model.is_none());
757 assert_eq!(outcome.diagnostics.len(), 3);
758 assert!(
759 outcome
760 .diagnostics
761 .iter()
762 .all(|diagnostic| diagnostic.code == "parse-error")
763 );
764 }
765
766 #[test]
767 fn diagnostics_carry_severity_code_and_span() {
768 let outcome = check_source("rule \"r\":\n @Event global\n frobnicate()\n");
769 let diagnostic = &outcome.diagnostics[0];
770 assert_eq!(diagnostic.severity, DiagnosticSeverity::Error);
771 assert_eq!(diagnostic.code, "unknown-action");
772 let span = diagnostic.span.as_ref().expect("source-located");
773 assert_eq!(span.path, "main.opy");
774 assert_eq!(span.start.line, 3);
775 }
776}