diff --git a/Cargo.lock b/Cargo.lock index da04eba3..1e6fcb76 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 ec72b750..461a251c 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/schemas/rules.v1.json b/schemas/rules.v1.json index 67589d9d..8723a857 100644 --- a/schemas/rules.v1.json +++ b/schemas/rules.v1.json @@ -106,6 +106,7 @@ "no-unsafe-negation", "no-unused-labels", "no-unused-vars", + "no-unversioned-import", "no-useless-rename", "no-var", "no-window", diff --git a/src/rules.rs b/src/rules.rs index 0c7fe926..b5f9fcfc 100644 --- a/src/rules.rs +++ b/src/rules.rs @@ -109,6 +109,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_useless_rename; pub mod no_var; pub mod no_window; @@ -355,6 +356,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_useless_rename::NoUselessRename), Box::new(no_var::NoVar), Box::new(no_window::NoWindow), diff --git a/src/rules/no_unversioned_import.rs b/src/rules/no_unversioned_import.rs new file mode 100644 index 00000000..36fa4b28 --- /dev/null +++ b/src/rules/no_unversioned_import.rs @@ -0,0 +1,149 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +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; + +#[derive(Debug)] +pub struct NoUnversionedImport; + +const CODE: &str = "no-unversioned-import"; +const MESSAGE: &str = "Missing version in specifier"; +const HINT: &str = "Add a version requirement after the package name"; + +impl LintRule for NoUnversionedImport { + fn tags(&self) -> Tags { + &[RECOMMENDED] + } + + fn code(&self) -> &'static str { + CODE + } + + fn lint_program_with_ast_view( + &self, + context: &mut Context, + program: Program<'_>, + ) { + NoUnversionedImportHandler.traverse(program, context); + } +} + +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); + } + } + } + } + } +} + +fn is_unversioned(s: &str) -> bool { + if let Some(req_ref) = get_package_req_ref(s) { + req_ref.req.version_req.version_text() == "*" + } else { + 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)] +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@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";"#, + 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 + }], + } + } +}