Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions crates/oxc_linter/src/rules/eslint/no_unused_vars/diagnostic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,13 @@ where
pronoun_singular.cow_to_ascii_lowercase()
))
}

/// Variable 'x' is declared but never used.
pub fn declared<R>(symbol: &Symbol<'_, '_>, pat: &IgnorePattern<R>) -> OxcDiagnostic
pub fn declared<R>(
symbol: &Symbol<'_, '_>,
pat: &IgnorePattern<R>,
only_used_as_type: bool,
) -> OxcDiagnostic
where
R: fmt::Display,
{
Expand All @@ -71,9 +76,13 @@ where
let (pronoun, pronoun_plural) = pronoun_for_symbol(symbol.flags());
let suffix = pat.diagnostic_help(pronoun_plural);

OxcDiagnostic::warn(format!("{pronoun} '{name}' is {verb} but never used.{suffix}"))
.with_label(symbol.span().label(format!("'{name}' is declared here")))
.with_help(help)
OxcDiagnostic::warn(if only_used_as_type {
format!("{pronoun} is {verb} but only used as a type{suffix}.",)
} else {
format!("{pronoun} '{name}' is {verb} but never used.{suffix}")
})
.with_label(symbol.span().label(format!("'{name}' is declared here")))
.with_help(help)
}

/// Variable 'x' is assigned a value but never used.
Expand Down
25 changes: 16 additions & 9 deletions crates/oxc_linter/src/rules/eslint/no_unused_vars/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,11 @@ impl NoUnusedVars {
let span = ctx.nodes().get_node(last_write.node_id()).kind().span();
diagnostic::assign(symbol, span, &self.vars_ignore_pattern)
}
_ => diagnostic::declared(symbol, &self.vars_ignore_pattern),
_ => diagnostic::declared(
symbol,
&self.vars_ignore_pattern,
symbol.has_reference_used_as_type_query(),
),
};

ctx.diagnostic_with_suggestion(report, |fixer| {
Expand All @@ -309,30 +313,34 @@ impl NoUnusedVars {
if NoUnusedVars::is_allowed_binding_rest_element(symbol) {
return;
}
ctx.diagnostic(diagnostic::declared(symbol, &self.vars_ignore_pattern));
ctx.diagnostic(diagnostic::declared(symbol, &self.vars_ignore_pattern, false));
}
AstKind::TSModuleDeclaration(namespace) => {
if self.is_allowed_ts_namespace(symbol, namespace) {
return;
}
ctx.diagnostic(diagnostic::declared(symbol, &IgnorePattern::<&str>::None));
ctx.diagnostic(diagnostic::declared(symbol, &IgnorePattern::<&str>::None, false));
}
AstKind::TSInterfaceDeclaration(_) => {
if symbol.is_in_declared_module() {
return;
}
ctx.diagnostic(diagnostic::declared(symbol, &IgnorePattern::<&str>::None));
ctx.diagnostic(diagnostic::declared(symbol, &IgnorePattern::<&str>::None, false));
}
AstKind::TSTypeParameter(_) => {
if self.is_allowed_type_parameter(symbol, declaration.id()) {
return;
}
ctx.diagnostic(diagnostic::declared(symbol, &self.vars_ignore_pattern));
ctx.diagnostic(diagnostic::declared(symbol, &self.vars_ignore_pattern, false));
}
AstKind::CatchParameter(_) => {
ctx.diagnostic(diagnostic::declared(symbol, &self.caught_errors_ignore_pattern));
ctx.diagnostic(diagnostic::declared(
symbol,
&self.caught_errors_ignore_pattern,
false,
));
}
_ => ctx.diagnostic(diagnostic::declared(symbol, &IgnorePattern::<&str>::None)),
_ => ctx.diagnostic(diagnostic::declared(symbol, &IgnorePattern::<&str>::None, false)),
}
}

