Skip to content

Commit ec9930b

Browse files
committed
fix(linter/no-unused-vars): report more type references within own declarations
1 parent 2cba7af commit ec9930b

File tree

5 files changed

+237
-9
lines changed

5 files changed

+237
-9
lines changed

crates/oxc_linter/src/rules/eslint/no_unused_vars/tests/oxc.rs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ use serde_json::json;
55
use super::NoUnusedVars;
66
use crate::{tester::Tester, FixKind, RuleMeta as _};
77

8+
// uncomment to only run a single test. useful for step-through debugging.
9+
// #[test]
10+
// fn test_debug() {
11+
// let pass: Vec<&str> = vec![];
12+
// let fail = vec![];
13+
// Tester::new(NoUnusedVars::NAME, pass, fail).intentionally_allow_no_fix_tests().test();
14+
// }
15+
816
#[test]
917
fn test_vars_simple() {
1018
let pass = vec![
@@ -618,6 +626,60 @@ fn test_functions() {
618626
.test_and_snapshot();
619627
}
620628

629+
#[test]
630+
fn test_self_call() {
631+
let pass = vec![
632+
"const _thunk = (function createThunk(count) {
633+
if (count === 0) return () => count
634+
return () => createThunk(count - 1)()
635+
})()",
636+
];
637+
638+
let fail = vec![
639+
// Functions that call themselves are considered unused, even if that
640+
// call happens within an inner function.
641+
"function foo() { return function bar() { return foo() } }",
642+
// Classes that construct themselves are considered unused
643+
"class Foo {
644+
static createFoo() {
645+
return new Foo();
646+
}
647+
}",
648+
"class Foo {
649+
static createFoo(): Foo {
650+
return new Foo();
651+
}
652+
}",
653+
"class Point {
654+
public x: number;
655+
public y: number;
656+
public add(other): Point {
657+
const p = new Point();
658+
p.x = this.x + (other as Point).x;
659+
p.y = this.y + (other as Point).y;
660+
return p;
661+
}
662+
}
663+
",
664+
// FIXME
665+
// "class Foo {
666+
// inner: any
667+
// public foo(): Foo {
668+
// if(this.inner?.constructor.name === Foo.name) {
669+
// return this.inner;
670+
// } else {
671+
// return new Foo();
672+
// }
673+
// }
674+
// }",
675+
];
676+
677+
Tester::new(NoUnusedVars::NAME, pass, fail)
678+
.intentionally_allow_no_fix_tests()
679+
.with_snapshot_suffix("oxc-self-call")
680+
.test_and_snapshot();
681+
}
682+
621683
#[test]
622684
fn test_imports() {
623685
let pass = vec![
@@ -993,6 +1055,7 @@ fn test_type_aliases() {
9931055
"type Foo = Foo",
9941056
"type Foo = Array<Foo>",
9951057
"type Unbox<B> = B extends Box<infer R> ? Unbox<R> : B",
1058+
"export type F<T> = T extends infer R ? /* R not used */ string : never",
9961059
];
9971060

9981061
Tester::new(NoUnusedVars::NAME, pass, fail)
@@ -1051,13 +1114,52 @@ fn test_type_references() {
10511114
];
10521115

10531116
let fail = vec![
1117+
// Type aliases
10541118
"type T = number; function foo<T>(a: T): T { return a as T }; foo(1)",
10551119
"type A = number; type B<A> = A; console.log(3 as B<3>)",
1120+
"type T = { foo: T }",
1121+
"type T = { foo?: T | undefined }",
1122+
"type A<T> = { foo: T extends Array<infer R> ? A<R> : T }",
1123+
"type T = { foo(): T }",
1124+
// Type references on class symbols within that classes' definition is
1125+
// not considered used
1126+
"class Foo {
1127+
private _inner: Foo | undefined;
1128+
}",
1129+
"class Foo {
1130+
_inner: any;
1131+
constructor(other: Foo);
1132+
constructor(somethingElse: any) {
1133+
this._inner = somethingElse;
1134+
}
1135+
}",
1136+
"class LinkedList<T> {
1137+
#next?: LinkedList<T>;
1138+
public append(other: LinkedList<T>) {
1139+
this.#next = other;
1140+
}
1141+
",
1142+
"class LinkedList<T> {
1143+
#next?: LinkedList<T>;
1144+
public nextUnchecked(): LinkedList<T> {
1145+
return <LinkedList<T>>this.#next!;
1146+
}
1147+
",
1148+
// FIXME: ambient classes declared using `declare` are not bound by
1149+
// semantic's binder.
1150+
// https://github.com/oxc-project/oxc/blob/a9260cf6d1b83917c7a61b25cabd2d40858b0fff/crates/oxc_semantic/src/binder.rs#L105
1151+
// "declare class LinkedList<T> {
1152+
// next(): LinkedList<T> | undefined;
1153+
// }"
1154+
1155+
// Same is true for interfaces
1156+
"interface LinkedList<T> { next: LinkedList<T> | undefined }",
10561157
];
10571158

