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/src/lib.rs ADDED
@@ -0,0 +1,251 @@
1
+ mod analyzer;
2
+ mod cli;
3
+ mod data;
4
+ mod engines;
5
+ mod help;
6
+ mod report;
7
+ mod scanner;
8
+ mod version;
9
+
10
+ use std::{collections::HashMap, time::Instant};
11
+
12
+ use anyhow::{Context, Result};
13
+
14
+ #[global_allocator]
15
+ static GLOBAL_ALLOCATOR: mimalloc::MiMalloc = mimalloc::MiMalloc;
16
+
17
+ pub use cli::{Cli, RuntimeKind};
18
+ use data::runtime;
19
+ use engines::check_engines;
20
+ pub use help::print_help;
21
+ use report::{ParserMode, Reporter, RuntimeReport};
22
+ use scanner::{DetectedFeature, FffMultiRuntimeScanner, Scanner, SourceDiscovery, SourceScan};
23
+
24
+ pub fn run(cli: Cli) -> Result<()> {
25
+ let started = Instant::now();
26
+ let root = cli
27
+ .dir
28
+ .canonicalize()
29
+ .with_context(|| format!("failed to resolve {}", cli.dir.display()))?;
30
+ if cli.fix && !matches!(cli.runtime, RuntimeKind::All | RuntimeKind::Node) {
31
+ anyhow::bail!("--fix is currently only supported for --runtime node or --runtime all");
32
+ }
33
+
34
+ let parser = if cli.fast {
35
+ ParserMode::Text
36
+ } else {
37
+ ParserMode::Oxc
38
+ };
39
+ let aggregate = cli.inspect.is_none();
40
+ let targets = cli.runtime.targets();
41
+ let first_runtime = runtime(targets[0])?;
42
+ let sources = SourceDiscovery.scan(&root, first_runtime)?;
43
+ let stats = sources.stats();
44
+
45
+ let reports = if cli.fast {
46
+ scan_fast(&root, &sources, targets, aggregate, cli.fix)?
47
+ } else {
48
+ scan_ast(&root, sources, targets, aggregate, cli.fix)?
49
+ };
50
+
51
+ Reporter::new(cli.summary, cli.inspect, parser).print(
52
+ &root,
53
+ &reports,
54
+ started.elapsed(),
55
+ stats,
56
+ );
57
+ Ok(())
58
+ }
59
+
60
+ fn scan_fast(
61
+ root: &std::path::Path,
62
+ sources: &SourceScan,
63
+ targets: &[RuntimeKind],
64
+ aggregate: bool,
65
+ fix: bool,
66
+ ) -> Result<Vec<RuntimeReport>> {
67
+ let target_runtimes = targets
68
+ .iter()
69
+ .copied()
70
+ .map(runtime)
71
+ .collect::<Result<Vec<_>>>()?;
72
+ let scan_plan = ScanPlan::new(targets, &target_runtimes)?;
73
+ let detections_by_runtime =
74
+ FffMultiRuntimeScanner.scan_files(&scan_plan.runtimes, sources.files())?;
75
+ let hidden_node_api_detected = scan_plan
76
+ .hidden_node_index
77
+ .and_then(|index| detections_by_runtime.get(index))
78
+ .is_some_and(|detections| has_node_api_detections(detections));
79
+ let mut reports = Vec::with_capacity(target_runtimes.len());
80
+
81
+ for (runtime, detections) in target_runtimes.into_iter().zip(detections_by_runtime) {
82
+ let node_api_detected = hidden_node_api_detected || has_node_api_detections(&detections);
83
+ reports.push(build_runtime_report(
84
+ root,
85
+ runtime.name(),
86
+ detections,
87
+ node_api_detected,
88
+ aggregate,
89
+ fix,
90
+ )?);
91
+ }
92
+
93
+ Ok(reports)
94
+ }
95
+
96
+ fn scan_ast(
97
+ root: &std::path::Path,
98
+ sources: SourceScan,
99
+ targets: &[RuntimeKind],
100
+ aggregate: bool,
101
+ fix: bool,
102
+ ) -> Result<Vec<RuntimeReport>> {
103
+ let target_runtimes = targets
104
+ .iter()
105
+ .copied()
106
+ .map(runtime)
107
+ .collect::<Result<Vec<_>>>()?;
108
+ let scan_plan = ScanPlan::new(targets, &target_runtimes)?;
109
+ let detections_by_runtime =
110
+ analyzer::analyze_files_for_runtimes(root, sources.files(), &scan_plan.runtimes)?;
111
+ let hidden_node_api_detected = scan_plan
112
+ .hidden_node_index
113
+ .and_then(|index| detections_by_runtime.get(index))
114
+ .is_some_and(|detections| has_node_api_detections(detections));
115
+ let mut reports = Vec::with_capacity(targets.len());
116
+
117
+ for (runtime, detections) in target_runtimes.into_iter().zip(detections_by_runtime) {
118
+ let node_api_detected = hidden_node_api_detected || has_node_api_detections(&detections);
119
+ reports.push(build_runtime_report(
120
+ root,
121
+ runtime.name(),
122
+ detections,
123
+ node_api_detected,
124
+ aggregate,
125
+ fix,
126
+ )?);
127
+ }
128
+
129
+ Ok(reports)
130
+ }
131
+
132
+ fn build_runtime_report(
133
+ root: &std::path::Path,
134
+ runtime_name: &str,
135
+ mut detections: Vec<DetectedFeature>,
136
+ has_node_api_detections: bool,
137
+ aggregate: bool,
138
+ fix: bool,
139
+ ) -> Result<RuntimeReport> {
140
+ let minimum = detections
141
+ .iter()
142
+ .map(|detection| detection.version)
143
+ .max()
144
+ .unwrap_or_default();
145
+
146
+ if aggregate {
147
+ collapse_prefix_detections(&mut detections);
148
+ detections = aggregate_feature_detections(detections);
149
+ }
150
+
151
+ let engines = if runtime_name == "node" {
152
+ check_engines(root, minimum, fix)?
153
+ } else {
154
+ None
155
+ };
156
+
157
+ Ok(RuntimeReport {
158
+ runtime: runtime_name.to_owned(),
159
+ detections,
160
+ minimum,
161
+ engines,
162
+ has_node_api_detections,
163
+ })
164
+ }
165
+
166
+ struct ScanPlan<'a> {
167
+ runtimes: Vec<&'a data::RuntimeDb>,
168
+ hidden_node_index: Option<usize>,
169
+ }
170
+
171
+ impl<'a> ScanPlan<'a> {
172
+ fn new(targets: &[RuntimeKind], target_runtimes: &[&'a data::RuntimeDb]) -> Result<Self> {
173
+ let needs_hidden_node = targets.iter().any(|target| is_browser_runtime(*target))
174
+ && !targets.contains(&RuntimeKind::Node);
175
+ let mut runtimes = target_runtimes.to_vec();
176
+ let hidden_node_index = if needs_hidden_node {
177
+ let index = runtimes.len();
178
+ runtimes.push(runtime(RuntimeKind::Node)?);
179
+ Some(index)
180
+ } else {
181
+ None
182
+ };
183
+
184
+ Ok(Self {
185
+ runtimes,
186
+ hidden_node_index,
187
+ })
188
+ }
189
+ }
190
+
191
+ fn is_browser_runtime(runtime: RuntimeKind) -> bool {
192
+ matches!(
193
+ runtime,
194
+ RuntimeKind::Safari | RuntimeKind::Chrome | RuntimeKind::Firefox
195
+ )
196
+ }
197
+
198
+ fn has_node_api_detections(detections: &[DetectedFeature]) -> bool {
199
+ detections
200
+ .iter()
201
+ .any(|detection| report::is_node_api_feature(&detection.feature))
202
+ }
203
+
204
+ fn aggregate_feature_detections(detections: Vec<DetectedFeature>) -> Vec<DetectedFeature> {
205
+ let mut aggregated: Vec<DetectedFeature> = Vec::new();
206
+ let mut indexes: HashMap<(String, crate::version::RuntimeVersion), usize> = HashMap::new();
207
+
208
+ for detection in detections {
209
+ let key = (detection.feature.clone(), detection.version);
210
+ if let Some(index) = indexes.get(&key).copied() {
211
+ aggregated[index].count += detection.count;
212
+ } else {
213
+ indexes.insert(key, aggregated.len());
214
+ aggregated.push(detection);
215
+ }
216
+ }
217
+
218
+ aggregated
219
+ }
220
+
221
+ fn collapse_prefix_detections(detections: &mut Vec<DetectedFeature>) {
222
+ let mut by_location: HashMap<_, Vec<usize>> = HashMap::new();
223
+ for (index, detection) in detections.iter().enumerate() {
224
+ by_location
225
+ .entry((detection.path.as_path(), detection.line, detection.column))
226
+ .or_default()
227
+ .push(index);
228
+ }
229
+
230
+ let mut remove = vec![false; detections.len()];
231
+ for indices in by_location.values() {
232
+ for &left in indices {
233
+ for &right in indices {
234
+ if left == right {
235
+ continue;
236
+ }
237
+ let prefix = format!("{}.", detections[left].feature);
238
+ if detections[right].feature.starts_with(&prefix) {
239
+ remove[left] = true;
240
+ }
241
+ }
242
+ }
243
+ }
244
+
245
+ let mut index = 0;
246
+ detections.retain(|_| {
247
+ let keep = !remove[index];
248
+ index += 1;
249
+ keep
250
+ });
251
+ }
package/src/main.rs ADDED
@@ -0,0 +1,12 @@
1
+ use clap::Parser;
2
+
3
+ fn main() -> anyhow::Result<()> {
4
+ let args = std::env::args_os().skip(1).collect::<Vec<_>>();
5
+ if args.is_empty() || args.iter().any(|arg| arg == "-h" || arg == "--help") {
6
+ runtime_checker::print_help();
7
+ return Ok(());
8
+ }
9
+
10
+ let cli = runtime_checker::Cli::parse();
11
+ runtime_checker::run(cli)
12
+ }