diff --git a/docs/rules/node_builtin_specifier.md b/docs/rules/node_builtin_specifier.md new file mode 100644 index 000000000..4c4383b6a --- /dev/null +++ b/docs/rules/node_builtin_specifier.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/src/rules.rs b/src/rules.rs index 33f80cd51..d6ac5e6a2 100644 --- a/src/rules.rs +++ b/src/rules.rs @@ -97,6 +97,7 @@ pub mod no_var; pub mod no_window; pub mod no_window_prefix; pub mod no_with; +pub mod node_builtin_specifier; pub mod prefer_as_const; pub mod prefer_ascii; pub mod prefer_const; @@ -329,6 +330,7 @@ fn get_all_rules_raw() -> Vec> { Box::new(no_var::NoVar), Box::new(no_window::NoWindow), Box::new(no_window_prefix::NoWindowPrefix), + Box::new(node_builtin_specifier::NodeBuiltinsSpecifier), Box::new(no_with::NoWith), Box::new(prefer_as_const::PreferAsConst), Box::new(prefer_ascii::PreferAscii), diff --git a/src/rules/node_builtin_specifier.rs b/src/rules/node_builtin_specifier.rs new file mode 100644 index 000000000..2e73bb589 --- /dev/null +++ b/src/rules/node_builtin_specifier.rs @@ -0,0 +1,238 @@ +// Copyright 2020-2021 the Deno authors. All rights reserved. MIT license. +use super::Context; +use super::LintRule; +use crate::diagnostic::LintFix; +use crate::diagnostic::LintFixChange; +use crate::handler::Handler; +use crate::handler::Traverse; +use crate::Program; + +use deno_ast::view as ast_view; +use deno_ast::SourceRange; +use deno_ast::SourceRanged; + +#[derive(Debug)] +pub struct NodeBuiltinsSpecifier; + +const CODE: &str = "node-builtin-specifier"; +const MESSAGE: &str = "built-in Node modules need the \"node:\" specifier"; +const HINT: &str = "Add \"node:\" prefix in front of the import specifier"; +const FIX_DESC: &str = "Add \"node:\" prefix"; + +impl LintRule for NodeBuiltinsSpecifier { + fn tags(&self) -> &'static [&'static str] { + &["recommended"] + } + + fn code(&self) -> &'static str { + CODE + } + + fn lint_program_with_ast_view( + &self, + context: &mut Context, + program: Program<'_>, + ) { + NodeBuiltinsSpecifierGlobalHandler.traverse(program, context); + } + + #[cfg(feature = "docs")] + fn docs(&self) -> &'static str { + include_str!("../../docs/rules/node_builtin_specifier.md") + } +} + +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) { + match expr.callee { + ast_view::Callee::Import(_) => { + if let Some(src_expr) = expr.args.first() { + match src_expr.expr { + ast_view::Expr::Lit(lit) => match lit { + ast_view::Lit::Str(str_value) => { + 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");"#), + } + ] + }; + } +} diff --git a/www/static/docs.json b/www/static/docs.json index 084532d0c..b08c11d3e 100644 --- a/www/static/docs.json +++ b/www/static/docs.json @@ -583,6 +583,13 @@ "recommended" ] }, + { + "code": "node-builtin-specifier", + "docs": "Enforces the use of the `node:` specifier for Node built-in modules.\n\nDeno requires Node built-in modules to be imported with the `node:` specifier.\n\n### Invalid:\n\n```typescript\nimport * as path from \"path\";\nimport * as fs from \"fs\";\nimport * as fsPromises from \"fs/promises\";\n```\n\n### Valid:\n\n```typescript\nimport * as path from \"node:path\";\nimport * as fs from \"node:fs\";\nimport * as fsPromises from \"node:fs/promises\";\n```\n", + "tags": [ + "recommended" + ] + }, { "code": "no-with", "docs": "Disallows the usage of `with` statements.\n\nThe `with` statement is discouraged as it may be the source of confusing bugs\nand compatibility issues. For more details, see [with - JavaScript | MDN].\n\n[with - JavaScript | MDN]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/with\n\n### Invalid:\n\n```typescript\nwith (someVar) {\n console.log(\"foo\");\n}\n```\n",