flarelint 0.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.
@@ -0,0 +1,120 @@
1
+ pub mod do_storage;
2
+ pub mod node_compat;
3
+ pub mod routes;
4
+ pub mod waituntil;
5
+
6
+ use oxc_allocator::Allocator;
7
+ use std::path::Path;
8
+ use std::time::Instant;
9
+
10
+ use crate::config::CloudflareConfig;
11
+ use crate::diagnostics::{Diagnostic, LintReport};
12
+ use crate::parser::{
13
+ discover_files, extract_scripts, is_routes_json_file, is_supported_source_file, parse_ast,
14
+ read_file_string,
15
+ };
16
+ use do_storage::DoStorageLinter;
17
+ use node_compat::NodeCompatLinter;
18
+ use routes::RoutesLinter;
19
+ use waituntil::WaitUntilLinter;
20
+
21
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
22
+ pub enum RuleCategory {
23
+ All,
24
+ NodeCompat,
25
+ WaitUntil,
26
+ DoStorage,
27
+ Routes,
28
+ }
29
+
30
+ pub fn run_linter_on_target(
31
+ target_path: &Path,
32
+ category: RuleCategory,
33
+ override_config: Option<CloudflareConfig>,
34
+ ) -> Result<LintReport, String> {
35
+ let start_time = Instant::now();
36
+ let mut report = LintReport::new();
37
+
38
+ let config = if let Some(cfg) = override_config {
39
+ cfg
40
+ } else {
41
+ CloudflareConfig::find_and_load(target_path).unwrap_or_default()
42
+ };
43
+
44
+ let files = discover_files(target_path);
45
+
46
+ for file_path in files {
47
+ if is_routes_json_file(&file_path) {
48
+ if category == RuleCategory::All || category == RuleCategory::Routes {
49
+ report.total_files_scanned += 1;
50
+ if let Ok(content) = read_file_string(&file_path) {
51
+ let mut linter = RoutesLinter::new(&file_path, &content);
52
+ linter.lint();
53
+ for diag in linter.diagnostics {
54
+ report.add_diagnostic(diag);
55
+ }
56
+ }
57
+ }
58
+ continue;
59
+ }
60
+
61
+ if !is_supported_source_file(&file_path) {
62
+ continue;
63
+ }
64
+
65
+ if category == RuleCategory::Routes {
66
+ continue;
67
+ }
68
+
69
+ report.total_files_scanned += 1;
70
+ let content = match read_file_string(&file_path) {
71
+ Ok(c) => c,
72
+ Err(e) => {
73
+ report.add_diagnostic(Diagnostic::error("io-error", e, &file_path));
74
+ continue;
75
+ }
76
+ };
77
+
78
+ let scripts = extract_scripts(&file_path, &content);
79
+ for script in scripts {
80
+ let allocator = Allocator::default();
81
+ match parse_ast(&allocator, &script) {
82
+ Ok(ast) => {
83
+ if category == RuleCategory::All || category == RuleCategory::NodeCompat {
84
+ let mut node_linter = NodeCompatLinter::new(&file_path, &content, &config);
85
+ node_linter.lint_ast(&ast);
86
+ for diag in node_linter.diagnostics {
87
+ report.add_diagnostic(diag);
88
+ }
89
+ }
90
+
91
+ if category == RuleCategory::All || category == RuleCategory::WaitUntil {
92
+ let mut wait_linter = WaitUntilLinter::new(&file_path, &content);
93
+ wait_linter.lint_ast(&ast);
94
+ for diag in wait_linter.diagnostics {
95
+ report.add_diagnostic(diag);
96
+ }
97
+ }
98
+
99
+ if category == RuleCategory::All || category == RuleCategory::DoStorage {
100
+ let mut do_linter = DoStorageLinter::new(&file_path, &content);
101
+ do_linter.lint_ast(&ast);
102
+ for diag in do_linter.diagnostics {
103
+ report.add_diagnostic(diag);
104
+ }
105
+ }
106
+ }
107
+ Err(err) => {
108
+ report.add_diagnostic(Diagnostic::error(
109
+ "syntax-error",
110
+ format!("Failed to parse source AST: {}", err),
111
+ &file_path,
112
+ ));
113
+ }
114
+ }
115
+ }
116
+ }
117
+
118
+ report.elapsed_ns = start_time.elapsed().as_nanos();
119
+ Ok(report)
120
+ }
@@ -0,0 +1,501 @@
1
+ use oxc_ast::ast::*;
2
+ use std::path::Path;
3
+
4
+ use crate::config::CloudflareConfig;
5
+ use crate::diagnostics::Diagnostic;
6
+ use crate::parser::{AstUnit, offset_to_location};
7
+
8
+ pub const STRICTLY_UNSUPPORTED_MODULES: &[&str] = &[
9
+ "child_process",
10
+ "cluster",
11
+ "dgram",
12
+ "v8",
13
+ "vm",
14
+ "worker_threads",
15
+ ];
16
+
17
+ pub const ALL_NODE_BUILTINS: &[&str] = &[
18
+ "assert",
19
+ "assert/strict",
20
+ "async_hooks",
21
+ "buffer",
22
+ "child_process",
23
+ "cluster",
24
+ "console",
25
+ "constants",
26
+ "crypto",
27
+ "dgram",
28
+ "diagnostics_channel",
29
+ "dns",
30
+ "dns/promises",
31
+ "domain",
32
+ "events",
33
+ "fs",
34
+ "fs/promises",
35
+ "http",
36
+ "http2",
37
+ "https",
38
+ "inspector",
39
+ "inspector/promises",
40
+ "module",
41
+ "net",
42
+ "os",
43
+ "path",
44
+ "path/posix",
45
+ "path/win32",
46
+ "perf_hooks",
47
+ "process",
48
+ "punycode",
49
+ "querystring",
50
+ "readline",
51
+ "readline/promises",
52
+ "repl",
53
+ "stream",
54
+ "stream/consumers",
55
+ "stream/promises",
56
+ "stream/web",
57
+ "string_decoder",
58
+ "sys",
59
+ "timers",
60
+ "timers/promises",
61
+ "tls",
62
+ "trace_events",
63
+ "tty",
64
+ "url",
65
+ "util",
66
+ "util/types",
67
+ "v8",
68
+ "vm",
69
+ "wasi",
70
+ "worker_threads",
71
+ "zlib",
72
+ ];
73
+
74
+ pub fn normalize_module_specifier(specifier: &str) -> &str {
75
+ specifier.strip_prefix("node:").unwrap_or(specifier)
76
+ }
77
+
78
+ pub fn is_node_builtin(specifier: &str) -> bool {
79
+ let clean = normalize_module_specifier(specifier);
80
+ ALL_NODE_BUILTINS.contains(&clean)
81
+ }
82
+
83
+ pub fn is_strictly_unsupported(specifier: &str) -> bool {
84
+ let clean = normalize_module_specifier(specifier);
85
+ STRICTLY_UNSUPPORTED_MODULES.contains(&clean)
86
+ }
87
+
88
+ pub struct NodeCompatLinter<'a> {
89
+ pub file_path: &'a Path,
90
+ pub full_source: &'a str,
91
+ pub config: &'a CloudflareConfig,
92
+ pub diagnostics: Vec<Diagnostic>,
93
+ }
94
+
95
+ impl<'a> NodeCompatLinter<'a> {
96
+ pub fn new(file_path: &'a Path, full_source: &'a str, config: &'a CloudflareConfig) -> Self {
97
+ Self {
98
+ file_path,
99
+ full_source,
100
+ config,
101
+ diagnostics: Vec::new(),
102
+ }
103
+ }
104
+
105
+ pub fn lint_ast(&mut self, ast: &AstUnit<'_>) {
106
+ for stmt in &ast.program.body {
107
+ self.visit_statement(stmt, ast);
108
+ }
109
+ }
110
+
111
+ fn check_specifier(
112
+ &mut self,
113
+ specifier: &str,
114
+ span_start: u32,
115
+ span_end: u32,
116
+ ast: &AstUnit<'_>,
117
+ ) {
118
+ let is_node_prefixed = specifier.starts_with("node:");
119
+ let base_module = normalize_module_specifier(specifier);
120
+
121
+ if !is_node_builtin(specifier) && !is_node_prefixed {
122
+ return;
123
+ }
124
+
125
+ let loc = offset_to_location(self.full_source, ast.byte_offset, span_start, span_end);
126
+
127
+ if is_strictly_unsupported(base_module) {
128
+ let mut diag = Diagnostic::error(
129
+ "node-compat/strictly-unsupported",
130
+ format!(
131
+ "Node.js built-in module '{}' is strictly unsupported in Cloudflare Workers and Astro edge runtime.",
132
+ specifier
133
+ ),
134
+ self.file_path,
135
+ )
136
+ .with_location(loc)
137
+ .with_suggestion(
138
+ format!(
139
+ "Remove dependency on '{}'. Use Web standard APIs (WebCrypto, fetch, Streams) or Cloudflare bindings.",
140
+ base_module
141
+ ),
142
+ None,
143
+ );
144
+ diag.code_snippet = self.extract_snippet(loc.line);
145
+ self.diagnostics.push(diag);
146
+ return;
147
+ }
148
+
149
+ let has_nodejs_compat = self.config.has_nodejs_compat();
150
+
151
+ if !has_nodejs_compat {
152
+ let mut diag = Diagnostic::error(
153
+ "node-compat/missing-flag",
154
+ format!(
155
+ "Node.js built-in module '{}' requires 'nodejs_compat' compatibility flag in wrangler config.",
156
+ specifier
157
+ ),
158
+ self.file_path,
159
+ )
160
+ .with_location(loc)
161
+ .with_suggestion(
162
+ "Add 'nodejs_compat' to 'compatibility_flags' in wrangler.jsonc or wrangler.toml",
163
+ Some(format!("node:{}", base_module)),
164
+ );
165
+ diag.code_snippet = self.extract_snippet(loc.line);
166
+ self.diagnostics.push(diag);
167
+ } else if !is_node_prefixed && !self.config.has_nodejs_compat_v2() {
168
+ let mut diag = Diagnostic::warning(
169
+ "node-compat/prefer-node-protocol",
170
+ format!(
171
+ "Import specifier '{}' should use explicit 'node:{}' prefix for Cloudflare Workers compatibility.",
172
+ specifier, base_module
173
+ ),
174
+ self.file_path,
175
+ )
176
+ .with_location(loc)
177
+ .with_suggestion(
178
+ format!("Change '{}' to 'node:{}'", specifier, base_module),
179
+ Some(format!("node:{}", base_module)),
180
+ );
181
+ diag.code_snippet = self.extract_snippet(loc.line);
182
+ self.diagnostics.push(diag);
183
+ }
184
+ }
185
+
186
+ fn extract_snippet(&self, target_line: usize) -> Option<String> {
187
+ let lines: Vec<&str> = self.full_source.lines().collect();
188
+ if target_line == 0 || target_line > lines.len() {
189
+ return None;
190
+ }
191
+ Some(lines[target_line - 1].trim().to_string())
192
+ }
193
+
194
+ fn visit_statement(&mut self, stmt: &Statement<'_>, ast: &AstUnit<'_>) {
195
+ match stmt {
196
+ Statement::ImportDeclaration(import_decl) => {
197
+ let specifier = import_decl.source.value.as_str();
198
+ self.check_specifier(
199
+ specifier,
200
+ import_decl.source.span.start,
201
+ import_decl.source.span.end,
202
+ ast,
203
+ );
204
+ }
205
+ Statement::ExportAllDeclaration(export_decl) => {
206
+ let specifier = export_decl.source.value.as_str();
207
+ self.check_specifier(
208
+ specifier,
209
+ export_decl.source.span.start,
210
+ export_decl.source.span.end,
211
+ ast,
212
+ );
213
+ }
214
+ Statement::ExportNamedDeclaration(export_decl) => {
215
+ if let Some(source) = &export_decl.source {
216
+ let specifier = source.value.as_str();
217
+ self.check_specifier(specifier, source.span.start, source.span.end, ast);
218
+ }
219
+ if let Some(decl) = &export_decl.declaration {
220
+ self.visit_declaration(decl, ast);
221
+ }
222
+ }
223
+ Statement::ExportDefaultDeclaration(export_decl) => match &export_decl.declaration {
224
+ ExportDefaultDeclarationKind::FunctionDeclaration(func) => {
225
+ self.visit_function(func, ast);
226
+ }
227
+ ExportDefaultDeclarationKind::ClassDeclaration(cls) => {
228
+ self.visit_class(cls, ast);
229
+ }
230
+ decl => {
231
+ if let Some(expr) = decl.as_expression() {
232
+ self.visit_expression(expr, ast);
233
+ }
234
+ }
235
+ },
236
+ Statement::ExpressionStatement(expr_stmt) => {
237
+ self.visit_expression(&expr_stmt.expression, ast);
238
+ }
239
+ Statement::BlockStatement(block) => {
240
+ for s in &block.body {
241
+ self.visit_statement(s, ast);
242
+ }
243
+ }
244
+ Statement::IfStatement(if_stmt) => {
245
+ self.visit_expression(&if_stmt.test, ast);
246
+ self.visit_statement(&if_stmt.consequent, ast);
247
+ if let Some(alt) = &if_stmt.alternate {
248
+ self.visit_statement(alt, ast);
249
+ }
250
+ }
251
+ Statement::VariableDeclaration(var_decl) => {
252
+ for decl in &var_decl.declarations {
253
+ if let Some(init) = &decl.init {
254
+ self.visit_expression(init, ast);
255
+ }
256
+ }
257
+ }
258
+ Statement::FunctionDeclaration(func) => {
259
+ self.visit_function(func, ast);
260
+ }
261
+ Statement::ClassDeclaration(cls) => {
262
+ self.visit_class(cls, ast);
263
+ }
264
+ Statement::ReturnStatement(ret) => {
265
+ if let Some(arg) = &ret.argument {
266
+ self.visit_expression(arg, ast);
267
+ }
268
+ }
269
+ Statement::TryStatement(try_stmt) => {
270
+ for s in &try_stmt.block.body {
271
+ self.visit_statement(s, ast);
272
+ }
273
+ if let Some(handler) = &try_stmt.handler {
274
+ for s in &handler.body.body {
275
+ self.visit_statement(s, ast);
276
+ }
277
+ }
278
+ if let Some(finalizer) = &try_stmt.finalizer {
279
+ for s in &finalizer.body {
280
+ self.visit_statement(s, ast);
281
+ }
282
+ }
283
+ }
284
+ Statement::ForStatement(for_stmt) => {
285
+ if let Some(init) = &for_stmt.init {
286
+ match init {
287
+ ForStatementInit::VariableDeclaration(v) => {
288
+ for decl in &v.declarations {
289
+ if let Some(e) = &decl.init {
290
+ self.visit_expression(e, ast);
291
+ }
292
+ }
293
+ }
294
+ other_init => {
295
+ if let Some(e) = other_init.as_expression() {
296
+ self.visit_expression(e, ast);
297
+ }
298
+ }
299
+ }
300
+ }
301
+ if let Some(test) = &for_stmt.test {
302
+ self.visit_expression(test, ast);
303
+ }
304
+ if let Some(update) = &for_stmt.update {
305
+ self.visit_expression(update, ast);
306
+ }
307
+ self.visit_statement(&for_stmt.body, ast);
308
+ }
309
+ Statement::ForInStatement(for_in) => {
310
+ self.visit_expression(&for_in.right, ast);
311
+ self.visit_statement(&for_in.body, ast);
312
+ }
313
+ Statement::ForOfStatement(for_of) => {
314
+ self.visit_expression(&for_of.right, ast);
315
+ self.visit_statement(&for_of.body, ast);
316
+ }
317
+ Statement::WhileStatement(while_stmt) => {
318
+ self.visit_expression(&while_stmt.test, ast);
319
+ self.visit_statement(&while_stmt.body, ast);
320
+ }
321
+ Statement::DoWhileStatement(dowhile) => {
322
+ self.visit_statement(&dowhile.body, ast);
323
+ self.visit_expression(&dowhile.test, ast);
324
+ }
325
+ Statement::SwitchStatement(switch_stmt) => {
326
+ self.visit_expression(&switch_stmt.discriminant, ast);
327
+ for case in &switch_stmt.cases {
328
+ if let Some(test) = &case.test {
329
+ self.visit_expression(test, ast);
330
+ }
331
+ for s in &case.consequent {
332
+ self.visit_statement(s, ast);
333
+ }
334
+ }
335
+ }
336
+ _ => {}
337
+ }
338
+ }
339
+
340
+ fn visit_declaration(&mut self, decl: &Declaration<'_>, ast: &AstUnit<'_>) {
341
+ match decl {
342
+ Declaration::VariableDeclaration(var_decl) => {
343
+ for d in &var_decl.declarations {
344
+ if let Some(init) = &d.init {
345
+ self.visit_expression(init, ast);
346
+ }
347
+ }
348
+ }
349
+ Declaration::FunctionDeclaration(func) => {
350
+ self.visit_function(func, ast);
351
+ }
352
+ Declaration::ClassDeclaration(cls) => {
353
+ self.visit_class(cls, ast);
354
+ }
355
+ _ => {}
356
+ }
357
+ }
358
+
359
+ fn visit_function(&mut self, func: &Function<'_>, ast: &AstUnit<'_>) {
360
+ if let Some(body) = &func.body {
361
+ for s in &body.statements {
362
+ self.visit_statement(s, ast);
363
+ }
364
+ }
365
+ }
366
+
367
+ fn visit_class(&mut self, cls: &Class<'_>, ast: &AstUnit<'_>) {
368
+ for element in &cls.body.body {
369
+ match element {
370
+ ClassElement::MethodDefinition(method) => {
371
+ if let Some(body) = &method.value.body {
372
+ for s in &body.statements {
373
+ self.visit_statement(s, ast);
374
+ }
375
+ }
376
+ }
377
+ ClassElement::PropertyDefinition(prop) => {
378
+ if let Some(value) = &prop.value {
379
+ self.visit_expression(value, ast);
380
+ }
381
+ }
382
+ ClassElement::AccessorProperty(acc) => {
383
+ if let Some(value) = &acc.value {
384
+ self.visit_expression(value, ast);
385
+ }
386
+ }
387
+ ClassElement::StaticBlock(block) => {
388
+ for s in &block.body {
389
+ self.visit_statement(s, ast);
390
+ }
391
+ }
392
+ _ => {}
393
+ }
394
+ }
395
+ }
396
+
397
+ fn visit_expression(&mut self, expr: &Expression<'_>, ast: &AstUnit<'_>) {
398
+ if let Some(mem) = expr.as_member_expression() {
399
+ self.visit_expression(mem.object(), ast);
400
+ return;
401
+ }
402
+
403
+ match expr {
404
+ Expression::CallExpression(call) => {
405
+ if let Expression::Identifier(ident) = &call.callee
406
+ && ident.name == "require"
407
+ && !call.arguments.is_empty()
408
+ && let Some(first_arg) = call.arguments.first()
409
+ && let Some(Expression::StringLiteral(lit)) = first_arg.as_expression()
410
+ {
411
+ self.check_specifier(lit.value.as_str(), lit.span.start, lit.span.end, ast);
412
+ }
413
+ self.visit_expression(&call.callee, ast);
414
+ for arg in &call.arguments {
415
+ if let Some(arg_expr) = arg.as_expression() {
416
+ self.visit_expression(arg_expr, ast);
417
+ }
418
+ }
419
+ }
420
+ Expression::ImportExpression(imp) => {
421
+ if let Expression::StringLiteral(lit) = &imp.source {
422
+ self.check_specifier(lit.value.as_str(), lit.span.start, lit.span.end, ast);
423
+ } else {
424
+ self.visit_expression(&imp.source, ast);
425
+ }
426
+ }
427
+ Expression::ArrayExpression(arr) => {
428
+ for elem in &arr.elements {
429
+ if let Some(elem_expr) = elem.as_expression() {
430
+ self.visit_expression(elem_expr, ast);
431
+ }
432
+ }
433
+ }
434
+ Expression::ObjectExpression(obj) => {
435
+ for prop in &obj.properties {
436
+ match prop {
437
+ ObjectPropertyKind::ObjectProperty(p) => {
438
+ self.visit_expression(&p.value, ast);
439
+ }
440
+ ObjectPropertyKind::SpreadProperty(p) => {
441
+ self.visit_expression(&p.argument, ast);
442
+ }
443
+ }
444
+ }
445
+ }
446
+ Expression::ArrowFunctionExpression(arrow) => {
447
+ if arrow.expression {
448
+ if let Some(Statement::ExpressionStatement(e)) = arrow.body.statements.first() {
449
+ self.visit_expression(&e.expression, ast);
450
+ }
451
+ } else {
452
+ for s in &arrow.body.statements {
453
+ self.visit_statement(s, ast);
454
+ }
455
+ }
456
+ }
457
+ Expression::FunctionExpression(func) => {
458
+ self.visit_function(func, ast);
459
+ }
460
+ Expression::AwaitExpression(aw) => {
461
+ self.visit_expression(&aw.argument, ast);
462
+ }
463
+ Expression::BinaryExpression(bin) => {
464
+ self.visit_expression(&bin.left, ast);
465
+ self.visit_expression(&bin.right, ast);
466
+ }
467
+ Expression::LogicalExpression(log) => {
468
+ self.visit_expression(&log.left, ast);
469
+ self.visit_expression(&log.right, ast);
470
+ }
471
+ Expression::UnaryExpression(un) => {
472
+ self.visit_expression(&un.argument, ast);
473
+ }
474
+ Expression::AssignmentExpression(assign) => {
475
+ self.visit_expression(&assign.right, ast);
476
+ }
477
+ Expression::SequenceExpression(seq) => {
478
+ for e in &seq.expressions {
479
+ self.visit_expression(e, ast);
480
+ }
481
+ }
482
+ Expression::ParenthesizedExpression(paren) => {
483
+ self.visit_expression(&paren.expression, ast);
484
+ }
485
+ Expression::ConditionalExpression(cond) => {
486
+ self.visit_expression(&cond.test, ast);
487
+ self.visit_expression(&cond.consequent, ast);
488
+ self.visit_expression(&cond.alternate, ast);
489
+ }
490
+ Expression::NewExpression(new_expr) => {
491
+ self.visit_expression(&new_expr.callee, ast);
492
+ for arg in &new_expr.arguments {
493
+ if let Some(arg_expr) = arg.as_expression() {
494
+ self.visit_expression(arg_expr, ast);
495
+ }
496
+ }
497
+ }
498
+ _ => {}
499
+ }
500
+ }
501
+ }