Skip to content

Commit ee8bb90

Browse files
feat: add no-unversioned-import rule (#1362)
1 parent 1ee9c49 commit ee8bb90

File tree

5 files changed

+214
-0
lines changed

5 files changed

+214
-0
lines changed

Cargo.lock

Lines changed: 61 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ derive_more = { version = "0.99.17", features = ["display"] }
3333
anyhow = "1.0.79"
3434
if_chain = "1.0.2"
3535
phf = { version = "0.11.2", features = ["macros"] }
36+
deno_semver = "0.9.0"
3637

3738
[dev-dependencies]
3839
ansi_term = "0.12.1"

schemas/rules.v1.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@
106106
"no-unsafe-negation",
107107
"no-unused-labels",
108108
"no-unused-vars",
109+
"no-unversioned-import",
109110
"no-useless-rename",
110111
"no-var",
111112
"no-window",

src/rules.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ pub mod no_unsafe_finally;
109109
pub mod no_unsafe_negation;
110110
pub mod no_unused_labels;
111111
pub mod no_unused_vars;
112+
pub mod no_unversioned_import;
112113
pub mod no_useless_rename;
113114
pub mod no_var;
114115
pub mod no_window;
@@ -355,6 +356,7 @@ fn get_all_rules_raw() -> Vec<Box<dyn LintRule>> {
355356
Box::new(no_unsafe_negation::NoUnsafeNegation),
356357
Box::new(no_unused_labels::NoUnusedLabels),
357358
Box::new(no_unused_vars::NoUnusedVars),
359+
Box::new(no_unversioned_import::NoUnversionedImport),
358360
Box::new(no_useless_rename::NoUselessRename),
359361
Box::new(no_var::NoVar),
360362
Box::new(no_window::NoWindow),

src/rules/no_unversioned_import.rs

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
2+
3+
use super::{Context, LintRule};
4+
use crate::handler::{Handler, Traverse};
5+
use crate::tags::{Tags, RECOMMENDED};
6+
use crate::Program;
7+
use deno_ast::view::{CallExpr, Callee, Expr, ImportDecl, Lit};
8+
use deno_ast::SourceRanged;
9+
10+
#[derive(Debug)]
11+
pub struct NoUnversionedImport;
12+
13+
const CODE: &str = "no-unversioned-import";
14+
const MESSAGE: &str = "Missing version in specifier";
15+
const HINT: &str = "Add a version requirement after the package name";
16+
17+
impl LintRule for NoUnversionedImport {
18+
fn tags(&self) -> Tags {
19+
&[RECOMMENDED]
20+
}
21+
22+
fn code(&self) -> &'static str {
23+
CODE
24+
}
25+
26+
fn lint_program_with_ast_view(
27+
&self,
28+
context: &mut Context,
29+
program: Program<'_>,
30+
) {
31+
NoUnversionedImportHandler.traverse(program, context);
32+
}
33+
}
34+
35+
struct NoUnversionedImportHandler;
36+
37+
impl Handler for NoUnversionedImportHandler {
38+
fn import_decl(&mut self, node: &ImportDecl, ctx: &mut Context) {
39+
if is_unversioned(node.src.value()) {
40+
ctx.add_diagnostic_with_hint(node.src.range(), CODE, MESSAGE, HINT);
41+
}
42+
}
43+
44+
fn call_expr(&mut self, node: &CallExpr, ctx: &mut Context) {
45+
if let Callee::Import(_) = node.callee {
46+
if let Some(arg) = node.args.first() {
47+
if let Expr::Lit(Lit::Str(lit)) = arg.expr {
48+
if is_unversioned(lit.value()) {
49+
ctx.add_diagnostic_with_hint(arg.range(), CODE, MESSAGE, HINT);
50+
}
51+
}
52+
}
53+
}
54+
}
55+
}
56+
57+
fn is_unversioned(s: &str) -> bool {
58+
if let Some(req_ref) = get_package_req_ref(s) {
59+
req_ref.req.version_req.version_text() == "*"
60+
} else {
61+
false
62+
}
63+
}
64+
65+
fn get_package_req_ref(
66+
s: &str,
67+
) -> Option<deno_semver::package::PackageReqReference> {
68+
if let Ok(req_ref) = deno_semver::npm::NpmPackageReqReference::from_str(s) {
69+
Some(req_ref.into_inner())
70+
} else if let Ok(req_ref) =
71+
deno_semver::jsr::JsrPackageReqReference::from_str(s)
72+
{
73+
Some(req_ref.into_inner())
74+
} else {
75+
None
76+
}
77+
}
78+
79+
#[cfg(test)]
80+
mod tests {
81+
use super::*;
82+
83+
#[test]
84+
fn no_with_valid() {
85+
assert_lint_ok! {
86+
NoUnversionedImport,
87+
r#"import foo from "foo";"#,
88+
r#"import foo from "@foo/bar";"#,
89+
r#"import foo from "./foo";"#,
90+
r#"import foo from "../foo";"#,
91+
r#"import foo from "~/foo";"#,
92+
r#"import foo from "npm:[email protected]";"#,
93+
r#"import foo from "npm:foo@latest";"#,
94+
r#"import foo from "npm:foo@^1.2.3";"#,
95+
r#"import foo from "npm:@foo/[email protected]";"#,
96+
r#"import foo from "npm:@foo/bar@^1.2.3";"#,
97+
r#"import foo from "jsr:@foo/[email protected]";"#,
98+
r#"import foo from "jsr:@foo/bar@^1.2.3";"#,
99+
r#"import("foo")"#,
100+
r#"import("@foo/bar")"#,
101+
r#"import("./foo")"#,
102+
r#"import("../foo")"#,
103+
r#"import("~/foo")"#,
104+
r#"import("npm:[email protected]")"#,
105+
r#"import("npm:foo@^1.2.3")"#,
106+
r#"import("npm:@foo/[email protected]")"#,
107+
r#"import("npm:@foo/bar@^1.2.3")"#,
108+
r#"import("jsr:@foo/[email protected]")"#,
109+
r#"import("jsr:@foo/bar@^1.2.3")"#,
110+
}
111+
}
112+
113+
#[test]
114+
fn no_with_invalid() {
115+
assert_lint_err! {
116+
NoUnversionedImport,
117+
r#"import foo from "jsr:@foo/foo";"#: [{
118+
col: 16,
119+
message: MESSAGE,
120+
hint: HINT
121+
}],
122+
r#"import foo from "npm:foo";"#: [{
123+
col: 16,
124+
message: MESSAGE,
125+
hint: HINT
126+
}],
127+
r#"import foo from "npm:@foo/bar";"#: [{
128+
col: 16,
129+
message: MESSAGE,
130+
hint: HINT
131+
}],
132+
r#"import("jsr:@foo/foo");"#: [{
133+
col: 7,
134+
message: MESSAGE,
135+
hint: HINT
136+
}],
137+
r#"import("npm:foo");"#: [{
138+
col: 7,
139+
message: MESSAGE,
140+
hint: HINT
141+
}],
142+
r#"import("npm:@foo/bar");"#: [{
143+
col: 7,
144+
message: MESSAGE,
145+
hint: HINT
146+
}],
147+
}
148+
}
149+
}

0 commit comments

Comments
 (0)