From 751957472f5e431afd482626dce1faa483dbc2a5 Mon Sep 17 00:00:00 2001 From: Marvin Hagemeister Date: Fri, 22 Nov 2024 22:40:06 +0100 Subject: [PATCH] feat: add no-unversioned-import rule --- docs/rules/no_unversioned_import.md | 17 ++++ schemas/rules.v1.json | 1 + src/rules.rs | 2 + src/rules/no_unversioned_import.rs | 147 ++++++++++++++++++++++++++++ www/static/docs.json | 5 + 5 files changed, 172 insertions(+) create mode 100644 docs/rules/no_unversioned_import.md create mode 100644 src/rules/no_unversioned_import.rs diff --git a/docs/rules/no_unversioned_import.md b/docs/rules/no_unversioned_import.md new file mode 100644 index 00000000..c0502cfd --- /dev/null +++ b/docs/rules/no_unversioned_import.md @@ -0,0 +1,17 @@ +Ensure that inline dependency imports have a version specifier. + +### Invalid: + +```ts +import foo from "npm:chalk"; +import foo from "jsr:@std/path"; +``` + +### Valid: + +```ts +import foo from "npm:chalk@5.3.0"; +import foo from "npm:chalk@^5.3.0"; +import foo from "jsr:@std/path@1.0.8"; +import foo from "jsr:@std/path@^1.0.8"; +``` diff --git a/schemas/rules.v1.json b/schemas/rules.v1.json index 32ea5162..3c1034db 100644 --- a/schemas/rules.v1.json +++ b/schemas/rules.v1.json @@ -92,6 +92,7 @@ "no-unsafe-negation", "no-unused-labels", "no-unused-vars", + "no-unversioned-import", "no-var", "no-window", "no-window-prefix", diff --git a/src/rules.rs b/src/rules.rs index 2b6a4a07..3b80dec1 100644 --- a/src/rules.rs +++ b/src/rules.rs @@ -95,6 +95,7 @@ pub mod no_unsafe_finally; pub mod no_unsafe_negation; pub mod no_unused_labels; pub mod no_unused_vars; +pub mod no_unversioned_import; pub mod no_var; pub mod no_window; pub mod no_window_prefix; @@ -330,6 +331,7 @@ fn get_all_rules_raw() -> Vec> { Box::new(no_unsafe_negation::NoUnsafeNegation), Box::new(no_unused_labels::NoUnusedLabels), Box::new(no_unused_vars::NoUnusedVars), + Box::new(no_unversioned_import::NoUnversionedImport), Box::new(no_var::NoVar), Box::new(no_window::NoWindow), Box::new(no_window_prefix::NoWindowPrefix), diff --git a/src/rules/no_unversioned_import.rs b/src/rules/no_unversioned_import.rs new file mode 100644 index 00000000..43ec7147 --- /dev/null +++ b/src/rules/no_unversioned_import.rs @@ -0,0 +1,147 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use super::{Context, LintRule}; +use crate::handler::{Handler, Traverse}; +use crate::Program; +use deno_ast::view::{CallExpr, Callee, Expr, ImportDecl, Lit}; +use deno_ast::SourceRanged; +use once_cell::sync::Lazy; +use regex::Regex; + +#[derive(Debug)] +pub struct NoUnversionedImport; + +const CODE: &str = "no-unversioned-import"; +const MESSAGE: &str = "Missing version in specifier"; +const HINT: &str = "Add a version at the end"; + +impl LintRule for NoUnversionedImport { + fn tags(&self) -> &'static [&'static str] { + &[] + } + + fn code(&self) -> &'static str { + CODE + } + + fn lint_program_with_ast_view( + &self, + context: &mut Context, + program: Program<'_>, + ) { + NoUnversionedImportHandler.traverse(program, context); + } + + #[cfg(feature = "docs")] + fn docs(&self) -> &'static str { + include_str!("../../docs/rules/no_unversioned_import.md") + } +} + +struct NoUnversionedImportHandler; + +impl Handler for NoUnversionedImportHandler { + fn import_decl(&mut self, node: &ImportDecl, ctx: &mut Context) { + if is_unversioned(node.src.value()) { + ctx.add_diagnostic_with_hint(node.src.range(), CODE, MESSAGE, HINT); + } + } + + fn call_expr(&mut self, node: &CallExpr, ctx: &mut Context) { + if let Callee::Import(_) = node.callee { + if let Some(arg) = node.args.first() { + if let Expr::Lit(Lit::Str(lit)) = arg.expr { + if is_unversioned(lit.value()) { + ctx.add_diagnostic_with_hint(arg.range(), CODE, MESSAGE, HINT); + } + } + } + } + } +} + +static NPM_REG: Lazy = + Lazy::new(|| Regex::new(r"^npm:(@.+\/[^@]+|[^@]+)$").unwrap()); +static JSR_REG: Lazy = + Lazy::new(|| Regex::new(r"^jsr:@.+\/[^@]+$").unwrap()); + +fn is_unversioned(s: &str) -> bool { + if s.starts_with("npm:") { + return NPM_REG.is_match(s); + } else if s.starts_with("jsr:") { + return JSR_REG.is_match(s); + } + + false +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn no_with_valid() { + assert_lint_ok! { + NoUnversionedImport, + r#"import foo from "foo";"#, + r#"import foo from "@foo/bar";"#, + r#"import foo from "./foo";"#, + r#"import foo from "../foo";"#, + r#"import foo from "~/foo";"#, + r#"import foo from "npm:foo@1.2.3";"#, + r#"import foo from "npm:foo@^1.2.3";"#, + r#"import foo from "npm:@foo/bar@1.2.3";"#, + r#"import foo from "npm:@foo/bar@^1.2.3";"#, + r#"import foo from "jsr:@foo/bar@1.2.3";"#, + r#"import foo from "jsr:@foo/bar@^1.2.3";"#, + r#"import("foo")"#, + r#"import("@foo/bar")"#, + r#"import("./foo")"#, + r#"import("../foo")"#, + r#"import("~/foo")"#, + r#"import("npm:foo@1.2.3")"#, + r#"import("npm:foo@^1.2.3")"#, + r#"import("npm:@foo/bar@1.2.3")"#, + r#"import("npm:@foo/bar@^1.2.3")"#, + r#"import("jsr:@foo/bar@1.2.3")"#, + r#"import("jsr:@foo/bar@^1.2.3")"#, + } + } + + #[test] + fn no_with_invalid() { + assert_lint_err! { + NoUnversionedImport, + r#"import foo from "jsr:@foo/foo";"#: [{ + col: 16, + message: MESSAGE, + hint: HINT + }], + r#"import foo from "npm:foo";"#: [{ + col: 16, + message: MESSAGE, + hint: HINT + }], + r#"import foo from "npm:@foo/bar";"#: [{ + col: 16, + message: MESSAGE, + hint: HINT + }], + r#"import("jsr:@foo/foo");"#: [{ + col: 7, + message: MESSAGE, + hint: HINT + }], + r#"import("npm:foo");"#: [{ + col: 7, + message: MESSAGE, + hint: HINT + }], + r#"import("npm:@foo/bar");"#: [{ + col: 7, + message: MESSAGE, + hint: HINT + }], + } + } +} diff --git a/www/static/docs.json b/www/static/docs.json index 3361afd6..716e8c29 100644 --- a/www/static/docs.json +++ b/www/static/docs.json @@ -576,6 +576,11 @@ "recommended" ] }, + { + "code": "no-unversioned-import", + "docs": "Ensure that inline dependency imports have a version specifier.\n\n### Invalid:\n\n```ts\nimport foo from \"npm:chalk\";\nimport foo from \"jsr:@std/path\";\n```\n\n### Valid:\n\n```ts\nimport foo from \"npm:chalk@5.3.0\";\nimport foo from \"npm:chalk@^5.3.0\";\nimport foo from \"jsr:@std/path@1.0.8\";\nimport foo from \"jsr:@std/path@^1.0.8\";\n```\n", + "tags": [] + }, { "code": "no-var", "docs": "Enforces the use of block scoped variables over more error prone function scoped\nvariables. Block scoped variables are defined using `const` and `let` keywords.\n\n`const` and `let` keywords ensure the variables defined using these keywords are\nnot accessible outside their block scope. On the other hand, variables defined\nusing `var` keyword are only limited by their function scope.\n\n### Invalid:\n\n```typescript\nvar foo = \"bar\";\n```\n\n### Valid:\n\n```typescript\nconst foo = 1;\nlet bar = 2;\n```\n",