fix(linter/no-unused-vars): false positive for functions and classes in arrays (#6394)

Fixes a false positive when a function or class expression is an array element.
Found while linting Jest tests in an internal repo.

```js
import { stringify } from './custom-stringify';

test.each(['a', 1, function foo() {}])('When stringifying %p, matches the snapshot', input => {
    expect(stringify(input)).toMatchSnapshot();
})
```
This commit is contained in:
DonIsaac 2024-10-09 19:43:56 +00:00
parent 994b60b420
commit e08f956c00
3 changed files with 59 additions and 1 deletions

View file

@ -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`

View file

@ -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![

View file

@ -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.