Skip to content
Merged
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
79 changes: 54 additions & 25 deletions crates/oxc_linter/src/rules/oxc/only_used_in_recursion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,21 @@ fn create_diagnostic(
let mut fix = fixer.new_fix_with_capacity(
ctx.semantic().symbol_references(arg.symbol_id()).count() + 1,
);
fix.push(Fix::delete(arg.span()));
// Delete the parameter, including the comma before it
fix.push(Fix::delete(Span::new(
skip_to_next_char(ctx.source_text(), arg.span().start, &Direction::Backward)
.unwrap_or(arg.span().start),
arg.span().end,
)));

for reference in ctx.semantic().symbol_references(arg.symbol_id()) {
let node = ctx.nodes().get_node(reference.node_id());
fix.push(Fix::delete(node.span()));
// Delete the argument reference, including the comma before it
fix.push(Fix::delete(Span::new(
skip_to_next_char(ctx.source_text(), node.span().start, &Direction::Backward)
.unwrap_or(node.span().start),
node.span().end,
)));
}

// search for references to the function and remove the argument
Expand All @@ -177,13 +187,13 @@ fn create_diagnostic(

let arg_to_delete = call_expr.arguments[arg_index].span();
fix.push(Fix::delete(Span::new(
arg_to_delete.start,
skip_to_next_char(
ctx.source_text(),
arg_to_delete.end,
&Direction::Forward,
arg_to_delete.start,
&Direction::Backward,
)
.unwrap_or(arg_to_delete.end),
.unwrap_or(arg_to_delete.start),
arg_to_delete.end,
)));
}
}
Expand Down Expand Up @@ -412,26 +422,32 @@ enum Direction {
}

// Skips whitespace and commas in a given direction and
// returns the next character if found.
// returns the byte offset of the next non-skipped character if found.
#[expect(clippy::cast_possible_truncation)]
fn skip_to_next_char(s: &str, start: u32, direction: &Direction) -> Option<u32> {
// span is a half-open interval: [start, end)
// so we should return in that way.
let start = start as usize;
match direction {
Direction::Forward => s
.char_indices()
.skip(start)
.find(|&(_, c)| !c.is_whitespace() && c != ',')
.map(|(i, _)| i as u32),

Direction::Backward => s
.char_indices()
.rev()
.skip(s.len() - start)
.take_while(|&(_, c)| c.is_whitespace() || c == ',')
.map(|(i, _)| i as u32)
.last(),
Direction::Forward => {
let slice = s.get(start..)?;
for (offset, c) in slice.char_indices() {
if !c.is_whitespace() && c != ',' {
return Some((start + offset) as u32);
}
}
None
}
Direction::Backward => {
let slice = s.get(..start)?;
let mut result = None;
for (i, c) in slice.char_indices().rev() {
if c.is_whitespace() || c == ',' {
result = Some(i as u32);
} else {
break;
}
}
result
}
}
}

Expand Down Expand Up @@ -737,9 +753,9 @@ function writeChunks(a,callac){writeChunks(m,callac)}writeChunks(i,{})",
}
"#,
r#"
test(foo, );
function test(arg0, ) {
return test("", );
test(foo);
function test(arg0) {
return test("");
}
"#,
),
Expand Down Expand Up @@ -831,6 +847,19 @@ function writeChunks(a,callac){writeChunks(m,callac)}writeChunks(i,{})",
}
",
),
// Test that trailing commas are removed at external call sites
(
r"function recurse(used, unused) {
return recurse(used + 1, unused);
}
recurse(0, 'delete_me');
",
r"function recurse(used) {
return recurse(used + 1);
}
recurse(0);
",
),
];

Tester::new(OnlyUsedInRecursion::NAME, OnlyUsedInRecursion::PLUGIN, pass, fail)
Expand Down
Loading