From 751957472f5e431afd482626dce1faa483dbc2a5 Mon Sep 17 00:00:00 2001 From: Marvin Hagemeister Date: Fri, 22 Nov 2024 22:40:06 +0100 Subject: [PATCH 1/5] 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 000000000..c0502cfde --- /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 32ea51622..3c1034dbe 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 2b6a4a070..3b80dec1a 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 000000000..43ec7147a --- /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 3361afd61..716e8c290 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", From 3d8723ce220e28bed593f918f09cf2d55257cb15 Mon Sep 17 00:00:00 2001 From: David Sherret Date: Wed, 3 Sep 2025 14:23:26 -0400 Subject: [PATCH 2/5] remove --- docs/rules/no_unversioned_import.md | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 docs/rules/no_unversioned_import.md diff --git a/docs/rules/no_unversioned_import.md b/docs/rules/no_unversioned_import.md deleted file mode 100644 index c0502cfde..000000000 --- a/docs/rules/no_unversioned_import.md +++ /dev/null @@ -1,17 +0,0 @@ -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"; -``` From 5c7c94f5bd43b6a6f6e107aa82e4b35cacd9b5b1 Mon Sep 17 00:00:00 2001 From: David Sherret Date: Wed, 3 Sep 2025 14:29:07 -0400 Subject: [PATCH 3/5] use deno_semver --- Cargo.lock | 61 ++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/rules/no_unversioned_import.rs | 36 +++++++++--------- 3 files changed, 79 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index da04eba30..1e6fcb76d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -156,6 +156,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f2d24a6dcf0cd402a21b65d35340f3a49ff3475dc5fdac91d22d2733e6641c6" dependencies = [ "capacity_builder_macros", + "ecow", + "hipstr", "itoa", ] @@ -341,6 +343,7 @@ dependencies = [ "clap", "console_static_text", "deno_ast", + "deno_semver", "derive_more", "env_logger", "globwalk", @@ -367,6 +370,23 @@ dependencies = [ "url", ] +[[package]] +name = "deno_semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2625b7107cc3f61a462886d5fa77c23e063c1fd15b90e3d5ee2646e9f6178d55" +dependencies = [ + "capacity_builder", + "deno_error", + "ecow", + "hipstr", + "monch", + "once_cell", + "serde", + "thiserror", + "url", +] + [[package]] name = "deno_terminal" version = "0.2.2" @@ -429,6 +449,15 @@ dependencies = [ "text_lines", ] +[[package]] +name = "ecow" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78e4f79b296fbaab6ce2e22d52cb4c7f010fe0ebe7a32e34fa25885fd797bd02" +dependencies = [ + "serde", +] + [[package]] name = "either" version = "1.15.0" @@ -559,6 +588,17 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +[[package]] +name = "hipstr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97971ffc85d4c98de12e2608e992a43f5294ebb625fdb045b27c731b64c4c6d6" +dependencies = [ + "serde", + "serde_bytes", + "sptr", +] + [[package]] name = "hstr" version = "2.0.1" @@ -780,6 +820,12 @@ version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +[[package]] +name = "monch" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b52c1b33ff98142aecea13138bd399b68aa7ab5d9546c300988c345004001eea" + [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -1091,6 +1137,15 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde_bytes" +version = "0.11.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96" +dependencies = [ + "serde", +] + [[package]] name = "serde_derive" version = "1.0.219" @@ -1160,6 +1215,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "sptr" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a" + [[package]] name = "stable_deref_trait" version = "1.2.0" diff --git a/Cargo.toml b/Cargo.toml index ec72b750d..461a251c0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ derive_more = { version = "0.99.17", features = ["display"] } anyhow = "1.0.79" if_chain = "1.0.2" phf = { version = "0.11.2", features = ["macros"] } +deno_semver = "0.9.0" [dev-dependencies] ansi_term = "0.12.1" diff --git a/src/rules/no_unversioned_import.rs b/src/rules/no_unversioned_import.rs index 43ec7147a..970b9552f 100644 --- a/src/rules/no_unversioned_import.rs +++ b/src/rules/no_unversioned_import.rs @@ -2,11 +2,10 @@ use super::{Context, LintRule}; use crate::handler::{Handler, Traverse}; +use crate::tags::{Tags, RECOMMENDED}; 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; @@ -16,8 +15,8 @@ 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 tags(&self) -> Tags { + &[RECOMMENDED] } fn code(&self) -> &'static str { @@ -31,11 +30,6 @@ impl LintRule for NoUnversionedImport { ) { NoUnversionedImportHandler.traverse(program, context); } - - #[cfg(feature = "docs")] - fn docs(&self) -> &'static str { - include_str!("../../docs/rules/no_unversioned_import.md") - } } struct NoUnversionedImportHandler; @@ -60,19 +54,22 @@ impl Handler for NoUnversionedImportHandler { } } -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); + if let Some(req_ref) = get_package_req_ref(s) { + req_ref.req.version_req.version_text() == "*" + } else { + false } +} - false +fn get_package_req_ref(s: &str) -> Option { + if let Ok(req_ref) = deno_semver::npm::NpmPackageReqReference::from_str(s) { + Some(req_ref.into_inner()) + } else if let Ok(req_ref) = deno_semver::jsr::JsrPackageReqReference::from_str(s) { + Some(req_ref.into_inner()) + } else { + None + } } #[cfg(test)] @@ -89,6 +86,7 @@ mod tests { 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@latest";"#, 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";"#, From 0f0a85fcaa4fa6d4945e056d0ff495b61fae6625 Mon Sep 17 00:00:00 2001 From: David Sherret Date: Wed, 3 Sep 2025 14:40:13 -0400 Subject: [PATCH 4/5] update to be more precise --- src/rules/no_unversioned_import.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rules/no_unversioned_import.rs b/src/rules/no_unversioned_import.rs index 970b9552f..d61a39655 100644 --- a/src/rules/no_unversioned_import.rs +++ b/src/rules/no_unversioned_import.rs @@ -12,7 +12,7 @@ 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"; +const HINT: &str = "Add a version requirement after the package name"; impl LintRule for NoUnversionedImport { fn tags(&self) -> Tags { From fa74ee39e69f4b3de091cbcc0a40e05c25a60943 Mon Sep 17 00:00:00 2001 From: David Sherret Date: Wed, 3 Sep 2025 14:40:59 -0400 Subject: [PATCH 5/5] format --- src/rules/no_unversioned_import.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/rules/no_unversioned_import.rs b/src/rules/no_unversioned_import.rs index d61a39655..36fa4b28c 100644 --- a/src/rules/no_unversioned_import.rs +++ b/src/rules/no_unversioned_import.rs @@ -62,10 +62,14 @@ fn is_unversioned(s: &str) -> bool { } } -fn get_package_req_ref(s: &str) -> Option { +fn get_package_req_ref( + s: &str, +) -> Option { if let Ok(req_ref) = deno_semver::npm::NpmPackageReqReference::from_str(s) { Some(req_ref.into_inner()) - } else if let Ok(req_ref) = deno_semver::jsr::JsrPackageReqReference::from_str(s) { + } else if let Ok(req_ref) = + deno_semver::jsr::JsrPackageReqReference::from_str(s) + { Some(req_ref.into_inner()) } else { None