feat(linter): start fixer for no-unused-vars (#4718)

Start to add dangerous suggestions for `no-unused-vars`. This PR focuses on fixing unused variable declarations.
- declarations with no references of any kind will be removed with a `FixKind` of dangerous suggestion.
- declarations with some usages will be renamed when the rule is configured with certain `"varsIgnorePattern"`s with a `FixKind` of dangerous fix.
This commit is contained in:
DonIsaac 2024-08-10 15:27:11 +00:00
parent 070ae53ad6
commit d2734f3bc4
13 changed files with 677 additions and 34 deletions

View file

@ -33,6 +33,7 @@ bitflags! {
const None = 0;
const SafeFix = Self::Fix.bits();
const DangerousFix = Self::Dangerous.bits() | Self::Fix.bits();
const DangerousSuggestion = Self::Dangerous.bits() | Self::Suggestion.bits();
/// Fixes and Suggestions that are safe or dangerous.
const All = Self::Dangerous.bits() | Self::Fix.bits() | Self::Suggestion.bits();
}

View file

@ -64,6 +64,11 @@ impl<'c, 'a: 'c> RuleFixer<'c, 'a> {
RuleFix::new(self.kind, None, CompositeFix::Multiple(Vec::with_capacity(capacity)))
}
#[inline]
pub fn source_text(&self) -> &'a str {
self.ctx.source_text()
}
/// Get a snippet of source text covered by the given [`Span`]. For details,
/// see [`Span::source_text`].
#[inline]

View file

@ -1,6 +1,6 @@
use oxc_diagnostics::OxcDiagnostic;
use oxc_semantic::SymbolFlags;
use oxc_span::Span;
use oxc_span::{GetSpan, Span};
use super::Symbol;

View file

