From 50125a733aa6c4581759799ebb530e3490c215f6 Mon Sep 17 00:00:00 2001 From: Dunqing Date: Thu, 14 Mar 2024 16:37:57 +0800 Subject: [PATCH] feat(linter/import) implement no_unused_modules rule --- .../src/rules/import/no_unused_modules.rs | 102 +++++++++++++++--- .../src/snapshots/no_unused_modules.snap | 15 +++ 2 files changed, 101 insertions(+), 16 deletions(-) create mode 100644 crates/oxc_linter/src/snapshots/no_unused_modules.snap diff --git a/crates/oxc_linter/src/rules/import/no_unused_modules.rs b/crates/oxc_linter/src/rules/import/no_unused_modules.rs index 9887b4e488c3..009aba308836 100644 --- a/crates/oxc_linter/src/rules/import/no_unused_modules.rs +++ b/crates/oxc_linter/src/rules/import/no_unused_modules.rs @@ -1,42 +1,112 @@ use oxc_diagnostics::{ - miette::{self, Diagnostic}, + miette::{self, diagnostic, Diagnostic}, thiserror::Error, }; use oxc_macros::declare_oxc_lint; -use oxc_span::{CompactStr, Span}; +use oxc_span::Span; use crate::{context::LintContext, rule::Rule}; #[derive(Debug, Error, Diagnostic)] -#[error("eslint-plugin-import(namespace): ")] -#[diagnostic(severity(warning), help(""))] -struct NoUnusedModulesDiagnostic(CompactStr, #[label] pub Span); +enum NoUnusedModulesDiagnostic { + #[error("eslint-plugin-import(no-unused-modules): No exports found")] + #[diagnostic(severity(warning))] + NoExportsFound(#[label] Span), +} -/// +/// #[derive(Debug, Default, Clone)] -pub struct NoUnusedModules; +pub struct NoUnusedModules { + missing_exports: bool, +} declare_oxc_lint!( /// ### What it does - /// TODO + /// + /// Reports: + /// * modules without any exports + /// * individual exports not being statically imported or requireed from other modules in the same project + /// * dynamic imports are supported if argument is a literal string NoUnusedModules, nursery ); impl Rule for NoUnusedModules { - fn run_once(&self, _ctx: &LintContext<'_>) {} + fn from_configuration(value: serde_json::Value) -> Self { + Self { + missing_exports: value + .get("missingExports") + .and_then(serde_json::Value::as_bool) + .unwrap_or(false), + } + } + + fn run_once(&self, ctx: &LintContext<'_>) { + let module_record = ctx.semantic().module_record(); + if self.missing_exports && module_record.local_export_entries.is_empty() { + ctx.diagnostic(NoUnusedModulesDiagnostic::NoExportsFound(Span::new(0, 0))); + } + } } #[test] fn test() { - // use crate::tester::Tester; + use crate::tester::Tester; + use serde_json::json; + + let missing_exports_options = json!({ + "missingExports": true, + }); + + let pass = vec![ + ("export default function noOptions() {}", None), + ("export default () => 1", Some(missing_exports_options.clone())), + ("const a = 1; export { a }", Some(missing_exports_options.clone())), + ("function a() { return true }; export { a }", Some(missing_exports_options.clone())), + ("const a = 1; const b = 2; export { a, b }", Some(missing_exports_options.clone())), + ("const a = 1; export default a", Some(missing_exports_options.clone())), + ("export class Foo {}", Some(missing_exports_options.clone())), + ("export const [foobar] = [];", Some(missing_exports_options.clone())), + ("export const [foobar] = foobarFactory();", Some(missing_exports_options.clone())), + ( + "export default function NewComponent () { + return 'I am new component' + }", + Some(missing_exports_options.clone()), + ), + ( + "export default function NewComponent () { + return 'I am new component' + }", + Some(missing_exports_options.clone()), + ), + ]; + + let fail = vec![ + ("const a = 1", Some(missing_exports_options.clone())), + ("/* const a = 1 */", Some(missing_exports_options.clone())), + ]; + + Tester::new(NoUnusedModules::NAME, pass, fail) + .change_rule_path("missing-exports.js") + .with_import_plugin(true) + .test_and_snapshot(); + + let unused_exports_options = json!({ + "unusedExports": true, + "src": ["./no-unused-modules/**/*.js"], + "ignoreExports": ["./no-unused-modules/*ignored*.js"], + }); - // let pass = vec![]; + let pass = vec![ + ("export default function noOptions() {}", None), + ("export default () => 1", Some(unused_exports_options)), + ]; - // let fail = vec![]; + let fail = vec![]; - // Tester::new(NoUnusedModules::NAME, pass, fail) - // .change_rule_path("index.js") - // .with_import_plugin(true) - // .test_and_snapshot(); + Tester::new(NoUnusedModules::NAME, pass, fail) + .change_rule_path("unused-exports.js") + .with_import_plugin(true) + .test_and_snapshot(); } diff --git a/crates/oxc_linter/src/snapshots/no_unused_modules.snap b/crates/oxc_linter/src/snapshots/no_unused_modules.snap new file mode 100644 index 000000000000..1232da87dd30 --- /dev/null +++ b/crates/oxc_linter/src/snapshots/no_unused_modules.snap @@ -0,0 +1,15 @@ +--- +source: crates/oxc_linter/src/tester.rs +expression: no_unused_modules +--- + ⚠ eslint-plugin-import(no-unused-modules): No exports found + ╭─[missing-exports.js:1:1] + 1 │ const a = 1 + · ▲ + ╰──── + + ⚠ eslint-plugin-import(no-unused-modules): No exports found + ╭─[missing-exports.js:1:1] + 1 │ /* const a = 1 */ + · ▲ + ╰────