1use std::sync::Arc;
13
14use crate::checker::{self, SemanticModel};
15use crate::diagnostic::Diagnostic;
16use crate::emitter::{self, EmitOptions, EmitOutput};
17use crate::lint::{LintProfile, LintTable};
18use crate::parser;
19use crate::program::ResolvedProgram;
20use crate::scanner;
21use crate::source::{ScriptKind, SourceId, SourceText};
22use crate::syntax::SourceFile;
23
24#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
26pub enum FrontendMode {
27 Check,
29 JavaScript,
31 Declaration,
33}
34
35impl FrontendMode {
36 #[must_use]
38 const fn emit_options(self) -> Option<EmitOptions> {
39 match self {
40 Self::Check => None,
41 Self::JavaScript => Some(EmitOptions::javascript()),
42 Self::Declaration => Some(EmitOptions::declaration()),
43 }
44 }
45}
46
47#[derive(Clone, Debug)]
49pub struct FrontendRequest {
50 pub source_id: SourceId,
52 pub script_kind: ScriptKind,
54 pub source: Arc<SourceText>,
56 pub mode: FrontendMode,
58}
59
60pub struct FrontendOutput {
67 mode: FrontendMode,
68 source_file: SourceFile,
69 semantic_model: SemanticModel,
70 emit: Option<EmitOutput>,
71 diagnostics: Vec<Diagnostic>,
72}
73
74impl FrontendOutput {
75 #[must_use]
77 pub const fn mode(&self) -> FrontendMode {
78 self.mode
79 }
80
81 #[must_use]
83 pub const fn source_file(&self) -> &SourceFile {
84 &self.source_file
85 }
86
87 #[must_use]
89 pub const fn semantic_model(&self) -> &SemanticModel {
90 &self.semantic_model
91 }
92
93 #[must_use]
95 pub const fn emit(&self) -> Option<&EmitOutput> {
96 self.emit.as_ref()
97 }
98
99 #[must_use]
101 pub fn diagnostics(&self) -> &[Diagnostic] {
102 &self.diagnostics
103 }
104
105 #[must_use]
107 pub fn has_errors(&self) -> bool {
108 self.diagnostics
109 .iter()
110 .any(|diagnostic| !diagnostic.is_warning())
111 }
112
113 #[must_use]
115 pub fn into_parts(
116 self,
117 ) -> (
118 SourceFile,
119 SemanticModel,
120 Option<EmitOutput>,
121 Vec<Diagnostic>,
122 ) {
123 (
124 self.source_file,
125 self.semantic_model,
126 self.emit,
127 self.diagnostics,
128 )
129 }
130}
131
132pub struct ProgramFrontendOutput {
134 entrypoint: SourceId,
135 modules: Vec<FrontendOutput>,
136}
137
138impl ProgramFrontendOutput {
139 #[must_use]
140 pub const fn entrypoint_id(&self) -> SourceId {
141 self.entrypoint
142 }
143
144 #[must_use]
146 pub fn modules(&self) -> &[FrontendOutput] {
147 &self.modules
148 }
149
150 #[must_use]
151 pub fn module(&self, source_id: SourceId) -> Option<&FrontendOutput> {
152 self.modules
153 .iter()
154 .find(|output| output.source_file().source_id() == source_id)
155 }
156}
157
158#[must_use]
160pub fn compile_program_frontend(
161 program: &ResolvedProgram,
162 mode: FrontendMode,
163) -> ProgramFrontendOutput {
164 compile_program_frontend_with_lints(program, mode, &LintTable::new(LintProfile::Default))
165}
166
167#[must_use]
169pub fn compile_program_frontend_with_lints(
170 program: &ResolvedProgram,
171 mode: FrontendMode,
172 levels: &LintTable,
173) -> ProgramFrontendOutput {
174 let modules = program
175 .modules()
176 .iter()
177 .map(|module| {
178 compile_frontend_with_lints(
179 FrontendRequest {
180 source_id: module.source_id(),
181 script_kind: module.script_kind(),
182 source: Arc::clone(module.source()),
183 mode,
184 },
185 levels,
186 )
187 })
188 .collect();
189 ProgramFrontendOutput {
190 entrypoint: program.entrypoint_id(),
191 modules,
192 }
193}
194
195#[must_use]
197pub fn compile_frontend(request: FrontendRequest) -> FrontendOutput {
198 compile_frontend_with_lints(request, &LintTable::new(LintProfile::Default))
199}
200
201#[must_use]
210pub fn compile_frontend_with_lints(request: FrontendRequest, levels: &LintTable) -> FrontendOutput {
211 let FrontendRequest {
212 source_id,
213 script_kind,
214 source,
215 mode,
216 } = request;
217
218 let scanned = scanner::scan(source_id, script_kind, source);
219 let parsed = parser::parse(scanned);
220 let checked = checker::check_with_lints(&parsed, levels);
221
222 let emit = mode
224 .emit_options()
225 .map(|options| emitter::emit(parsed.product(), options));
226
227 let (source_file, parse_diagnostics) = parsed.into_parts();
228 let (semantic_model, check_diagnostics) = checked.into_parts();
229
230 let mut diagnostics = parse_diagnostics;
231 diagnostics.extend(check_diagnostics);
232 if let Some(output) = &emit {
233 diagnostics.extend(output.diagnostics.iter().cloned());
234 }
235 let diagnostics = canonicalize(diagnostics);
236
237 FrontendOutput {
238 mode,
239 source_file,
240 semantic_model,
241 emit,
242 diagnostics,
243 }
244}
245
246fn canonicalize(mut diagnostics: Vec<Diagnostic>) -> Vec<Diagnostic> {
254 diagnostics.sort();
255 diagnostics.dedup();
256 diagnostics
257}
258
259#[cfg(test)]
260mod tests {
261 use super::{FrontendMode, FrontendRequest, canonicalize, compile_frontend};
262 use crate::diagnostic::{Diagnostic, DiagnosticCode, DiagnosticSeverity};
263 use crate::source::{ScriptKind, SourceId, SourceText, TextRange, Utf16Pos};
264 use std::sync::Arc;
265
266 fn request(source: &str, mode: FrontendMode) -> FrontendRequest {
267 FrontendRequest {
268 source_id: SourceId::new(0),
269 script_kind: ScriptKind::TypeScript,
270 source: Arc::new(SourceText::new(source)),
271 mode,
272 }
273 }
274
275 fn range(start: usize, end: usize) -> TextRange {
276 TextRange::new(Utf16Pos::new(start), Utf16Pos::new(end)).expect("ordered range")
277 }
278
279 fn has_code(diagnostics: &[Diagnostic], code: &str) -> bool {
280 diagnostics
281 .iter()
282 .any(|diagnostic| diagnostic.code().as_str() == code)
283 }
284
285 fn is_sorted_unique(diagnostics: &[Diagnostic]) -> bool {
286 diagnostics.windows(2).all(|pair| pair[0] < pair[1])
287 }
288
289 #[test]
290 fn merges_syntax_type_and_warning_diagnostics_into_one_ordered_vector() {
291 let source =
295 "try {} catch (e) { e.message }\nconst n: number = \"oops\";\nconst bad: number =";
296 let output = compile_frontend(request(source, FrontendMode::Check));
297 let diagnostics = output.diagnostics();
298
299 assert!(has_code(diagnostics, "BAMTS-W005"), "warning stage missing");
301 assert!(has_code(diagnostics, "BAMTS-C004"), "type stage missing");
302 assert!(
303 diagnostics.iter().any(
304 |diagnostic| diagnostic.severity() == DiagnosticSeverity::Error
305 && diagnostic.code().as_str() != "BAMTS-C004"
306 ),
307 "syntax stage missing",
308 );
309
310 assert!(diagnostics.iter().any(Diagnostic::is_warning));
312 assert!(
313 diagnostics
314 .iter()
315 .any(|diagnostic| !diagnostic.is_warning())
316 );
317
318 assert!(is_sorted_unique(diagnostics), "diagnostics not canonical");
320 }
321
322 #[test]
323 fn emits_despite_earlier_errors() {
324 let source = "const n: number = \"oops\";";
326 let output = compile_frontend(request(source, FrontendMode::JavaScript));
327
328 assert!(output.has_errors(), "expected a type error");
329 let emit = output.emit().expect("javascript mode must emit");
330 assert!(
331 emit.code.contains("oops"),
332 "emit should still print the recovered program",
333 );
334 }
335
336 #[test]
337 fn check_mode_produces_no_emit_while_js_and_declaration_do() {
338 let source = "let value: number = 1;";
339
340 let check = compile_frontend(request(source, FrontendMode::Check));
341 assert!(check.emit().is_none(), "check mode must not emit");
342
343 let js = compile_frontend(request(source, FrontendMode::JavaScript));
344 let js_emit = js.emit().expect("javascript mode emits");
345
346 let declaration = compile_frontend(request(source, FrontendMode::Declaration));
347 let declaration_emit = declaration.emit().expect("declaration mode emits");
348
349 assert!(!js_emit.code.contains("number"), "js must erase the type");
351 assert!(
352 declaration_emit.code.contains("number"),
353 "declaration must retain the type",
354 );
355 assert_ne!(js_emit.code, declaration_emit.code);
356 }
357
358 #[test]
359 fn canonicalize_collapses_exact_duplicates_and_preserves_distinct_ones() {
360 let source_id = SourceId::new(0);
361 let code = DiagnosticCode::new("BAMTS-C001");
362 let base = Diagnostic::error(code, source_id, range(0, 1), "duplicate");
363 let duplicate = base.clone();
364 let as_warning = Diagnostic::warning(code, source_id, range(0, 1), "duplicate");
366 let elsewhere = Diagnostic::error(code, source_id, range(2, 3), "duplicate");
368 let other_code = Diagnostic::error(
370 DiagnosticCode::new("BAMTS-C002"),
371 source_id,
372 range(0, 1),
373 "duplicate",
374 );
375
376 let merged = canonicalize(vec![
377 base.clone(),
378 duplicate,
379 as_warning.clone(),
380 elsewhere.clone(),
381 other_code.clone(),
382 base.clone(),
383 ]);
384
385 assert_eq!(merged.len(), 4);
387 assert_eq!(merged.iter().filter(|d| **d == base).count(), 1);
388 assert!(merged.contains(&as_warning));
389 assert!(merged.contains(&elsewhere));
390 assert!(merged.contains(&other_code));
391 assert!(is_sorted_unique(&merged));
392 }
393
394 #[test]
395 fn frontend_output_never_contains_duplicate_diagnostics() {
396 let source =
397 "try {} catch (e) { e.message }\nconst n: number = \"oops\";\nconst bad: number =";
398 let output = compile_frontend(request(source, FrontendMode::JavaScript));
399 assert!(is_sorted_unique(output.diagnostics()));
400 }
401}