runtime-checker 1.0.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/Cargo.lock +1972 -0
- package/Cargo.toml +46 -0
- package/LICENSE +29 -0
- package/README.md +26 -0
- package/data/mdn/README.md +30 -0
- package/data/mdn/bun.ron +1084 -0
- package/data/mdn/chrome.ron +7814 -0
- package/data/mdn/deno.ron +1497 -0
- package/data/mdn/firefox.ron +6452 -0
- package/data/mdn/node.ron +1311 -0
- package/data/mdn/safari.ron +6267 -0
- package/data/mdn-bcd.version +1 -0
- package/data/node.ron +3561 -0
- package/npm/bin/native/runtime-checker.exe +0 -0
- package/npm/bin/runtime-checker.js +23 -0
- package/npm/postinstall.js +29 -0
- package/npm/prepare-bin.js +11 -0
- package/package.json +42 -0
- package/src/analyzer.rs +405 -0
- package/src/bin/generate-mdn-data.rs +517 -0
- package/src/cli.rs +71 -0
- package/src/data.rs +266 -0
- package/src/engines.rs +154 -0
- package/src/help.rs +192 -0
- package/src/lib.rs +251 -0
- package/src/main.rs +12 -0
- package/src/report.rs +819 -0
- package/src/scanner.rs +404 -0
- package/src/version.rs +101 -0
|
Binary file
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
8
|
+
const exe = process.platform === "win32" ? "runtime-checker.exe" : "runtime-checker";
|
|
9
|
+
const native = join(root, "npm", "bin", "native", exe);
|
|
10
|
+
|
|
11
|
+
if (!existsSync(native)) {
|
|
12
|
+
console.error("runtime-checker native binary is missing. Try reinstalling the package.");
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const child = spawnSync(native, process.argv.slice(2), { stdio: "inherit" });
|
|
17
|
+
|
|
18
|
+
if (child.error) {
|
|
19
|
+
console.error(child.error.message);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
process.exit(child.status ?? 0);
|
|
@@ -0,0 +1,29 @@
|
|
|
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);
|
|
@@ -0,0 +1,11 @@
|
|
|
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/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "runtime-checker",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Detect minimum runtime versions required by JavaScript and TypeScript codebases.",
|
|
5
|
+
"license": "BSD-3-Clause",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/usebarekey/runtime-checker.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/usebarekey/runtime-checker",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/usebarekey/runtime-checker/issues"
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"runtime-checker": "./npm/bin/runtime-checker.js"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"Cargo.lock",
|
|
20
|
+
"Cargo.toml",
|
|
21
|
+
"LICENSE",
|
|
22
|
+
"README.md",
|
|
23
|
+
"data",
|
|
24
|
+
"npm",
|
|
25
|
+
"src"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "cargo build --release",
|
|
29
|
+
"check": "cargo clippy --all-targets -- -D warnings",
|
|
30
|
+
"test": "cargo test",
|
|
31
|
+
"postinstall": "node ./npm/postinstall.js",
|
|
32
|
+
"prepack": "cargo build --release && node ./npm/prepare-bin.js",
|
|
33
|
+
"publish:npm": "npm publish --access public --provenance",
|
|
34
|
+
"publish:jsr": "npx --yes jsr publish"
|
|
35
|
+
},
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/analyzer.rs
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
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
|
+
}
|