Skip to content

Commit

Permalink
feat: add no-unversioned-import rule
Browse files Browse the repository at this point in the history
  • Loading branch information
marvinhagemeister committed Nov 22, 2024
1 parent 5a7ce21 commit 7519574
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 0 deletions.
17 changes: 17 additions & 0 deletions docs/rules/no_unversioned_import.md
Original file line number Diff line number Diff line change
@@ -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:[email protected]";
import foo from "npm:chalk@^5.3.0";
import foo from "jsr:@std/[email protected]";
import foo from "jsr:@std/path@^1.0.8";
```
1 change: 1 addition & 0 deletions schemas/rules.v1.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
"no-unsafe-negation",
"no-unused-labels",
"no-unused-vars",
"no-unversioned-import",
"no-var",
"no-window",
"no-window-prefix",
Expand Down
2 changes: 2 additions & 0 deletions src/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -330,6 +331,7 @@ fn get_all_rules_raw() -> Vec<Box<dyn LintRule>> {
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),
Expand Down
147 changes: 147 additions & 0 deletions src/rules/no_unversioned_import.rs
Original file line number Diff line number Diff line change
@@ -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<Regex> =
Lazy::new(|| Regex::new(r"^npm:(@.+\/[^@]+|[^@]+)$").unwrap());
static JSR_REG: Lazy<Regex> =
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:[email protected]";"#,
r#"import foo from "npm:foo@^1.2.3";"#,
r#"import foo from "npm:@foo/[email protected]";"#,
r#"import foo from "npm:@foo/bar@^1.2.3";"#,
r#"import foo from "jsr:@foo/[email protected]";"#,
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:[email protected]")"#,
r#"import("npm:foo@^1.2.3")"#,
r#"import("npm:@foo/[email protected]")"#,
r#"import("npm:@foo/bar@^1.2.3")"#,
r#"import("jsr:@foo/[email protected]")"#,
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
}],
}
}
}
5 changes: 5 additions & 0 deletions www/static/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:[email protected]\";\nimport foo from \"npm:chalk@^5.3.0\";\nimport foo from \"jsr:@std/[email protected]\";\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",
Expand Down

0 comments on commit 7519574

Please sign in to comment.