summaryrefslogtreecommitdiff
path: root/cli/node/analyze.rs
diff options
context:
space:
mode:
Diffstat (limited to 'cli/node/analyze.rs')
-rw-r--r--cli/node/analyze.rs161
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;"));
+ }
+}