Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(linter/import) implement no_unused_modules rule #2720

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
108 changes: 94 additions & 14 deletions crates/oxc_linter/src/rules/import/no_unused_modules.rs
@@ -1,42 +1,122 @@
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),
}

/// <https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/namespace.md>
/// <https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/no-unused-modules.md>
#[derive(Debug, Default, Clone)]
pub struct NoUnusedModules;
pub struct NoUnusedModules {
missing_exports: bool,
unused_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),
unused_exports: value
.get("unusedExports")
.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)));
}
if self.unused_exports {
// TODO: implement unused exports
}
}
}

#[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();

// TODO: support unused exports
// 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![];

// Tester::new(NoUnusedModules::NAME, pass, fail)
// .change_rule_path("index.js")
// .with_import_plugin(true)
// .test_and_snapshot();
// .change_rule_path("unused-exports.js")
// .with_import_plugin(true)
// .test_and_snapshot();
}
15 changes: 15 additions & 0 deletions 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 */
· ▲
╰────