@ -0,0 +1,318 @@
use oxc_ast::{
ast::{BindingPatternKind, VariableDeclaration, VariableDeclarator},
AstKind,
};
use oxc_semantic::{AstNode, AstNodeId};
use oxc_span::{CompactStr, GetSpan, Span};
use regex::Regex;
use super::{NoUnusedVars, Symbol};
use crate::fixer::{Fix, RuleFix, RuleFixer};
impl NoUnusedVars {
#[allow(clippy::cast_possible_truncation)]
pub(super) fn rename_or_remove_var_declaration<'a>(
&self,
fixer: RuleFixer<'_, 'a>,
symbol: &Symbol<'_, 'a>,
decl: &VariableDeclarator<'a>,
decl_id: AstNodeId,
) -> RuleFix<'a> {
let Some(AstKind::VariableDeclaration(declaration)) =
symbol.nodes().parent_node(decl_id).map(AstNode::kind)
else {
panic!("VariableDeclarator nodes should always be direct children of VariableDeclaration nodes");
};
// `true` even if references aren't considered a usage.
let has_references = symbol.has_references();
// we can delete variable declarations that aren't referenced anywhere
if !has_references {
// for `let x = 1;` or `const { x } = obj; the whole declaration can
// be removed, but for `const { x, y } = obj;` or `let x = 1, y = 2`
// we need to keep the other declarations
let has_neighbors = declaration.declarations.len() > 1;
debug_assert!(!declaration.declarations.is_empty());
let binding_info = symbol.get_binding_info(&decl.id.kind);
match binding_info {
BindingInfo::SingleDestructure | BindingInfo::NotDestructure => {
if has_neighbors {
return self
.delete_declarator(fixer, symbol, declaration, decl)
.dangerously();
}
return fixer.delete(declaration).dangerously();
}
BindingInfo::MultiDestructure(mut span, is_object, is_last) => {
let source_after = &fixer.source_text()[(span.end as usize)..];
// remove trailing commas
span = span.expand_right(count_whitespace_or_commas(source_after.chars()));
// remove leading commas when removing the last element in
// an array
// `const [a, b] = [1, 2];` -> `const [a, b] = [1, 2];`
// ^ ^^^
if !is_object && is_last {
debug_assert!(span.start > 0);
let source_before = &fixer.source_text()[..(span.start as usize)];
let chars = source_before.chars().rev();
let start_offset = count_whitespace_or_commas(chars);
// do not walk past the beginning of the file
debug_assert!(start_offset < span.start);
span = span.expand_left(start_offset);
}
return if is_object || is_last {
fixer.delete_range(span).dangerously()
} else {
// infix array elements need a comma to preserve
// unpacking order of symbols around them
// e.g. `const [a, b, c] = [1, 2, 3];` -> `const [a, , c] = [1, 2, 3];`
fixer.replace(span, ",").dangerously()
};
}
BindingInfo::NotFound => {
return fixer.noop();
}
}
}
// otherwise, try to rename the variable to match the unused variable
// pattern
if let Some(new_name) = self.get_unused_var_name(symbol) {
return symbol.rename(&new_name).dangerously();
}
fixer.noop()
}
/// Delete a single declarator from a [`VariableDeclaration`] list with more
/// than one declarator.
#[allow(clippy::unused_self)]
fn delete_declarator<'a>(
&self,
fixer: RuleFixer<'_, 'a>,
symbol: &Symbol<'_, 'a>,
declaration: &VariableDeclaration<'a>,
decl: &VariableDeclarator<'a>,
) -> RuleFix<'a> {
let own_position = declaration
.declarations
.iter()
.position(|d| symbol == &d.id)
.expect("VariableDeclarator not found within its own parent VariableDeclaration");
let mut delete_range = decl.span();
let mut has_left = false;
let mut has_right = false;
// `let x = 1, y = 2, z = 3;` -> `let x = 1, y = 2, z = 3;`
// ^^^^^ ^^^^^^^
if let Some(right_neighbor) = declaration.declarations.get(own_position + 1) {
delete_range.end = right_neighbor.span.start;
has_right = true;
}
// `let x = 1, y = 2, z = 3;` -> `let x = 1, y = 2, z = 3;`
// ^^^^^ ^^^^^^^
if own_position > 0 {
if let Some(left_neighbor) = declaration.declarations.get(own_position - 1) {
delete_range.start = left_neighbor.span.end;
has_left = true;
}
}
// both left and right neighbors are present, so we need to
// re-insert a comma
// `let x = 1, y = 2, z = 3;`
// ^^^^^^^^^
if has_left && has_right {
return fixer.replace(delete_range, ", ");
}
return fixer.delete(&delete_range);
}
fn get_unused_var_name(&self, symbol: &Symbol<'_, '_>) -> Option<CompactStr> {
let pat = self.vars_ignore_pattern.as_ref().map(Regex::as_str)?;
let ignored_name: String = match pat {
// TODO: support more patterns
"^_" => format!("_{}", symbol.name()),
_ => return None,
};
// adjust name to avoid conflicts
let scopes = symbol.scopes();
let scope_id = symbol.scope_id();
let mut i = 0;
let mut new_name = ignored_name.clone();
while scopes.has_binding(scope_id, &new_name) {
new_name = format!("{ignored_name}{i}");
i += 1;
}
Some(new_name.into())
}
}
impl<'s, 'a> Symbol<'s, 'a> {
fn rename(&self, new_name: &CompactStr) -> RuleFix<'a> {
let mut fixes: Vec<Fix<'a>> = vec![];
let decl_span = self.span();
fixes.push(Fix::new(new_name.clone(), decl_span));
for reference in self.references() {
match self.nodes().get_node(reference.node_id()).kind() {
AstKind::IdentifierReference(id) => {
fixes.push(Fix::new(new_name.clone(), id.span()));
}
AstKind::TSTypeReference(ty) => {
fixes.push(Fix::new(new_name.clone(), ty.type_name.span()));
}
// we found a reference to an unknown node and we don't know how
// to replace it, so we abort the whole process
_ => return Fix::empty().into(),
}
}
return RuleFix::from(fixes)
.with_message(format!("Rename '{}' to '{new_name}'", self.name()));
}
/// - `true` if `pattern` is a destructuring pattern and only contains one symbol
/// - `false` if `pattern` is a destructuring pattern and contains more than one symbol
/// - `not applicable` if `pattern` is not a destructuring pattern
fn get_binding_info(&self, pattern: &BindingPatternKind<'a>) -> BindingInfo {
match pattern {
BindingPatternKind::ArrayPattern(arr) => match arr.elements.len() {
0 => {
debug_assert!(arr.rest.is_some());
BindingInfo::multi_or_single(arr.rest.as_ref().map(|r| (r.span, true)), true)
}
1 => {
let own_span =
arr.elements.iter().filter_map(|el| el.as_ref()).find(|el| self == *el);
if let Some(rest) = arr.rest.as_ref() {
if rest.span.contains_inclusive(self.span()) {
// spreads are after all elements, otherwise it
// would be a spread element
return BindingInfo::MultiDestructure(rest.span, false, true);
}
BindingInfo::multi_or_missing(own_span.map(|el| (el.span(), false)), false)
} else {
BindingInfo::single_or_missing(own_span.is_some())
}
}
_ => {
let last_position = arr.elements.len() - 1;
BindingInfo::multi_or_missing(
arr.elements
.iter()
.enumerate()
.filter_map(|(idx, el)| el.as_ref().map(|el| (idx, el)))
.find_map(|(idx, el)| {
if self == el {
Some((el.span(), idx == last_position))
} else {
None
}
}),
false,
)
}
},
BindingPatternKind::ObjectPattern(obj) => match obj.properties.len() {
0 => {
debug_assert!(obj.rest.is_some());
BindingInfo::multi_or_single(obj.rest.as_ref().map(|r| (r.span, true)), true)
}
1 => {
let last_property = obj.properties.len() - 1;
let own_span = obj.properties.iter().enumerate().find_map(|(idx, el)| {
if self == &el.value {
Some((el.span, idx == last_property))
} else {
None
}
});
if let Some(rest) = obj.rest.as_ref() {
if rest.span.contains_inclusive(self.span()) {
// assume rest spreading in objects are at the end
return BindingInfo::MultiDestructure(rest.span, true, true);
}
BindingInfo::multi_or_missing(own_span, true)
} else {
BindingInfo::single_or_missing(own_span.is_some())
}
}
_ => {
let last_property = obj.properties.len() - 1;
let own_span = obj.properties.iter().enumerate().find_map(|(idx, el)| {
(self == &el.value).then_some((el.span, idx == last_property))
});
BindingInfo::multi_or_missing(own_span, true)
}
},
BindingPatternKind::AssignmentPattern(assignment) => {
self.get_binding_info(&assignment.left.kind)
}
// not in a destructure
BindingPatternKind::BindingIdentifier(_) => BindingInfo::NotDestructure,
}
}
}
#[derive(Debug, Clone, Copy)]
enum BindingInfo {
NotDestructure,
SingleDestructure,
/// Notes:
/// 1. Symbol declaration span will cover the entire pattern, so we need
/// to extract the symbol's _exact_ span
/// 2. Unused symbols created in array destructures need to have a comma if
/// they are not in the last position. The second boolean arg indicates
/// if the pattern is an object destructure (true) or an array
/// destructure (false). When the symbol is in the last position, it
/// doesn't need a comma, which is what the third boolean arg indicates.
/// It is not used for objects.
MultiDestructure(Span, /* object? */ bool, /* last? */ bool),
/// Usually indicates a problem in the AST, though it's possible for it to
/// be a problem in the fixer
NotFound,
}
impl BindingInfo {
#[inline]
const fn single_or_missing(found: bool) -> Self {
if found {
BindingInfo::SingleDestructure
} else {
BindingInfo::NotFound
}
}
fn multi_or_missing(found: Option<(Span, bool)>, is_object: bool) -> Self {
match found {
Some((span, is_last)) => BindingInfo::MultiDestructure(span.span(), is_object, is_last),
None => BindingInfo::NotFound,
}
}
fn multi_or_single(found: Option<(Span, bool)>, is_object: bool) -> Self {
match found {
Some((span, is_last)) => BindingInfo::MultiDestructure(span.span(), is_object, is_last),
None => BindingInfo::SingleDestructure,
}
}
}
// source text will never be large enough for this usize to be truncated when
// getting cast to a u32
#[allow(clippy::cast_possible_truncation)]
fn count_whitespace_or_commas<I: Iterator<Item = char>>(iter: I) -> u32 {
iter.take_while(|c| c.is_whitespace() || *c == ',').count() as u32
}

