use std::sync::Arc;
use crate::checker::{self, SemanticModel};
use crate::diagnostic::Diagnostic;
use crate::emitter::{self, EmitOptions, EmitOutput};
use crate::lint::{LintProfile, LintTable};
use crate::parser;
use crate::program::ResolvedProgram;
use crate::scanner;
use crate::source::{ScriptKind, SourceId, SourceText};
use crate::syntax::SourceFile;
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum FrontendMode {
Check,
JavaScript,
Declaration,
}
impl FrontendMode {
#[must_use]
const fn emit_options(self) -> Option<EmitOptions> {
match self {
Self::Check => None,
Self::JavaScript => Some(EmitOptions::javascript()),
Self::Declaration => Some(EmitOptions::declaration()),
}
}
}
#[derive(Clone, Debug)]
pub struct FrontendRequest {
pub source_id: SourceId,
pub script_kind: ScriptKind,
pub source: Arc<SourceText>,
pub mode: FrontendMode,
}
pub struct FrontendOutput {
mode: FrontendMode,
source_file: SourceFile,
semantic_model: SemanticModel,
emit: Option<EmitOutput>,
diagnostics: Vec<Diagnostic>,
}
impl FrontendOutput {
#[must_use]
pub const fn mode(&self) -> FrontendMode {
self.mode
}
#[must_use]
pub const fn source_file(&self) -> &SourceFile {
&self.source_file
}
#[must_use]
pub const fn semantic_model(&self) -> &SemanticModel {
&self.semantic_model
}
#[must_use]
pub const fn emit(&self) -> Option<&EmitOutput> {
self.emit.as_ref()
}
#[must_use]
pub fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics
}
#[must_use]
pub fn has_errors(&self) -> bool {
self.diagnostics
.iter()
.any(|diagnostic| !diagnostic.is_warning())
}
#[must_use]
pub fn into_parts(
self,
) -> (
SourceFile,
SemanticModel,
Option<EmitOutput>,
Vec<Diagnostic>,
) {
(
self.source_file,
self.semantic_model,
self.emit,
self.diagnostics,
)
}
}
pub struct ProgramFrontendOutput {
entrypoint: SourceId,
modules: Vec<FrontendOutput>,
}
impl ProgramFrontendOutput {
#[must_use]
pub const fn entrypoint_id(&self) -> SourceId {
self.entrypoint
}
#[must_use]
pub fn modules(&self) -> &[FrontendOutput] {
&self.modules
}
#[must_use]
pub fn module(&self, source_id: SourceId) -> Option<&FrontendOutput> {
self.modules
.iter()
.find(|output| output.source_file().source_id() == source_id)
}
}
#[must_use]
pub fn compile_program_frontend(
program: &ResolvedProgram,
mode: FrontendMode,
) -> ProgramFrontendOutput {
compile_program_frontend_with_lints(program, mode, &LintTable::new(LintProfile::Default))
}
#[must_use]
pub fn compile_program_frontend_with_lints(
program: &ResolvedProgram,
mode: FrontendMode,
levels: &LintTable,
) -> ProgramFrontendOutput {
let modules = program
.modules()
.iter()
.map(|module| {
compile_frontend_with_lints(
FrontendRequest {
source_id: module.source_id(),
script_kind: module.script_kind(),
source: Arc::clone(module.source()),
mode,
},
levels,
)
})
.collect();
ProgramFrontendOutput {
entrypoint: program.entrypoint_id(),
modules,
}
}
#[must_use]
pub fn compile_frontend(request: FrontendRequest) -> FrontendOutput {
compile_frontend_with_lints(request, &LintTable::new(LintProfile::Default))
}
#[must_use]
pub fn compile_frontend_with_lints(request: FrontendRequest, levels: &LintTable) -> FrontendOutput {
let FrontendRequest {
source_id,
script_kind,
source,
mode,
} = request;
let scanned = scanner::scan(source_id, script_kind, source);
let parsed = parser::parse(scanned);
let checked = checker::check_with_lints(&parsed, levels);
let emit = mode
.emit_options()
.map(|options| emitter::emit(parsed.product(), options));
let (source_file, parse_diagnostics) = parsed.into_parts();
let (semantic_model, check_diagnostics) = checked.into_parts();
let mut diagnostics = parse_diagnostics;
diagnostics.extend(check_diagnostics);
if let Some(output) = &emit {
diagnostics.extend(output.diagnostics.iter().cloned());
}
let diagnostics = canonicalize(diagnostics);
FrontendOutput {
mode,
source_file,
semantic_model,
emit,
diagnostics,
}
}
fn canonicalize(mut diagnostics: Vec<Diagnostic>) -> Vec<Diagnostic> {
diagnostics.sort();
diagnostics.dedup();
diagnostics
}
#[cfg(test)]
mod tests {
use super::{FrontendMode, FrontendRequest, canonicalize, compile_frontend};
use crate::diagnostic::{Diagnostic, DiagnosticCode, DiagnosticSeverity};
use crate::source::{ScriptKind, SourceId, SourceText, TextRange, Utf16Pos};
use std::sync::Arc;
fn request(source: &str, mode: FrontendMode) -> FrontendRequest {
FrontendRequest {
source_id: SourceId::new(0),
script_kind: ScriptKind::TypeScript,
source: Arc::new(SourceText::new(source)),
mode,
}
}
fn range(start: usize, end: usize) -> TextRange {
TextRange::new(Utf16Pos::new(start), Utf16Pos::new(end)).expect("ordered range")
}
fn has_code(diagnostics: &[Diagnostic], code: &str) -> bool {
diagnostics
.iter()
.any(|diagnostic| diagnostic.code().as_str() == code)
}
fn is_sorted_unique(diagnostics: &[Diagnostic]) -> bool {
diagnostics.windows(2).all(|pair| pair[0] < pair[1])
}
#[test]
fn merges_syntax_type_and_warning_diagnostics_into_one_ordered_vector() {
let source =
"try {} catch (e) { e.message }\nconst n: number = \"oops\";\nconst bad: number =";
let output = compile_frontend(request(source, FrontendMode::Check));
let diagnostics = output.diagnostics();
assert!(has_code(diagnostics, "BAMTS-W005"), "warning stage missing");
assert!(has_code(diagnostics, "BAMTS-C004"), "type stage missing");
assert!(
diagnostics.iter().any(
|diagnostic| diagnostic.severity() == DiagnosticSeverity::Error
&& diagnostic.code().as_str() != "BAMTS-C004"
),
"syntax stage missing",
);
assert!(diagnostics.iter().any(Diagnostic::is_warning));
assert!(
diagnostics
.iter()
.any(|diagnostic| !diagnostic.is_warning())
);
assert!(is_sorted_unique(diagnostics), "diagnostics not canonical");
}
#[test]
fn emits_despite_earlier_errors() {
let source = "const n: number = \"oops\";";
let output = compile_frontend(request(source, FrontendMode::JavaScript));
assert!(output.has_errors(), "expected a type error");
let emit = output.emit().expect("javascript mode must emit");
assert!(
emit.code.contains("oops"),
"emit should still print the recovered program",
);
}
#[test]
fn check_mode_produces_no_emit_while_js_and_declaration_do() {
let source = "let value: number = 1;";
let check = compile_frontend(request(source, FrontendMode::Check));
assert!(check.emit().is_none(), "check mode must not emit");
let js = compile_frontend(request(source, FrontendMode::JavaScript));
let js_emit = js.emit().expect("javascript mode emits");
let declaration = compile_frontend(request(source, FrontendMode::Declaration));
let declaration_emit = declaration.emit().expect("declaration mode emits");
assert!(!js_emit.code.contains("number"), "js must erase the type");
assert!(
declaration_emit.code.contains("number"),
"declaration must retain the type",
);
assert_ne!(js_emit.code, declaration_emit.code);
}
#[test]
fn canonicalize_collapses_exact_duplicates_and_preserves_distinct_ones() {
let source_id = SourceId::new(0);
let code = DiagnosticCode::new("BAMTS-C001");
let base = Diagnostic::error(code, source_id, range(0, 1), "duplicate");
let duplicate = base.clone();
let as_warning = Diagnostic::warning(code, source_id, range(0, 1), "duplicate");
let elsewhere = Diagnostic::error(code, source_id, range(2, 3), "duplicate");
let other_code = Diagnostic::error(
DiagnosticCode::new("BAMTS-C002"),
source_id,
range(0, 1),
"duplicate",
);
let merged = canonicalize(vec![
base.clone(),
duplicate,
as_warning.clone(),
elsewhere.clone(),
other_code.clone(),
base.clone(),
]);
assert_eq!(merged.len(), 4);
assert_eq!(merged.iter().filter(|d| **d == base).count(), 1);
assert!(merged.contains(&as_warning));
assert!(merged.contains(&elsewhere));
assert!(merged.contains(&other_code));
assert!(is_sorted_unique(&merged));
}
#[test]
fn frontend_output_never_contains_duplicate_diagnostics() {
let source =
"try {} catch (e) { e.message }\nconst n: number = \"oops\";\nconst bad: number =";
let output = compile_frontend(request(source, FrontendMode::JavaScript));
assert!(is_sorted_unique(output.diagnostics()));
}
}