From f1e364fee526e93007899fe6810e4bb508f10e7a Mon Sep 17 00:00:00 2001 From: Boshen Date: Fri, 23 Feb 2024 18:27:57 +0800 Subject: [PATCH] feat(linter): eslint-plugin-import/no_unresolved (#2475) --- crates/oxc_linter/src/rules.rs | 2 + .../src/rules/import/no_unresolved.rs | 108 ++++++++++++++++++ .../src/snapshots/no_unresolved.snap | 65 +++++++++++ 3 files changed, 175 insertions(+) create mode 100644 crates/oxc_linter/src/rules/import/no_unresolved.rs create mode 100644 crates/oxc_linter/src/snapshots/no_unresolved.snap diff --git a/crates/oxc_linter/src/rules.rs b/crates/oxc_linter/src/rules.rs index 15bc9373a..bd5fd4444 100644 --- a/crates/oxc_linter/src/rules.rs +++ b/crates/oxc_linter/src/rules.rs @@ -17,6 +17,7 @@ mod import { pub mod no_named_as_default; pub mod no_named_as_default_member; pub mod no_self_import; + pub mod no_unresolved; pub mod no_unused_modules; } @@ -557,6 +558,7 @@ oxc_macros::declare_all_lint_rules! { import::no_named_as_default, import::no_named_as_default_member, import::no_self_import, + import::no_unresolved, import::no_unused_modules, jsx_a11y::alt_text, jsx_a11y::anchor_has_content, diff --git a/crates/oxc_linter/src/rules/import/no_unresolved.rs b/crates/oxc_linter/src/rules/import/no_unresolved.rs new file mode 100644 index 000000000..0e615c90e --- /dev/null +++ b/crates/oxc_linter/src/rules/import/no_unresolved.rs @@ -0,0 +1,108 @@ +use oxc_diagnostics::{ + miette::{self, Diagnostic}, + thiserror::Error, +}; +use oxc_macros::declare_oxc_lint; +use oxc_span::Span; + +use crate::{context::LintContext, rule::Rule}; + +#[derive(Debug, Error, Diagnostic)] +#[error("eslint-plugin-import(no-unresolved): Ensure imports point to a file/module that can be resolved")] +#[diagnostic(severity(warning))] +struct NoUnresolvedDiagnostic(#[label] pub Span); + +/// +#[derive(Debug, Default, Clone)] +pub struct NoUnresolved; + +declare_oxc_lint!( + /// ### What it does + /// + /// Ensures an imported module can be resolved to a module on the local filesystem. + NoUnresolved, + nursery +); + +impl Rule for NoUnresolved { + fn run_once(&self, ctx: &LintContext<'_>) { + let module_record = ctx.semantic().module_record(); + + for (specifier, spans) in &module_record.requested_modules { + if !module_record.loaded_modules.contains_key(specifier) { + for span in spans { + ctx.diagnostic(NoUnresolvedDiagnostic(*span)); + } + } + } + } +} + +#[test] +fn test() { + use crate::tester::Tester; + + let pass = vec![ + // TODO: handle malformed file? + // r#"import "./malformed.js""#, + r#"import foo from "./bar";"#, + r"import bar from './bar.js';", + r"import {someThing} from './test-module';", + // TODO: exclude nodejs builtin modules + // r#"import fs from 'fs';"#, + r"import('fs');", + r"import('fs');", + r#"import * as foo from "a""#, + r#"export { foo } from "./bar""#, + r#"export * from "./bar""#, + r"let foo; export { foo }", + r#"export * as bar from "./bar""#, + // parser: parsers.BABEL_OLD + // r#"export bar from "./bar""#, + r#"import foo from "./jsx/MyUnCoolComponent.jsx""#, + r#"var foo = require("./bar")"#, + r#"require("./bar")"#, + r#"require("./does-not-exist")"#, + r#"require("./does-not-exist")"#, + r#"require(["./bar"], function (bar) {})"#, + r#"define(["./bar"], function (bar) {})"#, + r#"require(["./does-not-exist"], function (bar) {})"#, + r#"define(["require", "exports", "module"], function (r, e, m) { })"#, + r#"require(["./does-not-exist"])"#, + r#"define(["./does-not-exist"], function (bar) {})"#, + r#"require("./does-not-exist", "another arg")"#, + r#"proxyquire("./does-not-exist")"#, + r#"(function() {})("./does-not-exist")"#, + r"define([0, foo], function (bar) {})", + r"require(0)", + r"require(foo)", + ]; + + let fail = vec![ + r#"import reallyfake from "./reallyfake/module""#, + r"import bar from './baz';", + r"import bar from './baz';", + r"import bar from './empty-folder';", + r"import { DEEP } from 'in-alternate-root';", + // TODO: dynamic import + // r#"import('in-alternate-root').then(function({DEEP}) {});"#, + r#"export { foo } from "./does-not-exist""#, + r#"export * from "./does-not-exist""#, + // TODO: dynamic import + // r#"import('in-alternate-root').then(function({DEEP}) {});"#, + r#"export * as bar from "./does-not-exist""#, + r#"export bar from "./does-not-exist""#, + r#"var bar = require("./baz")"#, + // TODO: require expression + // r#"require("./baz")"#, + // TODO: amd + // r#"require(["./baz"], function (bar) {})"#, + // r#"define(["./baz"], function (bar) {})"#, + // r#"define(["./baz", "./bar", "./does-not-exist"], function (bar) {})"#, + ]; + + Tester::new(NoUnresolved::NAME, pass, fail) + .change_rule_path("index.js") + .with_import_plugin(true) + .test_and_snapshot(); +} diff --git a/crates/oxc_linter/src/snapshots/no_unresolved.snap b/crates/oxc_linter/src/snapshots/no_unresolved.snap new file mode 100644 index 000000000..50febdff1 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/no_unresolved.snap @@ -0,0 +1,65 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: no_unresolved +--- + + ⚠ eslint-plugin-import(no-unresolved): Ensure imports point to a file/module that can be resolved + ╭─[index.js:1:24] + 1 │ import reallyfake from "./reallyfake/module" + · ───────────────────── + ╰──── + + ⚠ eslint-plugin-import(no-unresolved): Ensure imports point to a file/module that can be resolved + ╭─[index.js:1:17] + 1 │ import bar from './baz'; + · ─────── + ╰──── + + ⚠ eslint-plugin-import(no-unresolved): Ensure imports point to a file/module that can be resolved + ╭─[index.js:1:17] + 1 │ import bar from './baz'; + · ─────── + ╰──── + + ⚠ eslint-plugin-import(no-unresolved): Ensure imports point to a file/module that can be resolved + ╭─[index.js:1:17] + 1 │ import bar from './empty-folder'; + · ──────────────── + ╰──── + + ⚠ eslint-plugin-import(no-unresolved): Ensure imports point to a file/module that can be resolved + ╭─[index.js:1:22] + 1 │ import { DEEP } from 'in-alternate-root'; + · ─────────────────── + ╰──── + + ⚠ eslint-plugin-import(no-unresolved): Ensure imports point to a file/module that can be resolved + ╭─[index.js:1:21] + 1 │ export { foo } from "./does-not-exist" + · ────────────────── + ╰──── + + ⚠ eslint-plugin-import(no-unresolved): Ensure imports point to a file/module that can be resolved + ╭─[index.js:1:15] + 1 │ export * from "./does-not-exist" + · ────────────────── + ╰──── + + ⚠ eslint-plugin-import(no-unresolved): Ensure imports point to a file/module that can be resolved + ╭─[index.js:1:22] + 1 │ export * as bar from "./does-not-exist" + · ────────────────── + ╰──── + + × Unexpected token + ╭─[index.js:1:8] + 1 │ export bar from "./does-not-exist" + · ─── + ╰──── + + ⚠ eslint-plugin-import(no-unresolved): Ensure imports point to a file/module that can be resolved + ╭─[index.js:1:19] + 1 │ var bar = require("./baz") + · ─────── + ╰──── +