Files
sausage/host/tests/it/extension_names.rs
T

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