From 3ff0b178e084f441b47edf570656fbfb2cb02faf Mon Sep 17 00:00:00 2001 From: Marvin Hagemeister Date: Thu, 8 Aug 2024 18:09:20 +0200 Subject: [PATCH] feat(lint): add `require-node-prefix` rule --- cli/tools/lint/rules/mod.rs | 18 +- cli/tools/lint/rules/require_node_prefix.md | 19 ++ cli/tools/lint/rules/require_node_prefix.rs | 246 ++++++++++++++++++++ 3 files changed, 277 insertions(+), 6 deletions(-) create mode 100644 cli/tools/lint/rules/require_node_prefix.md create mode 100644 cli/tools/lint/rules/require_node_prefix.rs diff --git a/cli/tools/lint/rules/mod.rs b/cli/tools/lint/rules/mod.rs index dd723ad159..88c13d56b0 100644 --- a/cli/tools/lint/rules/mod.rs +++ b/cli/tools/lint/rules/mod.rs @@ -18,6 +18,7 @@ use crate::resolver::CliSloppyImportsResolver; mod no_sloppy_imports; mod no_slow_types; +mod require_node_prefix; // used for publishing pub use no_slow_types::collect_no_slow_type_diagnostics; @@ -177,12 +178,17 @@ impl LintRuleProvider { maybe_config_file: Option<&ConfigFile>, ) -> ConfiguredRules { let deno_lint_rules = deno_lint::rules::get_all_rules(); - let cli_lint_rules = vec![CliLintRule(CliLintRuleKind::Extended( - Box::new(no_sloppy_imports::NoSloppyImportsRule::new( - self.sloppy_imports_resolver.clone(), - self.workspace_resolver.clone(), - )), - ))]; + let cli_lint_rules = vec![ + CliLintRule(CliLintRuleKind::Extended(Box::new( + no_sloppy_imports::NoSloppyImportsRule::new( + self.sloppy_imports_resolver.clone(), + self.workspace_resolver.clone(), + ), + ))), + CliLintRule(CliLintRuleKind::Extended(Box::new( + require_node_prefix::RequireNodePrefix::new(), + ))), + ]; let cli_graph_rules = vec![CliLintRule(CliLintRuleKind::Package( Box::new(no_slow_types::NoSlowTypesRule), ))]; diff --git a/cli/tools/lint/rules/require_node_prefix.md b/cli/tools/lint/rules/require_node_prefix.md new file mode 100644 index 0000000000..4c4383b6a4 --- /dev/null +++ b/cli/tools/lint/rules/require_node_prefix.md @@ -0,0 +1,19 @@ +Enforces the use of the `node:` specifier for Node built-in modules. + +Deno requires Node built-in modules to be imported with the `node:` specifier. + +### Invalid: + +```typescript +import * as path from "path"; +import * as fs from "fs"; +import * as fsPromises from "fs/promises"; +``` + +### Valid: + +```typescript +import * as path from "node:path"; +import * as fs from "node:fs"; +import * as fsPromises from "node:fs/promises"; +``` diff --git a/cli/tools/lint/rules/require_node_prefix.rs b/cli/tools/lint/rules/require_node_prefix.rs new file mode 100644 index 0000000000..51ae52d6ea --- /dev/null +++ b/cli/tools/lint/rules/require_node_prefix.rs @@ -0,0 +1,246 @@ +use std::borrow::Cow; + +// Copyright 2020-2021 the Deno authors. All rights reserved. MIT license. +use super::ExtendedLintRule; + +use deno_ast::view as ast_view; +use deno_ast::SourceRange; +use deno_ast::SourceRanged; +use deno_lint::context::Context; +use deno_lint::diagnostic::LintFix; +use deno_lint::diagnostic::LintFixChange; +use deno_lint::rules::LintRule; + +#[derive(Debug)] +pub struct RequireNodePrefix; + +const CODE: &str = "require-node-prefix"; +const MESSAGE: &str = "built-in Node modules require the \"node:\" specifier"; +const HINT: &str = "Add \"node:\" prefix in front of the import specifier"; +const FIX_DESC: &str = "Add \"node:\" prefix"; +const DOCS_URL: &str = + "https://docs.deno.com/runtime/manual/node/migrate/#node.js-built-ins"; + +impl ExtendedLintRule for RequireNodePrefix { + fn supports_incremental_cache(&self) -> bool { + true + } + + fn help_docs_url(&self) -> std::borrow::Cow<'static, str> { + Cow::Borrowed(DOCS_URL) + } + + fn into_base(self: Box) -> Box { + self + } +} + +impl LintRule for RequireNodePrefix { + fn tags(&self) -> &'static [&'static str] { + &["recommended"] + } + + fn code(&self) -> &'static str { + CODE + } + + fn docs(&self) -> &'static str { + include_str!("require_node_prefix.md") + } + + fn lint_program_with_ast_view<'view>( + &self, + context: &mut Context<'view>, + program: deno_lint::Program<'view>, + ) { + NodeBuiltinsSpecifierGlobalHandler.traverse(program, context); + } +} + +struct NodeBuiltinsSpecifierGlobalHandler; + +impl NodeBuiltinsSpecifierGlobalHandler { + fn add_diagnostic(&self, ctx: &mut Context, src: &str, range: SourceRange) { + let specifier = format!(r#""node:{}""#, src); + + ctx.add_diagnostic_with_fixes( + range, + CODE, + MESSAGE, + Some(HINT.to_string()), + vec![LintFix { + description: FIX_DESC.into(), + changes: vec![LintFixChange { + new_text: specifier.into(), + range, + }], + }], + ); + } +} + +impl Handler for NodeBuiltinsSpecifierGlobalHandler { + fn import_decl(&mut self, decl: &ast_view::ImportDecl, ctx: &mut Context) { + let src = decl.src.inner.value.as_str(); + if is_bare_node_builtin(src) { + self.add_diagnostic(ctx, src, decl.src.range()); + } + } + + fn call_expr(&mut self, expr: &ast_view::CallExpr, ctx: &mut Context) { + if let ast_view::Callee::Import(_) = expr.callee { + if let Some(src_expr) = expr.args.first() { + if let ast_view::Expr::Lit(lit) = src_expr.expr { + if let ast_view::Lit::Str(str_value) = lit { + let src = str_value.inner.value.as_str(); + if is_bare_node_builtin(src) { + self.add_diagnostic(ctx, src, lit.range()); + } + } + } + } + } + } +} + +// Should match https://nodejs.org/api/module.html#modulebuiltinmodules +fn is_bare_node_builtin(src: &str) -> bool { + matches!( + src, + "assert" + | "assert/strict" + | "async_hooks" + | "buffer" + | "child_process" + | "cluster" + | "console" + | "constants" + | "crypto" + | "dgram" + | "diagnostics_channel" + | "dns" + | "dns/promises" + | "domain" + | "events" + | "fs" + | "fs/promises" + | "http" + | "http2" + | "https" + | "inspector" + | "inspector/promises" + | "module" + | "net" + | "os" + | "path" + | "path/posix" + | "path/win32" + | "perf_hooks" + | "process" + | "punycode" + | "querystring" + | "readline" + | "readline/promises" + | "repl" + | "stream" + | "stream/consumers" + | "stream/promises" + | "stream/web" + | "string_decoder" + | "sys" + | "timers" + | "timers/promises" + | "tls" + | "trace_events" + | "tty" + | "url" + | "util" + | "util/types" + | "v8" + | "vm" + | "wasi" + | "worker_threads" + | "zlib" + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn node_specifier_valid() { + assert_lint_ok! { + NodeBuiltinsSpecifier, + r#"import "node:path";"#, + r#"import "node:fs";"#, + r#"import "node:fs/promises";"#, + + r#"import * as fs from "node:fs";"#, + r#"import * as fsPromises from "node:fs/promises";"#, + r#"import fsPromises from "node:fs/promises";"#, + + r#"await import("node:fs");"#, + r#"await import("node:fs/promises");"#, + }; + } + + #[test] + fn node_specifier_invalid() { + assert_lint_err! { + NodeBuiltinsSpecifier, + MESSAGE, + HINT, + r#"import "path";"#: [ + { + col: 7, + fix: (FIX_DESC, r#"import "node:path";"#), + } + ], + r#"import "fs";"#: [ + { + col: 7, + fix: (FIX_DESC, r#"import "node:fs";"#), + } + ], + r#"import "fs/promises";"#: [ + { + col: 7, + fix: (FIX_DESC, r#"import "node:fs/promises";"#), + } + ], + + r#"import * as fs from "fs";"#: [ + { + col: 20, + fix: (FIX_DESC, r#"import * as fs from "node:fs";"#), + } + ], + r#"import * as fsPromises from "fs/promises";"#: [ + { + col: 28, + fix: (FIX_DESC, r#"import * as fsPromises from "node:fs/promises";"#), + } + ], + r#"import fsPromises from "fs/promises";"#: [ + { + col: 23, + fix: (FIX_DESC, r#"import fsPromises from "node:fs/promises";"#), + } + ], + + r#"await import("fs");"#: [ + { + col: 13, + fix: (FIX_DESC, r#"await import("node:fs");"#), + } + ], + r#"await import("fs/promises");"#: [ + { + col: 13, + fix: (FIX_DESC, r#"await import("node:fs/promises");"#), + } + ] + }; + } +}