/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
libs/resolver/emit.rs
772 строки
23 KB
crowlbot
fix: don't print duplicate parse diagnostics (#35933)
10 июл 2026, 22:02
Не верифицирован
10 июл 2026, 22:02
ddebb90
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. use std::hash::Hash; use std::hash::Hasher; use anyhow::Error as AnyError; use deno_ast::EmittedSourceText; use deno_ast::ModuleKind; use deno_ast::ParsedSource; use deno_ast::SourceMapOption; use deno_ast::SourceRange; use deno_ast::SourceRanged; use deno_ast::SourceRangedForSpanned; use deno_ast::TranspileModuleOptions; use deno_ast::TranspileResult; use deno_ast::swc::ast::Decorator; use deno_ast::swc::ecma_visit::Visit; use deno_ast::swc::ecma_visit::VisitWith; use deno_ast::swc::ecma_visit::noop_visit_type; use deno_error::JsErrorBox; use deno_graph::MediaType; use deno_graph::Module; use deno_graph::ModuleGraph; use deno_maybe_sync::MaybeSend; use deno_maybe_sync::MaybeSync; use futures::FutureExt; use futures::StreamExt; use futures::stream::FuturesUnordered; use node_resolver::InNpmPackageChecker; use url::Url; use crate::cache::EmitCacheRc; use crate::cache::EmitCacheSys; use crate::cache::ParsedSourceCacheRc; use crate::cjs::CjsTrackerRc; use crate::deno_json::CompilerOptionsParseError; use crate::deno_json::CompilerOptionsResolverRc; use crate::deno_json::TranspileAndEmitOptions; #[allow( clippy::disallowed_types, reason = "source text is always stored as Arc<str>" )] type ArcStr = std::sync::Arc<str>; #[allow(clippy::disallowed_types, reason = "definition")] pub type EmitterRc<TInNpmPackageChecker, TSys> = deno_maybe_sync::MaybeArc<Emitter<TInNpmPackageChecker, TSys>>; #[sys_traits::auto_impl] pub trait EmitterSys: EmitCacheSys {} #[derive(Debug)] pub struct Emitter<TInNpmPackageChecker: InNpmPackageChecker, TSys: EmitterSys> { cjs_tracker: CjsTrackerRc<TInNpmPackageChecker, TSys>, emit_cache: EmitCacheRc<TSys>, parsed_source_cache: ParsedSourceCacheRc, compiler_options_resolver: CompilerOptionsResolverRc, } impl<TInNpmPackageChecker: InNpmPackageChecker, TSys: EmitterSys> Emitter<TInNpmPackageChecker, TSys> { pub fn new( cjs_tracker: CjsTrackerRc<TInNpmPackageChecker, TSys>, emit_cache: EmitCacheRc<TSys>, parsed_source_cache: ParsedSourceCacheRc, compiler_options_resolver: CompilerOptionsResolverRc, ) -> Self { Self { cjs_tracker, emit_cache, parsed_source_cache, compiler_options_resolver, } } pub async fn cache_module_emits( &self, graph: &ModuleGraph, ) -> Result<(), AnyError> { let mut futures = FuturesUnordered::new(); for module in graph.modules() { let Module::Js(module) = module else { continue; }; if module.media_type.is_emittable() { futures.push( self .maybe_emit_source( &module.specifier, module.media_type, ModuleKind::from_is_cjs( self.cjs_tracker.is_cjs_with_known_is_script( &module.specifier, module.media_type, module.is_script, )?, ), &module.source.text, ) .boxed_local(), ); } } while let Some(result) = futures.next().await { result?; // surface errors } Ok(()) } /// Gets a cached emit if the source matches the hash found in the cache. pub fn maybe_cached_emit( &self, specifier: &Url, module_kind: deno_ast::ModuleKind, source: &str, ) -> Result<Option<String>, AnyError> { let transpile_and_emit_options = self .compiler_options_resolver .for_specifier(specifier) .transpile_options()?; let source_hash = self.get_source_hash(module_kind, transpile_and_emit_options, source); Ok(self.emit_cache.get_emit_code(specifier, source_hash)) } pub async fn maybe_emit_source( &self, specifier: &Url, media_type: MediaType, module_kind: ModuleKind, source: &ArcStr, ) -> Result<ArcStr, EmitParsedSourceHelperError> { self .maybe_emit_parsed_source_provider( ParsedSourceCacheParsedSourceProvider { parsed_source_cache: self.parsed_source_cache.clone(), specifier: specifier.clone(), media_type, source: source.clone(), }, module_kind, ) .await } pub async fn maybe_emit_parsed_source( &self, parsed_source: deno_ast::ParsedSource, module_kind: ModuleKind, ) -> Result<ArcStr, EmitParsedSourceHelperError> { // note: this method is used in deno-js-loader self .maybe_emit_parsed_source_provider(parsed_source, module_kind) .await } async fn maybe_emit_parsed_source_provider< TProvider: ParsedSourceProvider, >( &self, provider: TProvider, module_kind: ModuleKind, ) -> Result<ArcStr, EmitParsedSourceHelperError> { // Note: keep this in sync with the sync version below if !provider.media_type().is_emittable() { return Ok(provider.into_source()); } let transpile_and_emit_options = self .compiler_options_resolver .for_specifier(provider.specifier()) .transpile_options()?; if transpile_and_emit_options.no_transpile { return Ok(provider.into_source()); } let transpile_options = &transpile_and_emit_options.transpile; if matches!(provider.media_type(), MediaType::Jsx) && transpile_options.jsx.is_none() { // jsx disabled, so skip return Ok(provider.into_source()); } let helper = EmitParsedSourceHelper(self); match helper.pre_emit_parsed_source( provider.specifier(), module_kind, transpile_and_emit_options, provider.source(), ) { PreEmitResult::Cached(emitted_text) => Ok(emitted_text.into()), PreEmitResult::NotCached { source_hash } => { let specifier = provider.specifier().clone(); let emit = { let transpile_and_emit_options = transpile_and_emit_options.clone(); move || { let parsed_source = provider.parsed_source()?; transpile( parsed_source, module_kind, &transpile_and_emit_options.transpile, &transpile_and_emit_options.emit, ) .map(|r| r.text) } }; #[cfg(feature = "sync")] let transpiled_source = crate::rt::spawn_blocking(emit).await.unwrap()?; #[cfg(not(feature = "sync"))] let transpiled_source = emit()?; helper.post_emit_parsed_source( &specifier, &transpiled_source, source_hash, ); Ok(transpiled_source.into()) } } } #[allow( clippy::result_large_err, reason = "EmitParsedSourceHelperError is intentionally large" )] pub fn maybe_emit_source_sync( &self, specifier: &Url, media_type: MediaType, module_kind: deno_ast::ModuleKind, source: &ArcStr, ) -> Result<ArcStr, EmitParsedSourceHelperError> { // Note: keep this in sync with the async version above if !media_type.is_emittable() { return Ok(source.clone()); } let transpile_and_emit_options = self .compiler_options_resolver .for_specifier(specifier) .transpile_options()?; if transpile_and_emit_options.no_transpile { return Ok(source.clone()); } let transpile_options = &transpile_and_emit_options.transpile; if matches!(media_type, MediaType::Jsx) && transpile_options.jsx.is_none() { // jsx disabled, so skip return Ok(source.clone()); } let helper = EmitParsedSourceHelper(self); match helper.pre_emit_parsed_source( specifier, module_kind, transpile_and_emit_options, source, ) { PreEmitResult::Cached(emitted_text) => Ok(emitted_text.into()), PreEmitResult::NotCached { source_hash } => { let parsed_source = self.parsed_source_cache.remove_or_parse_module( specifier, media_type, source.clone(), )?; let transpiled_source = transpile( parsed_source, module_kind, &transpile_and_emit_options.transpile, &transpile_and_emit_options.emit, )? .text; helper.post_emit_parsed_source( specifier, &transpiled_source, source_hash, ); Ok(transpiled_source.into()) } } } pub fn emit_source_for_deno_compile( &self, specifier: &Url, media_type: MediaType, module_kind: deno_ast::ModuleKind, source: &ArcStr, ) -> Result<(String, String), AnyError> { let transpile_and_emit_options = self .compiler_options_resolver .for_specifier(specifier) .transpile_options()?; let mut emit_options = transpile_and_emit_options.emit.clone(); emit_options.inline_sources = false; emit_options.source_map = SourceMapOption::Separate; // strip off the path to have more deterministic builds as we don't care // about the source name because we manually provide the source map to v8 emit_options.source_map_base = Some(deno_path_util::url_parent(specifier)); let parsed_source = self.parsed_source_cache.remove_or_parse_module( specifier, media_type, source.clone(), )?; let source = transpile( parsed_source, module_kind, &transpile_and_emit_options.transpile, &emit_options, )?; Ok((source.text, source.source_map.unwrap())) } /// Expects a file URL, panics otherwise. pub fn emit_for_hmr( &self, specifier: &Url, source_code: String, ) -> Result<String, JsErrorBox> { let media_type = MediaType::from_specifier(specifier); match media_type { MediaType::TypeScript | MediaType::Mts | MediaType::Cts | MediaType::Jsx | MediaType::Tsx => { let source_arc: ArcStr = source_code.into(); let parsed_source = self .parsed_source_cache .remove_or_parse_module(specifier, media_type, source_arc) .map_err(JsErrorBox::from_err)?; // HMR doesn't work with embedded source maps for some reason, so set // the option to not use them (though you should test this out because // this statement is probably wrong) let transpile_and_emit_options = self .compiler_options_resolver .for_specifier(specifier) .transpile_options() .map_err(JsErrorBox::from_err)?; let mut options = transpile_and_emit_options.emit.clone(); options.source_map = SourceMapOption::None; let is_cjs = self .cjs_tracker .is_cjs_with_known_is_script( specifier, media_type, parsed_source.compute_is_script(), ) .map_err(JsErrorBox::from_err)?; let transpiled_source = parsed_source .transpile( &transpile_and_emit_options.transpile, &deno_ast::TranspileModuleOptions { module_kind: Some(ModuleKind::from_is_cjs(is_cjs)), }, &options, ) .map_err(dedupe_transpile_error) .map_err(JsErrorBox::from_err)? .into_source(); Ok(transpiled_source.text) } MediaType::JavaScript | MediaType::Mjs | MediaType::Cjs | MediaType::Dts | MediaType::Dmts | MediaType::Dcts | MediaType::Json | MediaType::Jsonc | MediaType::Json5 | MediaType::Markdown | MediaType::Wasm | MediaType::Css | MediaType::Html | MediaType::SourceMap | MediaType::Sql | MediaType::Unknown => { // clear this specifier from the parsed source cache as it's now out of date self.parsed_source_cache.free(specifier); Ok(source_code) } } } /// A hashing function that takes the source code and uses the global emit /// options then generates a string hash which can be stored to /// determine if the cached emit is valid or not. fn get_source_hash( &self, module_kind: ModuleKind, transpile_and_emit: &TranspileAndEmitOptions, source_text: &str, ) -> u64 { let mut hasher = twox_hash::XxHash64::default(); source_text.hash(&mut hasher); transpile_and_emit.pre_computed_hash.hash(&mut hasher); module_kind.hash(&mut hasher); hasher.finish() } } #[allow( clippy::result_large_err, reason = "EmitParsedSourceHelperError is intentionally large" )] trait ParsedSourceProvider: MaybeSend + MaybeSync + Clone + 'static { fn specifier(&self) -> &Url; fn media_type(&self) -> MediaType; fn source(&self) -> &ArcStr; fn into_source(self) -> ArcStr; fn parsed_source(self) -> Result<ParsedSource, deno_ast::ParseDiagnostic>; } #[derive(Clone)] struct ParsedSourceCacheParsedSourceProvider { parsed_source_cache: ParsedSourceCacheRc, specifier: Url, media_type: MediaType, source: ArcStr, } impl ParsedSourceProvider for ParsedSourceCacheParsedSourceProvider { fn specifier(&self) -> &Url { &self.specifier } fn media_type(&self) -> MediaType { self.media_type } fn source(&self) -> &ArcStr { &self.source } fn into_source(self) -> ArcStr { self.source } fn parsed_source(self) -> Result<ParsedSource, deno_ast::ParseDiagnostic> { self.parsed_source_cache.remove_or_parse_module( &self.specifier, self.media_type, self.source.clone(), ) } } impl ParsedSourceProvider for ParsedSource { fn specifier(&self) -> &Url { ParsedSource::specifier(self) } fn media_type(&self) -> MediaType { ParsedSource::media_type(self) } fn source(&self) -> &ArcStr { self.text() } fn into_source(self) -> ArcStr { self.text().clone() } fn parsed_source(self) -> Result<ParsedSource, deno_ast::ParseDiagnostic> { Ok(self) } } enum PreEmitResult { Cached(String), NotCached { source_hash: u64 }, } #[derive(Debug, thiserror::Error, deno_error::JsError)] pub enum EmitParsedSourceHelperError { #[class(inherit)] #[error(transparent)] CompilerOptionsParse(#[from] CompilerOptionsParseError), #[class(inherit)] #[error(transparent)] ParseDiagnostic(#[from] deno_ast::ParseDiagnostic), #[class(inherit)] #[error(transparent)] Transpile(#[from] deno_ast::TranspileError), #[class(inherit)] #[error(transparent)] Other(#[from] JsErrorBox), } /// Helper to share code between async and sync emit_parsed_source methods. struct EmitParsedSourceHelper< 'a, TInNpmPackageChecker: InNpmPackageChecker, TSys: EmitterSys, >(&'a Emitter<TInNpmPackageChecker, TSys>); impl<TInNpmPackageChecker: InNpmPackageChecker, TSys: EmitterSys> EmitParsedSourceHelper<'_, TInNpmPackageChecker, TSys> { pub fn pre_emit_parsed_source( &self, specifier: &Url, module_kind: deno_ast::ModuleKind, transpile_and_emit_options: &TranspileAndEmitOptions, source: &ArcStr, ) -> PreEmitResult { let source_hash = self .0 .get_source_hash(module_kind, transpile_and_emit_options, source); if let Some(emit_code) = self.0.emit_cache.get_emit_code(specifier, source_hash) { PreEmitResult::Cached(emit_code) } else { PreEmitResult::NotCached { source_hash } } } pub fn post_emit_parsed_source( &self, specifier: &Url, transpiled_source: &str, source_hash: u64, ) { self.0.emit_cache.set_emit_code( specifier, source_hash, transpiled_source.as_bytes(), ); } } #[allow( clippy::result_large_err, reason = "EmitParsedSourceHelperError is intentionally large" )] fn transpile( parsed_source: ParsedSource, module_kind: deno_ast::ModuleKind, transpile_options: &deno_ast::TranspileOptions, emit_options: &deno_ast::EmitOptions, ) -> Result<EmittedSourceText, EmitParsedSourceHelperError> { ensure_no_import_assertion(&parsed_source)?; if let Some(diagnostics) = invalid_syntax_parse_diagnostics(&parsed_source) { return Err(deno_ast::TranspileError::ParseErrors(diagnostics).into()); } // Skip the decorator transform when the module has no decorators. The // swc decorator pass otherwise hoists every computed class-member key into // a `var _computedKey; _computedKey = ...;` pair at module scope, which // looks like a side effect and blocks downstream tree-shaking (e.g. in // `deno bundle`). See denoland/deno#30817. let owned_options; let transpile_options = if !matches!( transpile_options.decorators, deno_ast::DecoratorsTranspileOption::None ) && !program_has_decorators( parsed_source.program_ref(), ) { owned_options = deno_ast::TranspileOptions { decorators: deno_ast::DecoratorsTranspileOption::None, ..transpile_options.clone() }; &owned_options } else { transpile_options }; let transpile_result = parsed_source .transpile( transpile_options, &TranspileModuleOptions { module_kind: Some(module_kind), }, emit_options, ) .map_err(dedupe_transpile_error)?; let mut transpiled_source = match transpile_result { TranspileResult::Owned(source) => source, TranspileResult::Cloned(source) => { debug_assert!(false, "Transpile owned failed."); source } }; patch_public_decorator_access_has(&mut transpiled_source.text); Ok(transpiled_source) } pub fn patch_public_decorator_access_has(source: &mut String) { if !source.contains("_apply_decs_2203_r") { return; } const OLD_EMITTED_ACCESS_OBJECT: &str = concat!( " ctx.access = get && set ? {\n", " get: get,\n", " set: set\n", " } : get ? {\n", " get: get\n", " } : {\n", " set: set\n", " };\n", ); const NEW_EMITTED_ACCESS_OBJECT: &str = concat!( " if (isPrivate) {\n", " ctx.access = get && set ? { get: get, set: set } : get ? { get: get } : { set: set };\n", " } else {\n", " if (get) { var originalGet = get; get = function(target) { if (arguments.length === 0) target = this; return originalGet.call(target); }; }\n", " if (set) { var originalSet = set; set = function(target, value) { if (arguments.length === 1) { value = target; target = this; } return originalSet.call(target, value); }; }\n", " var has = function(target) { return name in target; };\n", " ctx.access = get && set ? { has: has, get: get, set: set } : get ? { has: has, get: get } : { has: has, set: set };\n", " }\n", ); *source = source.replace(OLD_EMITTED_ACCESS_OBJECT, NEW_EMITTED_ACCESS_OBJECT); } fn program_has_decorators(program: deno_ast::ProgramRef<'_>) -> bool { #[derive(Default)] struct DecoratorDetector { found: bool, } impl Visit for DecoratorDetector { noop_visit_type!(); fn visit_decorator(&mut self, _: &Decorator) { self.found = true; } } let mut detector = DecoratorDetector::default(); program.visit_with(&mut detector); detector.found } /// When `swc` recovers from a syntax error it leaves an `Invalid` placeholder /// node in the AST. The code generator emits these as the literal text /// `<invalid>`, which then surfaces downstream as a misleading /// `Uncaught SyntaxError: Unexpected token '<'` once the emitted output is /// executed. If any such node is present, return the (otherwise non-fatal) /// parse diagnostics so a precise syntax error can be reported instead of /// emitting broken JavaScript. See denoland/deno#19457. pub fn invalid_syntax_parse_diagnostics( parsed_source: &ParsedSource, ) -> Option<deno_ast::ParseDiagnosticsError> { let diagnostics = parsed_source.diagnostics(); // Fast path: well-formed sources have no recovered-from diagnostics, and // `swc` only ever inserts an `Invalid` node alongside one, so there's no // need to walk the AST. if diagnostics.is_empty() { return None; } #[derive(Default)] struct InvalidNodeDetector { found: bool, } impl Visit for InvalidNodeDetector { noop_visit_type!(); fn visit_invalid(&mut self, _: &deno_ast::swc::ast::Invalid) { self.found = true; } } let mut detector = InvalidNodeDetector::default(); parsed_source.program_ref().visit_with(&mut detector); if !detector.found { return None; } Some(deno_ast::ParseDiagnosticsError(diagnostics.clone())) } /// `swc` sometimes reports the exact same fatal parse diagnostic more than /// once (ex. `for (console.log("a") of [1]);`), which would otherwise be shown /// to the user as a duplicated error message. Collapse identical diagnostics /// while keeping the original order. See denoland/deno#27804. pub fn dedupe_transpile_error( err: deno_ast::TranspileError, ) -> deno_ast::TranspileError { fn dedupe( diagnostics: deno_ast::ParseDiagnosticsError, ) -> deno_ast::ParseDiagnosticsError { let mut deduped = Vec::with_capacity(diagnostics.0.len()); for diagnostic in diagnostics.0 { if !deduped.contains(&diagnostic) { deduped.push(diagnostic); } } deno_ast::ParseDiagnosticsError(deduped) } match err { deno_ast::TranspileError::ParseErrors(diagnostics) => { deno_ast::TranspileError::ParseErrors(dedupe(diagnostics)) } deno_ast::TranspileError::FoldProgram( deno_ast::FoldProgramError::ParseDiagnostics(diagnostics), ) => deno_ast::TranspileError::FoldProgram( deno_ast::FoldProgramError::ParseDiagnostics(dedupe(diagnostics)), ), err => err, } } // todo(dsherret): this is a temporary measure until we have swc erroring for this fn ensure_no_import_assertion( parsed_source: &deno_ast::ParsedSource, ) -> Result<(), JsErrorBox> { fn has_import_assertion(text: &str) -> bool { // good enough text.contains(" assert ") && !text.contains(" with ") } fn create_err( parsed_source: &deno_ast::ParsedSource, range: SourceRange, ) -> JsErrorBox { let text_info = parsed_source.text_info_lazy(); let loc = text_info.line_and_column_display(range.start); let mut msg = "Import assertions are deprecated. Use `with` keyword, instead of 'assert' keyword.".to_string(); msg.push_str("\n\n"); msg.push_str(range.text_fast(text_info)); msg.push_str("\n\n"); msg.push_str(&format!( " at {}:{}:{}\n", parsed_source.specifier(), loc.line_number, loc.column_number, )); JsErrorBox::generic(msg) } let deno_ast::ProgramRef::Module(module) = parsed_source.program_ref() else { return Ok(()); }; for item in &module.body { match item { deno_ast::swc::ast::ModuleItem::ModuleDecl(decl) => match decl { deno_ast::swc::ast::ModuleDecl::Import(n) => { if n.with.is_some() && has_import_assertion(n.text_fast(parsed_source.text_info_lazy())) { return Err(create_err(parsed_source, n.range())); } } deno_ast::swc::ast::ModuleDecl::ExportAll(n) => { if n.with.is_some() && has_import_assertion(n.text_fast(parsed_source.text_info_lazy())) { return Err(create_err(parsed_source, n.range())); } } deno_ast::swc::ast::ModuleDecl::ExportNamed(n) => { if n.with.is_some() && has_import_assertion(n.text_fast(parsed_source.text_info_lazy())) { return Err(create_err(parsed_source, n.range())); } } deno_ast::swc::ast::ModuleDecl::ExportDecl(_) | deno_ast::swc::ast::ModuleDecl::ExportDefaultDecl(_) | deno_ast::swc::ast::ModuleDecl::ExportDefaultExpr(_) | deno_ast::swc::ast::ModuleDecl::TsImportEquals(_) | deno_ast::swc::ast::ModuleDecl::TsExportAssignment(_) | deno_ast::swc::ast::ModuleDecl::TsNamespaceExport(_) => {} }, deno_ast::swc::ast::ModuleItem::Stmt(_) => {} } } Ok(()) }