diff --git a/crates/oxc_linter/src/rules/eslint/no_unused_vars/allowed.rs b/crates/oxc_linter/src/rules/eslint/no_unused_vars/allowed.rs index 0461804773082..bebf230e0ae1b 100644 --- a/crates/oxc_linter/src/rules/eslint/no_unused_vars/allowed.rs +++ b/crates/oxc_linter/src/rules/eslint/no_unused_vars/allowed.rs @@ -26,7 +26,13 @@ impl<'s, 'a> Symbol<'s, 'a> { for parent in self.iter_parents() { match parent.kind() { - AstKind::MemberExpression(_) | AstKind::ParenthesizedExpression(_) => { + AstKind::MemberExpression(_) | AstKind::ParenthesizedExpression(_) + // e.g. `const x = [function foo() {}]` + // Only considered used if the array containing the symbol is used. + | AstKind::ArrayExpressionElement(_) + | AstKind::ExpressionArrayElement(_) + | AstKind::ArrayExpression(_) + => { continue; } // Returned from another function. Definitely won't be the same @@ -37,6 +43,9 @@ impl<'s, 'a> Symbol<'s, 'a> { // Function declaration is passed as an argument to another function. | AstKind::CallExpression(_) | AstKind::Argument(_) // e.g. `const x = { foo: function foo() {} }` + // Allowed off-the-bat since objects being the only child of an + // ExpressionStatement is rare, since you would need to wrap the + // object in parentheses to avoid creating a block statement. | AstKind::ObjectProperty(_) // e.g. var foo = function bar() { } // we don't want to check for violations on `bar`, just `foo` 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 3094c4d7927ea..92555dc9dbdee 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 @@ -689,6 +689,37 @@ fn test_imports() { .test_and_snapshot(); } +#[test] +fn test_used_declarations() { + let pass = vec![ + // function declarations passed as arguments, used in assignments, etc. are used, even if they are + // first put into an intermediate (e.g. an object or array) + "arr.reduce(function reducer (acc, el) { return acc + el }, 0)", + "console.log({ foo: function foo() {} })", + "test.each([ function foo() {} ])('test some function', (fn) => { expect(fn(1)).toBe(1) })", + "export default { foo() {} }", + "const arr = [function foo() {}, function bar() {}]; console.log(arr[0]())", + "const foo = function foo() {}; console.log(foo())", + "const foo = function bar() {}; console.log(foo())", + // Class expressions behave similarly + "console.log([class Foo {}])", + "export default { foo: class Foo {} }", + "export const Foo = class Foo {}", + "export const Foo = class Bar {}", + "export const Foo = @SomeDecorator() class Foo {}", + ]; + let fail = vec![ + // array is not used, so the function is not used + ";[function foo() {}]", + ";[class Foo {}]", + ]; + + Tester::new(NoUnusedVars::NAME, pass, fail) + .intentionally_allow_no_fix_tests() + .with_snapshot_suffix("oxc-used-declarations") + .test_and_snapshot(); +} + #[test] fn test_exports() { let pass = vec![ diff --git a/crates/oxc_linter/src/snapshots/no_unused_vars@oxc-used-declarations.snap b/crates/oxc_linter/src/snapshots/no_unused_vars@oxc-used-declarations.snap new file mode 100644 index 0000000000000..912dd129ec9fc --- /dev/null +++ b/crates/oxc_linter/src/snapshots/no_unused_vars@oxc-used-declarations.snap @@ -0,0 +1,18 @@ +--- +source: crates/oxc_linter/src/tester.rs +--- + ⚠ eslint(no-unused-vars): Function 'foo' is declared but never used. + ╭─[no_unused_vars.tsx:1:12] + 1 │ ;[function foo() {}] + · ─┬─ + · ╰── 'foo' is declared here + ╰──── + help: Consider removing this declaration. + + ⚠ eslint(no-unused-vars): Class 'Foo' is declared but never used. + ╭─[no_unused_vars.tsx:1:9] + 1 │ ;[class Foo {}] + · ─┬─ + · ╰── 'Foo' is declared here + ╰──── + help: Consider removing this declaration.