View file

@ -1,6 +1,7 @@
mod allowed;
mod binding_pattern;
mod diagnostic;
mod fixers;
mod ignored;
mod options;
mod symbol;
@ -135,7 +136,8 @@ declare_oxc_lint!(
/// var global_var = 42;
/// ```
NoUnusedVars,
nursery
nursery,
dangerous_suggestion
);
impl Deref for NoUnusedVars {
@ -207,9 +209,7 @@ impl NoUnusedVars {
| AstKind::ImportExpression(_)
| AstKind::ImportDefaultSpecifier(_)
| AstKind::ImportNamespaceSpecifier(_) => {
if !is_ignored {
ctx.diagnostic(diagnostic::imported(symbol));
}
ctx.diagnostic(diagnostic::imported(symbol));
}
AstKind::VariableDeclarator(decl) => {
if self.is_allowed_variable_declaration(symbol, decl) {
@ -223,7 +223,12 @@ impl NoUnusedVars {
} else {
diagnostic::declared(symbol)
};
ctx.diagnostic(report);
ctx.diagnostic_with_suggestion(report, |fixer| {
// NOTE: suggestions produced by this fixer are all flagged
// as dangerous
self.rename_or_remove_var_declaration(fixer, symbol, decl, declaration.id())
});
}
AstKind::FormalParameter(param) => {
if self.is_allowed_argument(ctx.semantic().as_ref(), symbol, param) {

View file

@ -1,4 +1,4 @@
use std::fmt;
use std::{cell::OnceCell, fmt};
use oxc_ast::{
ast::{AssignmentTarget, BindingIdentifier, BindingPattern, IdentifierReference},
@ -8,13 +8,14 @@ use oxc_semantic::{
AstNode, AstNodeId, AstNodes, Reference, ScopeId, ScopeTree, Semantic, SymbolFlags, SymbolId,
SymbolTable,
};
use oxc_span::Span;
use oxc_span::{GetSpan, Span};
#[derive(Clone)]
pub(super) struct Symbol<'s, 'a> {
semantic: &'s Semantic<'a>,
id: SymbolId,
flags: SymbolFlags,
span: OnceCell<Span>,
}
impl PartialEq for Symbol<'_, '_> {
@ -27,7 +28,7 @@ impl PartialEq for Symbol<'_, '_> {
impl<'s, 'a> Symbol<'s, 'a> {
pub fn new(semantic: &'s Semantic<'a>, symbol_id: SymbolId) -> Self {
let flags = semantic.symbols().get_flag(symbol_id);
Self { semantic, id: symbol_id, flags }
Self { semantic, id: symbol_id, flags, span: OnceCell::new() }
}
#[inline]
@ -40,12 +41,6 @@ impl<'s, 'a> Symbol<'s, 'a> {
self.symbols().get_name(self.id)
}
/// [`Span`] for the node declaring the [`Symbol`].
#[inline]
pub fn span(&self) -> Span {
self.symbols().get_span(self.id)
}
#[inline]
pub const fn flags(&self) -> SymbolFlags {
self.flags
@ -61,6 +56,13 @@ impl<'s, 'a> Symbol<'s, 'a> {
self.nodes().get_node(self.declaration_id())
}
/// Returns `true` if this symbol has any references of any kind. Does not
/// check if a references is "used" under the criteria of this rule.
#[inline]
pub fn has_references(&self) -> bool {
!self.symbols().get_resolved_reference_ids(self.id).is_empty()
}
#[inline]
pub fn references(&self) -> impl DoubleEndedIterator<Item = &Reference> + '_ {
self.symbols().get_resolved_references(self.id)
@ -91,8 +93,13 @@ impl<'s, 'a> Symbol<'s, 'a> {
self.semantic.symbols()
}
#[inline]
pub fn iter_parents(&self) -> impl Iterator<Item = &AstNode<'a>> + '_ {
self.nodes().iter_parents(self.declaration_id()).skip(1)
self.iter_self_and_parents().skip(1)
}
pub fn iter_self_and_parents(&self) -> impl Iterator<Item = &AstNode<'a>> + '_ {
self.nodes().iter_parents(self.declaration_id())
}
pub fn iter_relevant_parents(
@ -123,6 +130,29 @@ impl<'s, 'a> Symbol<'s, 'a> {
const fn is_relevant_kind(kind: AstKind<'a>) -> bool {
!matches!(kind, AstKind::ParenthesizedExpression(_))
}
/// <https://github.com/oxc-project/oxc/issues/4739>
fn derive_span(&self) -> Span {
for kind in self.iter_self_and_parents().map(AstNode::kind) {
match kind {
AstKind::BindingIdentifier(_) => continue,
AstKind::BindingRestElement(rest) => return rest.span,
AstKind::VariableDeclarator(decl) => return self.clean_binding_id(&decl.id),
AstKind::FormalParameter(param) => return self.clean_binding_id(&param.pattern),
_ => break,
}
}
self.symbols().get_span(self.id)
}
/// <https://github.com/oxc-project/oxc/issues/4739>
fn clean_binding_id(&self, binding: &BindingPattern) -> Span {
if binding.kind.is_destructuring_pattern() {
return self.symbols().get_span(self.id);
}
let own = binding.kind.span();
binding.type_annotation.as_ref().map_or(own, |ann| Span::new(own.start, ann.span.start))
}
}
impl<'s, 'a> Symbol<'s, 'a> {
@ -176,6 +206,18 @@ impl<'s, 'a> Symbol<'s, 'a> {
}
}
impl GetSpan for Symbol<'_, '_> {
/// [`Span`] for the node declaring the [`Symbol`].
#[inline]
fn span(&self) -> Span {
// TODO: un-comment and replace when BindingIdentifier spans are fixed
// https://github.com/oxc-project/oxc/issues/4739
// self.symbols().get_span(self.id)
*self.span.get_or_init(|| self.derive_span())
}
}
impl<'a> PartialEq<IdentifierReference<'a>> for Symbol<'_, 'a> {
fn eq(&self, other: &IdentifierReference<'a>) -> bool {
// cheap: no resolved reference means its a global reference

View file

@ -1,9 +1,31 @@
//! Test cases created by oxc maintainers
use super::NoUnusedVars;
use crate::{tester::Tester, RuleMeta as _};
use crate::{tester::Tester, FixKind, RuleMeta as _};
use serde_json::json;
#[test]
fn test_debug() {
let pass: Vec<&'static str> = vec![];
let fail = vec![];
let fix = vec![
// (
// "const { foo: fooBar, baz } = obj; f(baz);",
// "const { baz } = obj; f(baz);",
// None,
// FixKind::DangerousSuggestion,
// ),
("const [a, b] = arr; f(a)", "const [a] = arr; f(a)", None, FixKind::DangerousSuggestion),
// (
// "let x = 1; x = 2;",
// "let x = 1; x = 2;",
// Some(json!( [{ "varsIgnorePattern": "^tooCompli[cated]" }] )),
// FixKind::DangerousFix,
// ),
];
Tester::new(NoUnusedVars::NAME, pass, fail).expect_fix(fix).test();
}
#[test]
fn test_vars_simple() {
let pass = vec![
@ -12,18 +34,76 @@ fn test_vars_simple() {
("let a = 1; let b = a + 1; console.log(b)", None),
("let a = 1; if (true) { console.log(a) }", None),
("let _a = 1", Some(json!([{ "varsIgnorePattern": "^_" }]))),
("const { foo: _foo, baz } = obj; f(baz);", Some(json!([{ "varsIgnorePattern": "^_" }]))),
];
let fail = vec![
("let a = 1", None),
("let a: number = 1", None),
("let a = 1; a = 2", None),
(
"let _a = 1; console.log(_a)",
Some(json!([{ "varsIgnorePattern": "^_", "reportUsedIgnorePattern": true }])),
),
("const { foo: fooBar, baz } = obj; f(baz);", None),
("let _a = 1", Some(json!([{ "argsIgnorePattern": "^_" }]))),
];
let fix = vec![
// unused vars should be removed
("let a = 1;", "", None, FixKind::DangerousSuggestion),
// FIXME: b should be deleted as well.
("let a = 1, b = 2;", "let b = 2;", None, FixKind::DangerousSuggestion),
(
"let a = 1; let b = 2; console.log(a);",
"let a = 1; console.log(a);",
None,
FixKind::DangerousSuggestion,
),
(
"let a = 1; let b = 2; console.log(b);",
" let b = 2; console.log(b);",
None,
FixKind::DangerousSuggestion,
),
(
"let a = 1, b = 2; console.log(b);",
"let b = 2; console.log(b);",
None,
FixKind::DangerousSuggestion,
),
(
"let a = 1, b = 2; console.log(a);",
"let a = 1; console.log(a);",
None,
FixKind::DangerousSuggestion,
),
(
"let a = 1, b = 2, c = 3; console.log(a + c);",
"let a = 1, c = 3; console.log(a + c);",
None,
FixKind::DangerousSuggestion,
),
(
"let a = 1, b = 2, c = 3; console.log(b + c);",
"let b = 2, c = 3; console.log(b + c);",
None,
FixKind::DangerousSuggestion,
),
// vars with references get renamed
("let x = 1; x = 2;", "let _x = 1; _x = 2;", None, FixKind::DangerousFix),
(
"let x = 1; x = 2;",
"let x = 1; x = 2;",
Some(json!( [{ "varsIgnorePattern": "^tooCompli[cated]" }] )),
FixKind::DangerousFix,
),
// type annotations do not get clobbered
("let x: number = 1; x = 2;", "let _x: number = 1; _x = 2;", None, FixKind::DangerousFix),
("const { a } = obj;", "", None, FixKind::DangerousSuggestion),
];
Tester::new(NoUnusedVars::NAME, pass, fail)
.expect_fix(fix)
.with_snapshot_suffix("oxc-vars-simple")
.test_and_snapshot();
}
@ -185,7 +265,7 @@ fn test_vars_reassignment() {
}
#[test]
fn test_vars_destructure_ignored() {
fn test_vars_destructure() {
let pass = vec![
// ("const { a, ...rest } = obj; console.log(rest)", Some(json![{ "ignoreRestSiblings": true }]))
];
@ -198,8 +278,53 @@ fn test_vars_destructure_ignored() {
),
];
let fix = vec![
("const { a } = obj;", "", None, FixKind::DangerousSuggestion),
("const [a] = arr;", "", None, FixKind::DangerousSuggestion),
(
"const { a, b } = obj; f(b)",
"const { b } = obj; f(b)",
None,
FixKind::DangerousSuggestion,
),
("const [a, b] = arr; f(b)", "const [,b] = arr; f(b)", None, FixKind::DangerousSuggestion),
("const [a, b] = arr; f(a)", "const [a] = arr; f(a)", None, FixKind::DangerousSuggestion),
(
"const [a, b, c] = arr; f(a, c)",
"const [a, ,c] = arr; f(a, c)",
None,
FixKind::DangerousSuggestion,
),
(
"const [a, b, c, d, e] = arr; f(a, e)",
"const [a, ,,,e] = arr; f(a, e)",
None,
FixKind::DangerousSuggestion,
),
(
"const [a, b, c, d, e, f] = arr; fn(a, e)",
"const [a, ,,,e] = arr; fn(a, e)",
None,
FixKind::DangerousSuggestion,
),
(
"const { foo: fooBar, baz } = obj; f(baz);",
"const { baz } = obj; f(baz);",
None,
FixKind::DangerousSuggestion,
),
// renaming
// (
// "let a = 1; a = 2;",
// "let _a = 1; _a = 2;",
// Some(json!([{ "varsIgnorePattern": "^_" }])),
// FixKind::DangerousSuggestion,
// ),
];
Tester::new(NoUnusedVars::NAME, pass, fail)
.with_snapshot_suffix("oxc-vars-destructure-ignored")
.expect_fix(fix)
.with_snapshot_suffix("oxc-vars-destructure")
.test_and_snapshot();
}
@ -440,6 +565,7 @@ fn test_arguments() {
];
let fail = vec![
("function foo(a) {} foo()", None),
("function foo(a: number) {} foo()", None),
("function foo({ a }, b) { return b } foo()", Some(json!([{ "args": "after-used" }]))),
];

View file

@ -9,6 +9,14 @@ source: crates/oxc_linter/src/tester.rs
╰────
help: Consider removing this parameter.
⚠ eslint(no-unused-vars): Parameter 'a' is declared but never used.
╭─[no_unused_vars.tsx:1:14]
1 │ function foo(a: number) {} foo()
· ┬
· ╰── 'a' is declared here
╰────
help: Consider removing this parameter.
⚠ eslint(no-unused-vars): Parameter 'a' is declared but never used.
╭─[no_unused_vars.tsx:1:16]
1 │ function foo({ a }, b) { return b } foo()

View file

@ -4,8 +4,8 @@ source: crates/oxc_linter/src/tester.rs
⚠ eslint(no-unused-vars): Parameter 'a' is declared but never used.
╭─[no_unused_vars.tsx:1:32]
1 │ export class Foo { constructor(a: number) {} }
· ────────
· ╰── 'a' is declared here
· ┬
· ╰── 'a' is declared here
╰────
help: Consider removing this parameter.
@ -21,8 +21,8 @@ source: crates/oxc_linter/src/tester.rs
╭─[no_unused_vars.tsx:3:24]
2 │ export abstract class Foo {
3 │ public bar(a: number): string {}
· ────────
· ╰── 'a' is declared here
· ┬
· ╰── 'a' is declared here
4 │ }
╰────
help: Consider removing this parameter.

View file

@ -0,0 +1,50 @@
---
source: crates/oxc_linter/src/tester.rs
---
⚠ eslint(no-unused-vars): Variable 'a' is declared but never used.
╭─[no_unused_vars.tsx:1:9]
1 │ const { a, ...rest } = obj
· ┬
· ╰── 'a' is declared here
╰────
help: Consider removing this declaration.
⚠ eslint(no-unused-vars): Variable 'rest' is declared but never used.
╭─[no_unused_vars.tsx:1:15]
1 │ const { a, ...rest } = obj
· ──┬─
· ╰── 'rest' is declared here
╰────
help: Consider removing this declaration.
⚠ eslint(no-unused-vars): Variable 'a' is declared but never used.
╭─[no_unused_vars.tsx:1:8]
1 │ const [a, ...rest] = arr
· ┬
· ╰── 'a' is declared here
╰────
help: Consider removing this declaration.
⚠ eslint(no-unused-vars): Variable 'rest' is declared but never used.
╭─[no_unused_vars.tsx:1:14]
1 │ const [a, ...rest] = arr
· ──┬─
· ╰── 'rest' is declared here
╰────
help: Consider removing this declaration.
⚠ eslint(no-unused-vars): Variable 'b' is declared but never used.
╭─[no_unused_vars.tsx:1:14]
1 │ const { a: { b }, ...rest } = obj; console.log(a)
· ┬
· ╰── 'b' is declared here
╰────
help: Consider removing this declaration.
⚠ eslint(no-unused-vars): Variable 'rest' is declared but never used.
╭─[no_unused_vars.tsx:1:22]
1 │ const { a: { b }, ...rest } = obj; console.log(a)
· ──┬─
· ╰── 'rest' is declared here
╰────
help: Consider removing this declaration.

View file

@ -9,6 +9,14 @@ source: crates/oxc_linter/src/tester.rs
╰────
help: Consider removing this declaration.
⚠ eslint(no-unused-vars): Variable 'a' is declared but never used.
╭─[no_unused_vars.tsx:1:5]
1 │ let a: number = 1
· ┬
· ╰── 'a' is declared here
╰────
help: Consider removing this declaration.
⚠ eslint(no-unused-vars): Variable 'a' is assigned a value but never used.
╭─[no_unused_vars.tsx:1:5]
1 │ let a = 1; a = 2
@ -26,6 +34,14 @@ source: crates/oxc_linter/src/tester.rs
╰────
help: Consider renaming this variable.
⚠ eslint(no-unused-vars): Variable 'fooBar' is declared but never used.
╭─[no_unused_vars.tsx:1:14]
1 │ const { foo: fooBar, baz } = obj; f(baz);
· ───┬──
· ╰── 'fooBar' is declared here
╰────
help: Consider removing this declaration.
⚠ eslint(no-unused-vars): Variable '_a' is declared but never used.
╭─[no_unused_vars.tsx:1:5]
1 │ let _a = 1

View file

@ -336,7 +336,7 @@ source: crates/oxc_linter/src/tester.rs
⚠ eslint(no-unused-vars): Variable 'foo' is declared but never used.
╭─[no_unused_vars.ts:1:7]
1 │ const foo: number = 1;
· ─────┬─────
· ╰── 'foo' is declared here
· ─┬─
· ╰── 'foo' is declared here
╰────
help: Consider removing this declaration.

View file

@ -65,24 +65,96 @@ impl From<(&str, Option<Value>, Option<Value>, Option<PathBuf>)> for TestCase {
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum ExpectFixKind {
/// We expect no fix to be applied
#[default]
None,
/// We expect some fix to be applied, but don't care what kind it is
Any,
/// We expect a fix of a certain [`FixKind`] to be applied
Specific(FixKind),
}
impl ExpectFixKind {
#[inline]
pub fn is_none(self) -> bool {
matches!(self, Self::None)
}
#[inline]
pub fn is_some(self) -> bool {
!self.is_none()
}
}
impl From<FixKind> for ExpectFixKind {
fn from(kind: FixKind) -> Self {
Self::Specific(kind)
}
}
impl From<ExpectFixKind> for FixKind {
fn from(expected_kind: ExpectFixKind) -> Self {
match expected_kind {
ExpectFixKind::None => FixKind::None,
ExpectFixKind::Any => FixKind::All,
ExpectFixKind::Specific(kind) => kind,
}
}
}
impl From<Option<FixKind>> for ExpectFixKind {
fn from(maybe_kind: Option<FixKind>) -> Self {
match maybe_kind {
Some(kind) => Self::Specific(kind),
None => Self::Any, // intentionally not None
}
}
}
#[derive(Debug, Clone)]
pub struct ExpectFix {
/// Source code being tested
source: String,
/// Expected source code after fix has been applied
expected: String,
kind: ExpectFixKind,
rule_config: Option<Value>,
}
impl<S: Into<String>> From<(S, S, Option<Value>)> for ExpectFix {
fn from(value: (S, S, Option<Value>)) -> Self {
Self { source: value.0.into(), expected: value.1.into(), rule_config: value.2 }
Self {
source: value.0.into(),
expected: value.1.into(),
kind: ExpectFixKind::Any,
rule_config: value.2,
}
}
}
impl<S: Into<String>> From<(S, S)> for ExpectFix {
fn from(value: (S, S)) -> Self {
Self { source: value.0.into(), expected: value.1.into(), rule_config: None }
Self {
source: value.0.into(),
expected: value.1.into(),
kind: ExpectFixKind::Any,
rule_config: None,
}
}
}
impl<S, F> From<(S, S, Option<Value>, F)> for ExpectFix
where
S: Into<String>,
F: Into<ExpectFixKind>,
{
fn from((source, expected, config, kind): (S, S, Option<Value>, F)) -> Self {
Self {
source: source.into(),
expected: expected.into(),
kind: kind.into(),
rule_config: config,
}
}
}
@ -237,7 +309,7 @@ impl Tester {
fn test_pass(&mut self) {
for TestCase { source, rule_config, eslint_config, path } in self.expect_pass.clone() {
let result = self.run(&source, rule_config, &eslint_config, path, false);
let result = self.run(&source, rule_config, &eslint_config, path, ExpectFixKind::None);
let passed = result == TestResult::Passed;
assert!(passed, "expect test to pass: {source} {}", self.snapshot);
}
@ -245,7 +317,7 @@ impl Tester {
fn test_fail(&mut self) {
for TestCase { source, rule_config, eslint_config, path } in self.expect_fail.clone() {
let result = self.run(&source, rule_config, &eslint_config, path, false);
let result = self.run(&source, rule_config, &eslint_config, path, ExpectFixKind::None);
let failed = result == TestResult::Failed;
assert!(failed, "expect test to fail: {source}");
}
@ -253,8 +325,8 @@ impl Tester {
fn test_fix(&mut self) {
for fix in self.expect_fix.clone() {
let ExpectFix { source, expected, rule_config: config } = fix;
let result = self.run(&source, config, &None, None, true);
let ExpectFix { source, expected, kind, rule_config: config } = fix;
let result = self.run(&source, config, &None, None, kind);
match result {
TestResult::Fixed(fixed_str) => assert_eq!(
expected, fixed_str,
@ -272,12 +344,12 @@ impl Tester {
rule_config: Option<Value>,
eslint_config: &Option<Value>,
path: Option<PathBuf>,
is_fix: bool,
fix: ExpectFixKind,
) -> TestResult {
let allocator = Allocator::default();
let rule = self.find_rule().read_json(rule_config.unwrap_or_default());
let options = LintOptions::default()
.with_fix(is_fix.then_some(FixKind::All).unwrap_or_default())
.with_fix(fix.into())
.with_import_plugin(self.import_plugin)
.with_jest_plugin(self.jest_plugin)
.with_vitest_plugin(self.vitest_plugin)
@ -312,7 +384,7 @@ impl Tester {
return TestResult::Passed;
}
if is_fix {
if fix.is_some() {
let fix_result = Fixer::new(source_text, result).fix();
return TestResult::Fixed(fix_result.fixed_code.to_string());
}