deno/cli/node/analyze.rs
David Sherret 87f80ff6be
feat(unstable): initial support for npm specifiers (#15484)
Co-authored-by: Bartek Iwańczuk <biwanczuk@gmail.com>
2022-08-20 11:31:33 -04:00

162 lines
5 KiB
Rust

// 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;"));
}
}