10581159
Tester::new(NoUnusedVars::NAME, pass, fail)
10591160
.intentionally_allow_no_fix_tests()
10601161
.with_snapshot_suffix("oxc-type-references")
1162+
.change_rule_path_extension("ts")
10611163
.test_and_snapshot();
10621164
}
10631165

crates/oxc_linter/src/rules/eslint/no_unused_vars/usage.rs

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -60,16 +60,18 @@ impl<'s, 'a> Symbol<'s, 'a> {
6060
}
6161

6262
#[inline]
63-
const fn is_type_alias(&self) -> bool {
64-
self.flags().contains(SymbolFlags::TypeAlias)
63+
const fn could_have_type_reference_within_own_decl(&self) -> bool {
64+
const TYPE_DECLS: SymbolFlags =
65+
SymbolFlags::TypeAlias.union(SymbolFlags::Interface).union(SymbolFlags::Class);
66+
self.flags().intersects(TYPE_DECLS)
6567
}
6668

6769
/// Check if this [`Symbol`] has an [`Reference`]s that are considered a usage.
6870
pub fn has_usages(&self, options: &NoUnusedVars) -> bool {
6971
// Use symbol flags to skip the usage checks we are certain don't need
7072
// to be run.
7173
let do_reassignment_checks = self.is_possibly_reassignable();
72-
let do_type_self_usage_checks = self.is_type_alias();
74+
let do_type_self_usage_checks = self.could_have_type_reference_within_own_decl();
7375
let do_self_call_check = self.is_maybe_callable();
7476
let do_discarded_read_checks = self.is_definitely_reassignable_variable();
7577

@@ -251,12 +253,12 @@ impl<'s, 'a> Symbol<'s, 'a> {
251253
}
252254
// definitely not within a type alias, we can be sure this isn't
253255
// a self-usage. Safe CPU cycles by breaking early.
254-
AstKind::CallExpression(_)
255-
| AstKind::BinaryExpression(_)
256-
| AstKind::Function(_)
257-
| AstKind::Class(_)
258-
| AstKind::TSInterfaceDeclaration(_)
259-
| AstKind::TSModuleDeclaration(_)
256+
// NOTE: we cannot short-circuit on functions since they could
257+
// be methods with annotations referencing the type they're in.
258+
// e.g.:
259+
// - `type Foo = { bar(): Foo }`
260+
// - `class Foo { static factory(): Foo { return new Foo() } }`
261+
AstKind::TSModuleDeclaration(_)
260262
| AstKind::VariableDeclaration(_)
261263
| AstKind::VariableDeclarator(_)
262264
| AstKind::ExportNamedDeclaration(_)
@@ -266,6 +268,27 @@ impl<'s, 'a> Symbol<'s, 'a> {
266268
return false;
267269
}
268270

271+
AstKind::CallExpression(_) | AstKind::BinaryExpression(_) => {
272+
// interfaces/type aliases cannot have value expressions
273+
// within their declarations, so we know we're not in one.
274+
// However, classes can.
275+
if self.flags().is_class() {
276+
continue;
277+
}
278+
return false;
279+
}
280+
281+
// `interface LinkedList<T> { next?: LinkedList<T> }`
282+
AstKind::TSInterfaceDeclaration(iface) => {
283+
return self.flags().is_interface() && self == &iface.id;
284+
}
285+
286+
// `class Foo { bar(): Foo }`
287+
AstKind::Class(class) => {
288+
return self.flags().is_class()
289+
&& class.id.as_ref().is_some_and(|id| self == id);
290+
}
291+
269292
_ => continue,
270293
}
271294
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
source: crates/oxc_linter/src/tester.rs
3+
---
4+
eslint(no-unused-vars): Function 'foo' is declared but never used.
5+
╭─[no_unused_vars.tsx:1:10]
6+
1function foo() { return function bar() { return foo() } }
7+
· ─┬─
8+
· ╰── 'foo' is declared here
9+
╰────
10+
help: Consider removing this declaration.
11+
12+
eslint(no-unused-vars): Class 'Foo' is declared but never used.
13+
╭─[no_unused_vars.tsx:1:7]
14+
1class Foo {
15+
· ─┬─
16+
· ╰── 'Foo' is declared here
17+
2static createFoo() {
18+
╰────
19+
help: Consider removing this declaration.
20+
21+
eslint(no-unused-vars): Class 'Foo' is declared but never used.
22+
╭─[no_unused_vars.tsx:1:7]
23+
1class Foo {
24+
· ─┬─
25+
· ╰── 'Foo' is declared here
26+
2static createFoo(): Foo {
27+
╰────
28+
help: Consider removing this declaration.
29+
30+
eslint(no-unused-vars): Class 'Point' is declared but never used.
31+
╭─[no_unused_vars.tsx:1:7]
32+
1class Point {
33+
· ──┬──
34+
· ╰── 'Point' is declared here
35+
2public x: number;
36+
╰────
37+
help: Consider removing this declaration.

crates/oxc_linter/src/snapshots/[email protected]

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,11 @@ source: crates/oxc_linter/src/tester.rs
2424
· ╰── 'Unbox' is declared here
2525
╰────
2626
help: Consider removing this declaration.
27+
28+
eslint(no-unused-vars): Variable 'R' is declared but never used.
29+
╭─[no_unused_vars.tsx:1:36]
30+
1export type F<T> = T extends infer R ? /* R not used */ string : never
31+
· ┬
32+
· ╰── 'R' is declared here
33+
╰────
34+
help: Consider removing this declaration.

crates/oxc_linter/src/snapshots/[email protected]

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,61 @@ source: crates/oxc_linter/src/tester.rs
1616
· ╰── 'A' is declared here
1717
╰────
1818
help: Consider removing this declaration.
19+
20+
eslint(no-unused-vars): Type alias 'T' is declared but never used.
21+
╭─[no_unused_vars.tsx:1:6]
22+
1type T = { foo: T }
23+
· ┬
24+
· ╰── 'T' is declared here
25+
╰────
26+
help: Consider removing this declaration.
27+
28+
eslint(no-unused-vars): Type alias 'T' is declared but never used.
29+
╭─[no_unused_vars.tsx:1:6]
30+
1type T = { foo?: T | undefined }
31+
· ┬
32+
· ╰── 'T' is declared here
33+
╰────
34+
help: Consider removing this declaration.
35+
36+
eslint(no-unused-vars): Type alias 'A' is declared but never used.
37+
╭─[no_unused_vars.tsx:1:6]
38+
1type A<T> = { foo: T extends Array<infer R> ? A<R> : T }
39+
· ┬
40+
· ╰── 'A' is declared here
41+
╰────
42+
help: Consider removing this declaration.
43+
44+
eslint(no-unused-vars): Type alias 'T' is declared but never used.
45+
╭─[no_unused_vars.tsx:1:6]
46+
1type T = { foo(): T }
47+
· ┬
48+
· ╰── 'T' is declared here
49+
╰────
50+
help: Consider removing this declaration.
51+
52+
eslint(no-unused-vars): Class 'Foo' is declared but never used.
53+
╭─[no_unused_vars.tsx:1:7]
54+
1class Foo {
55+
· ─┬─
56+
· ╰── 'Foo' is declared here
57+
2private _inner: Foo | undefined;
58+
╰────
59+
help: Consider removing this declaration.
60+
61+
⚠ eslint(no-unused-vars): Class 'Foo' is declared but never used.
62+
╭─[no_unused_vars.tsx:1:7]
63+
1 │ class Foo {
64+
· ─┬─
65+
· ╰── 'Foo' is declared here
66+
2_inner: any;
67+
╰────
68+
help: Consider removing this declaration.
69+
70+
eslint(no-unused-vars): Interface 'LinkedList' is declared but never used.
71+
╭─[no_unused_vars.tsx:1:11]
72+
1interface LinkedList<T> { next: LinkedList<T> | undefined }
73+
· ─────┬────
74+
· ╰── 'LinkedList' is declared here
75+
╰────
76+
help: Consider removing this declaration.

0 commit comments

Comments
 (0)