runtime-checker 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -26
- package/npm/bin/native/darwin-arm64/runtime-checker +0 -0
- package/npm/bin/native/darwin-x64/runtime-checker +0 -0
- package/npm/bin/native/linux-x64/runtime-checker +0 -0
- package/npm/bin/native/win32-x64/runtime-checker.exe +0 -0
- package/npm/bin/runtime-checker.js +7 -3
- package/package.json +5 -12
- package/Cargo.lock +0 -1972
- package/Cargo.toml +0 -46
- package/data/mdn/README.md +0 -30
- package/data/mdn/bun.ron +0 -1084
- package/data/mdn/chrome.ron +0 -7814
- package/data/mdn/deno.ron +0 -1497
- package/data/mdn/firefox.ron +0 -6452
- package/data/mdn/node.ron +0 -1311
- package/data/mdn/safari.ron +0 -6267
- package/data/mdn-bcd.version +0 -1
- package/data/node.ron +0 -3561
- package/npm/bin/native/runtime-checker.exe +0 -0
- package/npm/postinstall.js +0 -29
- package/npm/prepare-bin.js +0 -11
- package/src/analyzer.rs +0 -405
- package/src/bin/generate-mdn-data.rs +0 -517
- package/src/cli.rs +0 -71
- package/src/data.rs +0 -266
- package/src/engines.rs +0 -154
- package/src/help.rs +0 -192
- package/src/lib.rs +0 -251
- package/src/main.rs +0 -12
- package/src/report.rs +0 -819
- package/src/scanner.rs +0 -404
- package/src/version.rs +0 -101
|
Binary file
|
package/npm/postinstall.js
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
|
|
6
|
-
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
7
|
-
const exe = process.platform === "win32" ? "runtime-checker.exe" : "runtime-checker";
|
|
8
|
-
const native = join(root, "npm", "bin", "native", exe);
|
|
9
|
-
|
|
10
|
-
if (existsSync(native)) {
|
|
11
|
-
process.exit(0);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const cargo = spawnSync("cargo", ["build", "--release"], {
|
|
15
|
-
cwd: root,
|
|
16
|
-
stdio: "inherit",
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
if (cargo.status !== 0) {
|
|
20
|
-
console.error("Failed to build runtime-checker. Install Rust from https://rustup.rs/ and retry.");
|
|
21
|
-
process.exit(cargo.status ?? 1);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const prepare = spawnSync(process.execPath, ["./npm/prepare-bin.js"], {
|
|
25
|
-
cwd: root,
|
|
26
|
-
stdio: "inherit",
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
process.exit(prepare.status ?? 0);
|
package/npm/prepare-bin.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { copyFileSync, mkdirSync } from "node:fs";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
|
|
5
|
-
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
6
|
-
const exe = process.platform === "win32" ? "runtime-checker.exe" : "runtime-checker";
|
|
7
|
-
const source = join(root, "target", "release", exe);
|
|
8
|
-
const target = join(root, "npm", "bin", "native", exe);
|
|
9
|
-
|
|
10
|
-
mkdirSync(dirname(target), { recursive: true });
|
|
11
|
-
copyFileSync(source, target);
|
package/src/analyzer.rs
DELETED
|
@@ -1,405 +0,0 @@
|
|
|
1
|
-
use std::{
|
|
2
|
-
collections::{HashMap, HashSet},
|
|
3
|
-
path::Path,
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
use anyhow::{Context, Result};
|
|
7
|
-
use oxc_allocator::Allocator;
|
|
8
|
-
use oxc_ast::ast::{
|
|
9
|
-
Argument, BindingIdentifier, BindingPattern, BlockStatement, CallExpression, Expression,
|
|
10
|
-
Function, IdentifierReference, ImportDeclaration, ImportDeclarationSpecifier, ModuleExportName,
|
|
11
|
-
PropertyKey, StaticMemberExpression, VariableDeclarator,
|
|
12
|
-
};
|
|
13
|
-
use oxc_ast_visit::{Visit, walk};
|
|
14
|
-
use oxc_parser::Parser;
|
|
15
|
-
use oxc_semantic::{Semantic, SemanticBuilder};
|
|
16
|
-
use oxc_span::{SourceType, Span};
|
|
17
|
-
use oxc_syntax::scope::ScopeFlags;
|
|
18
|
-
|
|
19
|
-
use crate::{
|
|
20
|
-
data::{Feature, RuntimeDb},
|
|
21
|
-
scanner::{DetectedFeature, DetectionSeen, SourceFile, push_detection},
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
pub fn analyze_files_for_runtimes(
|
|
25
|
-
root: &Path,
|
|
26
|
-
files: &[SourceFile],
|
|
27
|
-
runtimes: &[&RuntimeDb],
|
|
28
|
-
) -> Result<Vec<Vec<DetectedFeature>>> {
|
|
29
|
-
let mut detections_by_runtime = vec![Vec::new(); runtimes.len()];
|
|
30
|
-
for file in files {
|
|
31
|
-
analyze_file_for_runtimes(file, runtimes, &mut detections_by_runtime)
|
|
32
|
-
.with_context(|| format!("failed to analyze {}", file.path.display()))?;
|
|
33
|
-
}
|
|
34
|
-
let _ = root;
|
|
35
|
-
Ok(detections_by_runtime)
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
fn analyze_file_for_runtimes(
|
|
39
|
-
file: &SourceFile,
|
|
40
|
-
runtimes: &[&RuntimeDb],
|
|
41
|
-
detections_by_runtime: &mut [Vec<DetectedFeature>],
|
|
42
|
-
) -> Result<()> {
|
|
43
|
-
let allocator = Allocator::default();
|
|
44
|
-
let source_type =
|
|
45
|
-
SourceType::from_path(&file.path).unwrap_or_else(|_| SourceType::unambiguous());
|
|
46
|
-
let parsed = Parser::new(&allocator, &file.text, source_type).parse();
|
|
47
|
-
if !parsed.errors.is_empty() {
|
|
48
|
-
return Ok(());
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
let semantic = SemanticBuilder::new_compiler().build(&parsed.program);
|
|
52
|
-
if !semantic.errors.is_empty() {
|
|
53
|
-
return Ok(());
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
let line_index = LineIndex::new(&file.text);
|
|
57
|
-
for (runtime, detections) in runtimes.iter().zip(detections_by_runtime.iter_mut()) {
|
|
58
|
-
let mut visitor = AstVisitor {
|
|
59
|
-
runtime,
|
|
60
|
-
semantic: &semantic.semantic,
|
|
61
|
-
line_index: &line_index,
|
|
62
|
-
path: file.path.as_path(),
|
|
63
|
-
namespace_imports: HashMap::new(),
|
|
64
|
-
named_imports: HashMap::new(),
|
|
65
|
-
local_scopes: vec![HashSet::new()],
|
|
66
|
-
detections: Vec::new(),
|
|
67
|
-
seen: HashSet::new(),
|
|
68
|
-
};
|
|
69
|
-
visitor.visit_program(&parsed.program);
|
|
70
|
-
detections.append(&mut visitor.detections);
|
|
71
|
-
}
|
|
72
|
-
Ok(())
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
struct AstVisitor<'a, 'db> {
|
|
76
|
-
runtime: &'db RuntimeDb,
|
|
77
|
-
semantic: &'a Semantic<'a>,
|
|
78
|
-
line_index: &'a LineIndex,
|
|
79
|
-
path: &'a Path,
|
|
80
|
-
namespace_imports: HashMap<String, String>,
|
|
81
|
-
named_imports: HashMap<String, String>,
|
|
82
|
-
local_scopes: Vec<HashSet<String>>,
|
|
83
|
-
detections: Vec<DetectedFeature>,
|
|
84
|
-
seen: DetectionSeen,
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
impl<'a> Visit<'a> for AstVisitor<'a, '_> {
|
|
88
|
-
fn visit_function(&mut self, function: &Function<'a>, flags: ScopeFlags) {
|
|
89
|
-
self.local_scopes.push(HashSet::new());
|
|
90
|
-
walk::walk_function(self, function, flags);
|
|
91
|
-
self.local_scopes.pop();
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
fn visit_block_statement(&mut self, block: &BlockStatement<'a>) {
|
|
95
|
-
self.local_scopes.push(HashSet::new());
|
|
96
|
-
walk::walk_block_statement(self, block);
|
|
97
|
-
self.local_scopes.pop();
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
fn visit_binding_identifier(&mut self, ident: &BindingIdentifier<'a>) {
|
|
101
|
-
if let Some(scope) = self.local_scopes.last_mut() {
|
|
102
|
-
scope.insert(ident.name.as_str().to_owned());
|
|
103
|
-
}
|
|
104
|
-
walk::walk_binding_identifier(self, ident);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
fn visit_import_declaration(&mut self, declaration: &ImportDeclaration<'a>) {
|
|
108
|
-
self.record_import(declaration);
|
|
109
|
-
walk::walk_import_declaration(self, declaration);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
fn visit_variable_declarator(&mut self, declarator: &VariableDeclarator<'a>) {
|
|
113
|
-
self.record_require(declarator);
|
|
114
|
-
walk::walk_variable_declarator(self, declarator);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
fn visit_identifier_reference(&mut self, ident: &IdentifierReference<'a>) {
|
|
118
|
-
let name = ident.name.as_str();
|
|
119
|
-
if !self.is_shadowed(name)
|
|
120
|
-
&& self.semantic.is_reference_to_global_variable(ident)
|
|
121
|
-
&& let Some(feature) = self.runtime.match_global(name)
|
|
122
|
-
{
|
|
123
|
-
self.emit(feature, ident.span);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if let Some(feature_name) = self.named_imports.get(name)
|
|
127
|
-
&& let Some(feature) = self.runtime.match_member_chain(feature_name)
|
|
128
|
-
{
|
|
129
|
-
self.emit(feature, ident.span);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
walk::walk_identifier_reference(self, ident);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
fn visit_static_member_expression(&mut self, member: &StaticMemberExpression<'a>) {
|
|
136
|
-
let property = member.property.name.as_str();
|
|
137
|
-
if let Some(feature) = self.runtime.match_property(property) {
|
|
138
|
-
self.emit(feature, member.property.span);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if let Some(chain) = member_chain(member) {
|
|
142
|
-
let chain = self.canonicalize_chain(chain);
|
|
143
|
-
if let Some(feature) = self.runtime.match_member_chain(&chain) {
|
|
144
|
-
self.emit(feature, member.span);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
walk::walk_static_member_expression(self, member);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
impl AstVisitor<'_, '_> {
|
|
153
|
-
fn record_import(&mut self, declaration: &ImportDeclaration<'_>) {
|
|
154
|
-
let Some(module) = normalize_module_name(declaration.source.value.as_str()) else {
|
|
155
|
-
return;
|
|
156
|
-
};
|
|
157
|
-
let Some(specifiers) = &declaration.specifiers else {
|
|
158
|
-
return;
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
for specifier in specifiers {
|
|
162
|
-
match specifier {
|
|
163
|
-
ImportDeclarationSpecifier::ImportNamespaceSpecifier(specifier) => {
|
|
164
|
-
self.namespace_imports
|
|
165
|
-
.insert(specifier.local.name.as_str().to_owned(), module.clone());
|
|
166
|
-
}
|
|
167
|
-
ImportDeclarationSpecifier::ImportSpecifier(specifier) => {
|
|
168
|
-
let Some(imported) = module_export_name(&specifier.imported) else {
|
|
169
|
-
continue;
|
|
170
|
-
};
|
|
171
|
-
let local = specifier.local.name.as_str().to_owned();
|
|
172
|
-
self.named_imports
|
|
173
|
-
.insert(local, format!("{module}.{imported}"));
|
|
174
|
-
}
|
|
175
|
-
ImportDeclarationSpecifier::ImportDefaultSpecifier(specifier) => {
|
|
176
|
-
self.namespace_imports
|
|
177
|
-
.insert(specifier.local.name.as_str().to_owned(), module.clone());
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
fn record_require(&mut self, declarator: &VariableDeclarator<'_>) {
|
|
184
|
-
let Some(init) = declarator.init.as_ref() else {
|
|
185
|
-
return;
|
|
186
|
-
};
|
|
187
|
-
let Some(module) = require_module(init) else {
|
|
188
|
-
return;
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
match &declarator.id {
|
|
192
|
-
BindingPattern::BindingIdentifier(binding) => {
|
|
193
|
-
self.namespace_imports
|
|
194
|
-
.insert(binding.name.as_str().to_owned(), module);
|
|
195
|
-
}
|
|
196
|
-
BindingPattern::ObjectPattern(pattern) => {
|
|
197
|
-
for property in &pattern.properties {
|
|
198
|
-
let Some(imported) = property_key_name(&property.key) else {
|
|
199
|
-
continue;
|
|
200
|
-
};
|
|
201
|
-
if let BindingPattern::BindingIdentifier(local) = &property.value {
|
|
202
|
-
self.named_imports.insert(
|
|
203
|
-
local.name.as_str().to_owned(),
|
|
204
|
-
format!("{module}.{imported}"),
|
|
205
|
-
);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
_ => {}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
fn canonicalize_chain(&self, parts: Vec<&str>) -> String {
|
|
214
|
-
let Some((root, tail)) = parts.split_first() else {
|
|
215
|
-
return String::new();
|
|
216
|
-
};
|
|
217
|
-
let root = self
|
|
218
|
-
.namespace_imports
|
|
219
|
-
.get(*root)
|
|
220
|
-
.map(String::as_str)
|
|
221
|
-
.unwrap_or(*root);
|
|
222
|
-
let capacity = root.len() + tail.iter().map(|part| part.len() + 1).sum::<usize>();
|
|
223
|
-
let mut chain = String::with_capacity(capacity);
|
|
224
|
-
chain.push_str(root);
|
|
225
|
-
for part in tail {
|
|
226
|
-
chain.push('.');
|
|
227
|
-
chain.push_str(part);
|
|
228
|
-
}
|
|
229
|
-
chain
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
fn is_shadowed(&self, name: &str) -> bool {
|
|
233
|
-
self.local_scopes
|
|
234
|
-
.iter()
|
|
235
|
-
.rev()
|
|
236
|
-
.any(|scope| scope.contains(name))
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
fn emit(&mut self, feature: &Feature, span: Span) {
|
|
240
|
-
let (line, column) = self.line_index.line_column(span.start as usize);
|
|
241
|
-
push_detection(
|
|
242
|
-
&mut self.detections,
|
|
243
|
-
&mut self.seen,
|
|
244
|
-
feature,
|
|
245
|
-
0,
|
|
246
|
-
self.path,
|
|
247
|
-
line,
|
|
248
|
-
column,
|
|
249
|
-
);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
fn member_chain<'a>(member: &'a StaticMemberExpression<'a>) -> Option<Vec<&'a str>> {
|
|
254
|
-
let mut parts = expression_chain(&member.object)?;
|
|
255
|
-
parts.push(member.property.name.as_str());
|
|
256
|
-
Some(parts)
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
fn expression_chain<'a>(expression: &'a Expression<'a>) -> Option<Vec<&'a str>> {
|
|
260
|
-
match expression {
|
|
261
|
-
Expression::Identifier(ident) => Some(vec![ident.name.as_str()]),
|
|
262
|
-
Expression::StaticMemberExpression(member) => member_chain(member),
|
|
263
|
-
Expression::ParenthesizedExpression(expression) => expression_chain(&expression.expression),
|
|
264
|
-
Expression::TSAsExpression(expression) => expression_chain(&expression.expression),
|
|
265
|
-
Expression::TSSatisfiesExpression(expression) => expression_chain(&expression.expression),
|
|
266
|
-
Expression::TSNonNullExpression(expression) => expression_chain(&expression.expression),
|
|
267
|
-
Expression::TSTypeAssertion(expression) => expression_chain(&expression.expression),
|
|
268
|
-
_ => None,
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
fn require_module(expression: &Expression<'_>) -> Option<String> {
|
|
273
|
-
let Expression::CallExpression(call) = expression else {
|
|
274
|
-
return None;
|
|
275
|
-
};
|
|
276
|
-
if !is_require_call(call) {
|
|
277
|
-
return None;
|
|
278
|
-
}
|
|
279
|
-
let first = call.arguments.first()?;
|
|
280
|
-
match first {
|
|
281
|
-
Argument::StringLiteral(literal) => normalize_module_name(literal.value.as_str()),
|
|
282
|
-
_ => None,
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
fn is_require_call(call: &CallExpression<'_>) -> bool {
|
|
287
|
-
matches!(&call.callee, Expression::Identifier(ident) if ident.name.as_str() == "require")
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
fn normalize_module_name(module: &str) -> Option<String> {
|
|
291
|
-
let module = module.strip_prefix("node:").unwrap_or(module);
|
|
292
|
-
match module {
|
|
293
|
-
"fs/promises" => Some("fsPromises".to_owned()),
|
|
294
|
-
"timers/promises" => Some("timersPromises".to_owned()),
|
|
295
|
-
value
|
|
296
|
-
if value
|
|
297
|
-
.chars()
|
|
298
|
-
.all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '/') =>
|
|
299
|
-
{
|
|
300
|
-
Some(value.replace('/', "."))
|
|
301
|
-
}
|
|
302
|
-
_ => None,
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
fn module_export_name(name: &ModuleExportName<'_>) -> Option<String> {
|
|
307
|
-
match name {
|
|
308
|
-
ModuleExportName::IdentifierName(name) => Some(name.name.as_str().to_owned()),
|
|
309
|
-
ModuleExportName::IdentifierReference(name) => Some(name.name.as_str().to_owned()),
|
|
310
|
-
ModuleExportName::StringLiteral(name) => Some(name.value.as_str().to_owned()),
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
fn property_key_name(key: &PropertyKey<'_>) -> Option<String> {
|
|
315
|
-
match key {
|
|
316
|
-
PropertyKey::StaticIdentifier(name) => Some(name.name.as_str().to_owned()),
|
|
317
|
-
PropertyKey::StringLiteral(name) => Some(name.value.as_str().to_owned()),
|
|
318
|
-
_ => None,
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
struct LineIndex {
|
|
323
|
-
starts: Vec<usize>,
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
impl LineIndex {
|
|
327
|
-
fn new(text: &str) -> Self {
|
|
328
|
-
let mut starts = vec![0];
|
|
329
|
-
for (index, byte) in text.bytes().enumerate() {
|
|
330
|
-
if byte == b'\n' {
|
|
331
|
-
starts.push(index + 1);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
Self { starts }
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
fn line_column(&self, offset: usize) -> (u64, u64) {
|
|
338
|
-
let line_index = self
|
|
339
|
-
.starts
|
|
340
|
-
.partition_point(|start| *start <= offset)
|
|
341
|
-
.saturating_sub(1);
|
|
342
|
-
let line_start = self.starts[line_index];
|
|
343
|
-
((line_index + 1) as u64, (offset - line_start + 1) as u64)
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
#[cfg(test)]
|
|
348
|
-
mod tests {
|
|
349
|
-
use std::{fs, path::Path};
|
|
350
|
-
|
|
351
|
-
use tempfile::tempdir;
|
|
352
|
-
|
|
353
|
-
use crate::{analyzer::analyze_files_for_runtimes, data::node_runtime, scanner::SourceFile};
|
|
354
|
-
|
|
355
|
-
fn analyze(path: &Path, text: &str) -> Vec<String> {
|
|
356
|
-
let runtime = node_runtime().unwrap();
|
|
357
|
-
let files = vec![SourceFile {
|
|
358
|
-
path: path.to_path_buf(),
|
|
359
|
-
text: text.to_owned(),
|
|
360
|
-
}];
|
|
361
|
-
analyze_files_for_runtimes(path.parent().unwrap(), &files, &[runtime])
|
|
362
|
-
.unwrap()
|
|
363
|
-
.pop()
|
|
364
|
-
.unwrap()
|
|
365
|
-
.into_iter()
|
|
366
|
-
.map(|detection| detection.feature)
|
|
367
|
-
.collect()
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
#[test]
|
|
371
|
-
fn detects_global_temporal_but_not_local_binding() {
|
|
372
|
-
let dir = tempdir().unwrap();
|
|
373
|
-
let path = dir.path().join("date.ts");
|
|
374
|
-
fs::write(
|
|
375
|
-
&path,
|
|
376
|
-
"Temporal.Now.instant();\nfunction shadow() { const Temporal = local;\nTemporal.Now.instant();\n}",
|
|
377
|
-
)
|
|
378
|
-
.unwrap();
|
|
379
|
-
|
|
380
|
-
let detections = analyze(&path, &fs::read_to_string(&path).unwrap());
|
|
381
|
-
assert!(detections.iter().any(|feature| feature == "Temporal"));
|
|
382
|
-
assert_eq!(
|
|
383
|
-
detections
|
|
384
|
-
.iter()
|
|
385
|
-
.filter(|feature| feature.as_str() == "Temporal")
|
|
386
|
-
.count(),
|
|
387
|
-
1
|
|
388
|
-
);
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
#[test]
|
|
392
|
-
fn detects_imported_fs_members() {
|
|
393
|
-
let dir = tempdir().unwrap();
|
|
394
|
-
let path = dir.path().join("fs.ts");
|
|
395
|
-
fs::write(
|
|
396
|
-
&path,
|
|
397
|
-
"import * as fs from 'node:fs';\nimport { cp } from 'node:fs/promises';\nfs.cp('a', 'b', () => {});\ncp('a', 'b');",
|
|
398
|
-
)
|
|
399
|
-
.unwrap();
|
|
400
|
-
|
|
401
|
-
let detections = analyze(&path, &fs::read_to_string(&path).unwrap());
|
|
402
|
-
assert!(detections.iter().any(|feature| feature == "fs.cp"));
|
|
403
|
-
assert!(detections.iter().any(|feature| feature == "fsPromises.cp"));
|
|
404
|
-
}
|
|
405
|
-
}
|