Expand All @@ -342,8 +350,7 @@ impl NoUnusedVars {
let flags = symbol.flags();

// 1. ignore enum members. Only enums get checked
// 2. ignore all ambient TS declarations, e.g. `declare class Foo {}`
if flags.intersects(SymbolFlags::EnumMember.union(SymbolFlags::Ambient))
if flags.intersects(SymbolFlags::EnumMember)
// ambient namespaces
|| flags == AMBIENT_NAMESPACE_FLAGS
|| (symbol.is_in_ts() && symbol.is_in_declare_global())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1119,9 +1119,6 @@ fn test_type_references() {
export type C = B<A<number>>;
",
"const x: number = 1; function foo(): typeof x { return x }; foo()",
// not handled by typescript-eslint. Maybe we'll add this one day
"function foo(): typeof foo { }",
"function foo(): typeof foo { return foo }",
// ---
"type T = number; console.log(3 as T);",
"type T = number; console.log(((3) as T));",
Expand Down Expand Up @@ -1194,6 +1191,8 @@ fn test_type_references() {

// Same is true for interfaces
"interface LinkedList<T> { next: LinkedList<T> | undefined }",
"function foo(): typeof foo { }",
"function foo(): typeof foo { return foo }",
];

Tester::new(NoUnusedVars::NAME, NoUnusedVars::PLUGIN, pass, fail)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1013,7 +1013,7 @@ fn test() {
// https://github.com/typescript-eslint/typescript-eslint/issues/5152
(
"
function foo<T>(value: T): T {
export function foo<T>(value: T): T {
return { value };
}
export type Foo<T> = typeof foo<T>;
Expand Down Expand Up @@ -1380,6 +1380,28 @@ fn test() {
",
None,
),
(
"
type _Foo = 1;
export const x: _Foo = 1;
",
Some(json!([{ "varsIgnorePattern": "^_", "reportUnusedIgnorePattern": false }])),
),
(
"
export const foo: number = 1;
export type Foo = typeof foo;
",
None,
),
(
"
import { foo } from 'foo';
export type Foo = typeof foo;
export const bar = (): Foo => foo;
",
None,
),
];

let fail = vec![
Expand Down Expand Up @@ -1697,6 +1719,63 @@ fn test() {
None,
),
("const foo: number = 1;", None),
(
"
const foo: number = 1;
export type Foo = typeof foo;
",
None,
),
(
"
declare const foo: number;
export type Foo = typeof foo;
",
None,
),
(
"
const foo: number = 1;
export type Foo = typeof foo | string;
",
None,
),
(
"
const foo: number = 1;
export type Foo = (typeof foo | string) & { __brand: 'foo' };
",
None,
),
(
"
const foo = {
bar: {
baz: 123,
},
};
export type Bar = typeof foo.bar;
",
None,
),
(
"
const foo = {
bar: {
baz: 123,
},
};
export type Bar = (typeof foo)['bar'];
",
None,
),
(
"
import { foo } from 'foo';
export type Bar = typeof foo;
",
None,
),
];

Tester::new(NoUnusedVars::NAME, NoUnusedVars::PLUGIN, pass, fail)
Expand Down
33 changes: 33 additions & 0 deletions crates/oxc_linter/src/rules/eslint/no_unused_vars/usage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,13 @@ impl<'a> Symbol<'_, 'a> {
if do_type_self_usage_checks && self.is_type_self_usage(reference) {
continue;
}

if !self.flags().intersects(SymbolFlags::TypeImport)
&& self.reference_contains_type_query(reference)
{
continue;
}

return true;
}

Expand Down Expand Up @@ -779,4 +786,30 @@ impl<'a> Symbol<'_, 'a> {

None
}

pub fn has_reference_used_as_type_query(&self) -> bool {
self.references().any(|reference| self.reference_contains_type_query(reference))
}

fn reference_contains_type_query(&self, reference: &Reference) -> bool {
let Some(mut node) = self.get_ref_relevant_node(reference) else {
debug_assert!(false);
return false;
};

loop {
node = match node.kind() {
AstKind::TSTypeQuery(_) => return true,
AstKind::TSQualifiedName(_) | AstKind::TSTypeName(_) => {
if let Some(parent) = self.nodes().parent_node(node.id()) {
parent
} else {
debug_assert!(false);
return false;
}
}
_ => return false,
};
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,19 @@ source: crates/oxc_linter/src/tester.rs
· ╰── 'LinkedList' is declared here
╰────
help: Consider removing this declaration.

⚠ eslint(no-unused-vars): Function 'foo' is declared but never used.
╭─[no_unused_vars.ts:1:10]
1 │ function foo(): typeof foo { }
· ─┬─
· ╰── 'foo' is declared here
╰────
help: Consider removing this declaration.

⚠ eslint(no-unused-vars): Function 'foo' is declared but never used.
╭─[no_unused_vars.ts:1:10]
1 │ function foo(): typeof foo { return foo }
· ─┬─
· ╰── 'foo' is declared here
╰────
help: Consider removing this declaration.
Original file line number Diff line number Diff line change
Expand Up @@ -340,3 +340,73 @@ source: crates/oxc_linter/src/tester.rs
· ╰── 'foo' is declared here
╰────
help: Consider removing this declaration.

⚠ eslint(no-unused-vars): Variable is declared but only used as a type Unused variables should start with a '_'..
╭─[no_unused_vars.ts:2:16]
1 │
2 │ const foo: number = 1;
· ─┬─
· ╰── 'foo' is declared here
3 │ export type Foo = typeof foo;
╰────
help: Consider removing this declaration.

⚠ eslint(no-unused-vars): Variable is declared but only used as a type Unused variables should start with a '_'..
╭─[no_unused_vars.ts:2:23]
1 │
2 │ declare const foo: number;
· ─┬─
· ╰── 'foo' is declared here
3 │ export type Foo = typeof foo;
╰────
help: Consider removing this declaration.

⚠ eslint(no-unused-vars): Variable is declared but only used as a type Unused variables should start with a '_'..
╭─[no_unused_vars.ts:2:15]
1 │
2 │ const foo: number = 1;
· ─┬─
· ╰── 'foo' is declared here
3 │ export type Foo = typeof foo | string;
╰────
help: Consider removing this declaration.

⚠ eslint(no-unused-vars): Variable is declared but only used as a type Unused variables should start with a '_'..
╭─[no_unused_vars.ts:2:15]
1 │
2 │ const foo: number = 1;
· ─┬─
· ╰── 'foo' is declared here
3 │ export type Foo = (typeof foo | string) & { __brand: 'foo' };
╰────
help: Consider removing this declaration.

⚠ eslint(no-unused-vars): Variable is declared but only used as a type Unused variables should start with a '_'..
╭─[no_unused_vars.ts:2:15]
1 │
2 │ const foo = {
· ─┬─
· ╰── 'foo' is declared here
3 │ bar: {
╰────
help: Consider removing this declaration.

⚠ eslint(no-unused-vars): Variable is declared but only used as a type Unused variables should start with a '_'..
╭─[no_unused_vars.ts:2:15]
1 │
2 │ const foo = {
· ─┬─
· ╰── 'foo' is declared here
3 │ bar: {
╰────
help: Consider removing this declaration.

⚠ eslint(no-unused-vars): Identifier 'foo' is imported but never used.
╭─[no_unused_vars.ts:2:18]
1 │
2 │ import { foo } from 'foo';
· ─┬─
· ╰── 'foo' is imported here
3 │ export type Bar = typeof foo;
╰────
help: Consider removing this import.
Loading