diff --git a/crates/oxc_linter/src/rules/eslint/no_unused_vars/options.rs b/crates/oxc_linter/src/rules/eslint/no_unused_vars/options.rs index 7c9c2964373f2..569a682fe8e8d 100644 --- a/crates/oxc_linter/src/rules/eslint/no_unused_vars/options.rs +++ b/crates/oxc_linter/src/rules/eslint/no_unused_vars/options.rs @@ -205,6 +205,34 @@ pub struct NoUnusedVarsOptions { /// console.log(firstVar, secondVar); /// ``` pub report_used_ignore_pattern: bool, + + /// The `reportVarsOnlyUsedAsTypes` option is a boolean (default: `false`). + /// + /// If `true`, the rule will also report variables that are only used as types. + /// + /// ## Examples + /// + /// Examples of **incorrect** code for the `{ "reportVarsOnlyUsedAsTypes": true }` option: + /// + /// ```javascript + /// /* eslint no-unused-vars: ["error", { "reportVarsOnlyUsedAsTypes": true }] */ + /// + /// const myNumber: number = 4; + /// export type MyNumber = typeof myNumber + /// ``` + /// + /// Examples of **correct** code for the `{ "reportVarsOnlyUsedAsTypes": true }` option: + /// + /// ```javascript + /// export type MyNumber = number; + /// ``` + /// + /// Note: even with `{ "reportVarsOnlyUsedAsTypes": false }`, cases where the value is + /// only used a type within itself will still be reported: + /// ```javascript + /// function foo(): typeof foo {} + /// ``` + pub report_vars_only_used_as_types: bool, } /// Represents an `Option` with an additional `Default` variant, @@ -314,6 +342,7 @@ impl Default for NoUnusedVarsOptions { destructured_array_ignore_pattern: IgnorePattern::None, ignore_class_with_static_init_block: false, report_used_ignore_pattern: false, + report_vars_only_used_as_types: false, } } } @@ -555,6 +584,11 @@ impl TryFrom for NoUnusedVarsOptions { .map_or(Some(false), Value::as_bool) .unwrap_or(false); + let report_vars_only_used_as_types: bool = config + .get("reportVarsOnlyUsedAsTypes") + .map_or(Some(false), Value::as_bool) + .unwrap_or(false); + Ok(Self { vars, vars_ignore_pattern, @@ -566,6 +600,7 @@ impl TryFrom for NoUnusedVarsOptions { destructured_array_ignore_pattern, ignore_class_with_static_init_block, report_used_ignore_pattern, + report_vars_only_used_as_types, }) } Value::Null => Ok(Self::default()), diff --git a/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/oxc.rs b/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/oxc.rs index 5e32ed09423f9..25bde28fd96ff 100644 --- a/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/oxc.rs +++ b/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/oxc.rs @@ -1253,6 +1253,33 @@ fn test_loops() { Tester::new(NoUnusedVars::NAME, NoUnusedVars::PLUGIN, pass, fail).expect_fix(fix).test(); } +#[test] +fn test_report_vars_only_used_as_types() { + let pass = vec![ + ("const foo = 123; export type Foo = typeof foo;", None), + ( + "const foo = 123; export type Foo = typeof foo;", + Some(json!([{ "reportVarsOnlyUsedAsTypes": false, "varsIgnorePattern": "^_" }])), + ), + ( + "export const foo = 123; export type Foo = typeof foo;", + Some(json!([{ "reportVarsOnlyUsedAsTypes": true, "varsIgnorePattern": "^_" }])), + ), + ]; + + let fail = vec![ + ( + "const foo = 123; export type Foo = typeof foo;", + Some(json!([{ "reportVarsOnlyUsedAsTypes": true, "varsIgnorePattern": "^_" }])), + ), + ("function foo(): typeof foo {}", None), + ]; + + Tester::new(NoUnusedVars::NAME, NoUnusedVars::PLUGIN, pass, fail) + .intentionally_allow_no_fix_tests() + .test(); +} + // #[test] // fn test_template() { // let pass = vec![]; diff --git a/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/typescript_eslint.rs b/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/typescript_eslint.rs index 7861a3667acb4..b6bf3c2454d6c 100644 --- a/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/typescript_eslint.rs +++ b/crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/typescript_eslint.rs @@ -3,8 +3,6 @@ use serde_json::json; use super::NoUnusedVars; use crate::{RuleMeta as _, tester::Tester}; -// TODO: port these over. I (@DonIsaac) would love some help with this... - #[test] fn test() { let pass = vec![ @@ -1731,28 +1729,28 @@ fn test() { const foo: number = 1; export type Foo = typeof foo; ", - None, + Some(json!([{ "reportVarsOnlyUsedAsTypes": true, "varsIgnorePattern": "^_" }])), ), ( " declare const foo: number; export type Foo = typeof foo; ", - None, + Some(json!([{ "reportVarsOnlyUsedAsTypes": true, "varsIgnorePattern": "^_" }])), ), ( " const foo: number = 1; export type Foo = typeof foo | string; ", - None, + Some(json!([{ "reportVarsOnlyUsedAsTypes": true, "varsIgnorePattern": "^_" }])), ), ( " const foo: number = 1; export type Foo = (typeof foo | string) & { __brand: 'foo' }; ", - None, + Some(json!([{ "reportVarsOnlyUsedAsTypes": true, "varsIgnorePattern": "^_" }])), ), ( " @@ -1763,7 +1761,7 @@ fn test() { }; export type Bar = typeof foo.bar; ", - None, + Some(json!([{ "reportVarsOnlyUsedAsTypes": true, "varsIgnorePattern": "^_" }])), ), ( " @@ -1774,7 +1772,7 @@ fn test() { }; export type Bar = (typeof foo)['bar']; ", - None, + Some(json!([{ "reportVarsOnlyUsedAsTypes": true, "varsIgnorePattern": "^_" }])), ), ]; diff --git a/crates/oxc_linter/src/rules/eslint/no_unused_vars/usage.rs b/crates/oxc_linter/src/rules/eslint/no_unused_vars/usage.rs index cee6fda730e5c..c14fc288b498a 100644 --- a/crates/oxc_linter/src/rules/eslint/no_unused_vars/usage.rs +++ b/crates/oxc_linter/src/rules/eslint/no_unused_vars/usage.rs @@ -137,11 +137,25 @@ impl<'a> Symbol<'_, 'a> { continue; } - if !self.flags().intersects(SymbolFlags::TypeImport.union(SymbolFlags::Import)) + // ```ts + // const foo = 123; + // export type Foo = typeof foo + // ``` + if options.report_vars_only_used_as_types + && !self.flags().intersects(SymbolFlags::TypeImport.union(SymbolFlags::Import)) && self.reference_contains_type_query(reference) { continue; } + // ``` + // function foo(): foo { } + // ``` + if self + .get_ref_relevant_node(reference) + .is_some_and(|node| self.declaration().span().contains_inclusive(node.span())) + { + continue; + } return true; }