166 lines
4.4 KiB
Rust
166 lines
4.4 KiB
Rust
//! Parse extension modules and reject free runtime identifiers outside the world runtime globals.
|
|
use deno_ast::swc::{
|
|
ast::{Ident, TsInterfaceDecl, TsType},
|
|
common::SyntaxContext,
|
|
ecma_visit::{Visit, VisitWith},
|
|
};
|
|
use deno_ast::{MediaType, ParseParams, ProgramRef, parse_module};
|
|
use std::path::Path;
|
|
|
|
// Standard JavaScript globals. World extensions also see Deno.
|
|
const GLOBALS: &[&str] = &[
|
|
"undefined",
|
|
"globalThis",
|
|
"Deno",
|
|
"console",
|
|
"crypto",
|
|
"fetch",
|
|
"performance",
|
|
"setTimeout",
|
|
"clearTimeout",
|
|
"setInterval",
|
|
"clearInterval",
|
|
"queueMicrotask",
|
|
"structuredClone",
|
|
"atob",
|
|
"btoa",
|
|
"parseInt",
|
|
"parseFloat",
|
|
"isNaN",
|
|
"Infinity",
|
|
"NaN",
|
|
"Object",
|
|
"Function",
|
|
"Boolean",
|
|
"Number",
|
|
"BigInt",
|
|
"String",
|
|
"Symbol",
|
|
"Array",
|
|
"Map",
|
|
"Set",
|
|
"WeakMap",
|
|
"WeakSet",
|
|
"WeakRef",
|
|
"FinalizationRegistry",
|
|
"Promise",
|
|
"Date",
|
|
"RegExp",
|
|
"Error",
|
|
"TypeError",
|
|
"RangeError",
|
|
"SyntaxError",
|
|
"AggregateError",
|
|
"JSON",
|
|
"Math",
|
|
"Reflect",
|
|
"Proxy",
|
|
"Intl",
|
|
"Iterator",
|
|
"Uint8Array",
|
|
"ArrayBuffer",
|
|
"TextEncoder",
|
|
"TextDecoder",
|
|
"URL",
|
|
"URLSearchParams",
|
|
"AbortController",
|
|
"AbortSignal",
|
|
"Request",
|
|
"Response",
|
|
"Headers",
|
|
"ReadableStream",
|
|
"WritableStream",
|
|
"TransformStream",
|
|
"Blob",
|
|
"File",
|
|
"FormData",
|
|
"DOMException",
|
|
"encodeURIComponent",
|
|
"decodeURIComponent",
|
|
];
|
|
fn undefined(source: &str) -> Vec<String> {
|
|
let parsed = parse_module(ParseParams {
|
|
specifier: "file:///extension.ts".parse().unwrap(),
|
|
text: source.into(),
|
|
media_type: MediaType::TypeScript,
|
|
capture_tokens: false,
|
|
scope_analysis: true,
|
|
maybe_syntax: None,
|
|
})
|
|
.unwrap();
|
|
struct FreeNames {
|
|
context: SyntaxContext,
|
|
names: Vec<String>,
|
|
}
|
|
impl Visit for FreeNames {
|
|
// Type signatures contain parameter names but do not execute.
|
|
fn visit_ts_type(&mut self, _node: &TsType) {}
|
|
fn visit_ts_interface_decl(&mut self, _node: &TsInterfaceDecl) {}
|
|
fn visit_ident(&mut self, ident: &Ident) {
|
|
if ident.ctxt == self.context && !GLOBALS.contains(&ident.sym.as_ref()) {
|
|
self.names.push(ident.sym.to_string());
|
|
}
|
|
}
|
|
}
|
|
let mut free = FreeNames {
|
|
context: parsed.unresolved_context(),
|
|
names: Vec::new(),
|
|
};
|
|
match parsed.program_ref() {
|
|
ProgramRef::Module(module) => module.visit_with(&mut free),
|
|
ProgramRef::Script(script) => script.visit_with(&mut free),
|
|
}
|
|
free.names.sort();
|
|
free.names.dedup();
|
|
free.names
|
|
}
|
|
fn scan(directory: &Path, failures: &mut Vec<String>) {
|
|
for entry in std::fs::read_dir(directory).unwrap() {
|
|
let path = entry.unwrap().path();
|
|
if path.is_dir() {
|
|
scan(&path, failures);
|
|
} else if path.extension().is_some_and(|ext| ext == "ts")
|
|
&& !path.to_string_lossy().ends_with(".test.ts")
|
|
{
|
|
let names = undefined(&std::fs::read_to_string(&path).unwrap());
|
|
if !names.is_empty() {
|
|
failures.push(format!("{}: {}", path.display(), names.join(", ")));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#[test]
|
|
fn extension_identifiers_are_defined() {
|
|
let mut failures = Vec::new();
|
|
scan(
|
|
&Path::new(env!("CARGO_MANIFEST_DIR")).join("../extensions"),
|
|
&mut failures,
|
|
);
|
|
assert!(
|
|
failures.is_empty(),
|
|
"Undefined identifiers:\n{}",
|
|
failures.join("\n")
|
|
);
|
|
}
|
|
#[test]
|
|
fn catches_the_missing_route_import() {
|
|
let source = std::fs::read_to_string(
|
|
Path::new(env!("CARGO_MANIFEST_DIR")).join("../extensions/messages/route.ts"),
|
|
)
|
|
.unwrap();
|
|
let old = source.replace(
|
|
"import { ConversationsApi } from '../conversations/api.ts'\n",
|
|
"",
|
|
);
|
|
assert!(
|
|
undefined(&old)
|
|
.iter()
|
|
.any(|name| name == "ConversationsApi")
|
|
);
|
|
assert!(undefined("import { Api } from 'service'; type Callback = (arg: UnknownType) => void; interface Service { use(parameter: UnknownType): void }; function use(host: any) { const local = Api; return host.need(local) }").is_empty());
|
|
assert_eq!(
|
|
undefined("function use(host: any) { return host.need(MissingApi) }"),
|
|
["MissingApi"]
|
|
);
|
|
}
|