diff options
Diffstat (limited to 'cli/node/analyze.rs')
-rw-r--r-- | cli/node/analyze.rs | 161 |
1 files changed, 161 insertions, 0 deletions
diff --git a/cli/node/analyze.rs b/cli/node/analyze.rs new file mode 100644 index 000000000..9da2cbf4b --- /dev/null +++ b/cli/node/analyze.rs @@ -0,0 +1,161 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. + +use std::collections::HashSet; + +use deno_ast::swc::common::SyntaxContext; +use deno_ast::view::Node; +use deno_ast::view::NodeTrait; +use deno_ast::ModuleSpecifier; +use deno_ast::ParsedSource; +use deno_ast::SourceRanged; +use deno_core::error::AnyError; +use std::fmt::Write; + +static NODE_GLOBALS: &[&str] = &[ + "Buffer", + "clearImmediate", + "clearInterval", + "clearTimeout", + "global", + "process", + "setImmediate", + "setInterval", + "setTimeout", +]; + +// TODO(dsherret): this code is way more inefficient than it needs to be. +// +// In the future, we should disable capturing tokens & scope analysis +// and instead only use swc's APIs to go through the portions of the tree +// that we know will affect the global scope while still ensuring that +// `var` decls are taken into consideration. + +pub fn esm_code_with_node_globals( + specifier: &ModuleSpecifier, + code: String, +) -> Result<String, AnyError> { + let parsed_source = deno_ast::parse_program(deno_ast::ParseParams { + specifier: specifier.to_string(), + text_info: deno_ast::SourceTextInfo::from_string(code), + media_type: deno_ast::MediaType::from(specifier), + capture_tokens: true, + scope_analysis: true, + maybe_syntax: None, + })?; + let top_level_decls = analyze_top_level_decls(&parsed_source)?; + let mut globals = Vec::with_capacity(NODE_GLOBALS.len()); + let has_global_this = top_level_decls.contains("globalThis"); + for global in NODE_GLOBALS.iter() { + if !top_level_decls.contains(&global.to_string()) { + globals.push(*global); + } + } + + let mut result = String::new(); + let has_deno_decl = top_level_decls.contains("Deno"); + let global_this_expr = if has_deno_decl { + if top_level_decls.contains("window") { + // Will probably never happen, but if it does then we should consider + // creating an obscure global name to get this from. + panic!("The node esm module had a local `Deno` declaration and `window` declaration."); + } + // fallback to using `window.Deno` + "window.Deno[Deno.internal].node.globalThis" + } else { + "Deno[Deno.internal].node.globalThis" + }; + let global_this_expr = if has_global_this { + global_this_expr + } else { + write!(result, "var globalThis = {};", global_this_expr).unwrap(); + "globalThis" + }; + for global in globals { + write!(result, "var {0} = {1}.{0};", global, global_this_expr).unwrap(); + } + + result.push_str(parsed_source.text_info().text_str()); + + Ok(result) +} + +fn analyze_top_level_decls( + parsed_source: &ParsedSource, +) -> Result<HashSet<String>, AnyError> { + let top_level_context = parsed_source.top_level_context(); + + parsed_source.with_view(|program| { + let mut results = HashSet::new(); + visit_children(program.into(), top_level_context, &mut results); + Ok(results) + }) +} + +fn visit_children( + node: Node, + top_level_context: SyntaxContext, + results: &mut HashSet<String>, +) { + if let Node::Ident(ident) = node { + if ident.ctxt() == top_level_context && is_local_declaration_ident(node) { + results.insert(ident.sym().to_string()); + } + } + + for child in node.children() { + visit_children(child, top_level_context, results); + } +} + +fn is_local_declaration_ident(node: Node) -> bool { + if let Some(parent) = node.parent() { + match parent { + Node::BindingIdent(decl) => decl.id.range().contains(&node.range()), + Node::ClassDecl(decl) => decl.ident.range().contains(&node.range()), + Node::ClassExpr(decl) => decl + .ident + .as_ref() + .map(|i| i.range().contains(&node.range())) + .unwrap_or(false), + Node::TsInterfaceDecl(decl) => decl.id.range().contains(&node.range()), + Node::FnDecl(decl) => decl.ident.range().contains(&node.range()), + Node::FnExpr(decl) => decl + .ident + .as_ref() + .map(|i| i.range().contains(&node.range())) + .unwrap_or(false), + Node::TsModuleDecl(decl) => decl.id.range().contains(&node.range()), + Node::TsNamespaceDecl(decl) => decl.id.range().contains(&node.range()), + Node::VarDeclarator(decl) => decl.name.range().contains(&node.range()), + Node::ImportNamedSpecifier(decl) => { + decl.local.range().contains(&node.range()) + } + Node::ImportDefaultSpecifier(decl) => { + decl.local.range().contains(&node.range()) + } + Node::ImportStarAsSpecifier(decl) => decl.range().contains(&node.range()), + Node::KeyValuePatProp(decl) => decl.key.range().contains(&node.range()), + Node::AssignPatProp(decl) => decl.key.range().contains(&node.range()), + _ => false, + } + } else { + false + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_esm_code_with_node_globals() { + let r = esm_code_with_node_globals( + &ModuleSpecifier::parse("https://example.com/foo/bar.js").unwrap(), + "export const x = 1;".to_string(), + ) + .unwrap(); + assert!(r.contains("var globalThis = Deno[Deno.internal].node.globalThis;")); + assert!(r.contains("var process = globalThis.process;")); + assert!(r.contains("export const x = 1;")); + } +} |