Skip to content
Open
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export default defineConfig({
### Generic rules

- `no-array-filter-map` — rejects adjacent eager array filter/map passes while allowing lazy iterator pipelines.
- `no-reduce-accumulator-copy` — rejects non-spread accumulator copies inside reducers; complements native `oxc/no-accumulating-spread`.
- `no-reduce-accumulator-copy` — rejects non-spread accumulator copies inside reducers; complements native `oxc/no-accumulating-spread`. By default, this uses the `reduce`/`reduceRight` method name as a heuristic, including unknown and custom receivers. Configure `["error", { "requireArrayReceiver": true }]` to report only receivers with local array evidence (literals, array annotations, or recognized array chains), avoiding warnings on custom one-call APIs.
- `no-chained-type-assertions` — rejects nested `as` and angle-bracket assertions that fabricate evidence; chains made only of `as const` remain valid.
- `no-conditional-empty-object-spread` — reports object spreads that use a conditional `{}` branch to omit fields. It intentionally has no autofix because omission is not equivalent to assigning `undefined`.
- `no-known-value-widening` — rejects known expressions flowing into explicit `unknown`, `object`, anonymous-object, or open-dictionary targets, including known arguments passed to local `unknown` type predicates. Empty dictionary accumulators and finite-key `Record` targets remain valid.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ function enclosingReducer(node: ESTree.Node) {
const firstParameter = callback.params[0];
const accumulator = firstParameter?.type === "AssignmentPattern" ? firstParameter.left : firstParameter;
if (accumulator?.type !== "Identifier") return null;
return { callback, accumulator, initialValue: owner.arguments[1] };
return { callback, accumulator, receiver: method.object, initialValue: owner.arguments[1] };
}
parent = parent.parent;
}
Expand Down Expand Up @@ -72,6 +72,12 @@ export const noReduceAccumulatorCopyRule = defineRule({
messages: {
accumulatorCopy: "Do not copy the reducer accumulator on every iteration; growing copies can cause quadratic work. Mutate a fresh, locally owned accumulator and return it, or use an iterator pipeline/flatMap.",
},
schema: [{
type: "object",
properties: { requireArrayReceiver: { type: "boolean" } },
additionalProperties: false,
}],
defaultOptions: [{ requireArrayReceiver: false }],
},
createOnce(context) {
return {
Expand All @@ -80,6 +86,12 @@ export const noReduceAccumulatorCopyRule = defineRule({
if (method === null) return;
const reducer = enclosingReducer(node);
if (reducer === null) return;
const option = context.options?.[0];
if (
typeof option === "object" && option !== null && !Array.isArray(option) &&
option.requireArrayReceiver === true &&
!isKnownArrayExpression(context.sourceCode, reducer.receiver)
) return;
const accumulator = context.sourceCode.getDeclaredVariables(reducer.callback).find(variable =>
variable.identifiers.some(identifier => identifier.start === reducer.accumulator.start),
);
Expand Down
28 changes: 28 additions & 0 deletions src/rules/no-reduce-accumulator-copy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,22 @@ tester.run("anti-slop/no-reduce-accumulator-copy", noReduceAccumulatorCopyRule,
"items.reduce((acc, item) => { let alias = acc; alias = item; return Object.assign({}, alias); }, {});",
"items.reduce((acc, item) => [...acc, item], []);", // Owned by the native rule.
"items.reduce((acc, item) => ({ ...acc, [item.id]: item }), {});",
{
code: "const once = { reduce(fn, initial) { return fn(initial, { id: 1 }); } }; once.reduce((acc, item) => Object.assign({}, acc, item), {});",
options: [{ requireArrayReceiver: true }],
},
{
code: "items.reduce((acc, item) => Object.assign({}, acc, item), {});",
options: [{ requireArrayReceiver: true }],
},
{
code: "const custom = { reduceRight(fn, initial) { return fn(initial); } }; custom.reduceRight(acc => Array.from(acc), []);",
options: [{ requireArrayReceiver: true }],
},
{
code: "const items = [1, 2]; items.reduce((acc, item) => { acc.push(item); return acc; }, []);",
options: [{ requireArrayReceiver: true }],
},
],
invalid: [
{ code: "items.reduce((acc, item) => Object.assign({}, acc, { [item.id]: item }), {});", errors: [error] },
Expand All @@ -48,5 +64,17 @@ tester.run("anti-slop/no-reduce-accumulator-copy", noReduceAccumulatorCopyRule,
{ code: "items.reduce((acc, item) => acc.toSorted(), []);", errors: [error] },
{ code: "items.reduce((acc, item) => acc.toReversed(), []);", errors: [error] },
{ code: "items.reduce((acc, item) => acc.with(0, item), []);", errors: [error] },
{
code: "[1, 2].reduce((acc, item) => Object.assign({}, acc, { [item]: item }), {});",
options: [{ requireArrayReceiver: true }], errors: [error],
},
{
code: "const source = [1, 2]; const alias = source; alias.reduceRight((acc, item) => Array.from(acc), []);",
options: [{ requireArrayReceiver: true }], errors: [error],
},
{
code: "function run(items: readonly string[]) { return items.filter(Boolean).reduce((acc, item) => acc.concat(item), []); }",
options: [{ requireArrayReceiver: true }], errors: [error],
},
],
});
14 changes: 13 additions & 1 deletion src/rules/no-reduce-accumulator-copy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ function enclosingReducer(node: ESTree.Node) {
const firstParameter = callback.params[0];
const accumulator = firstParameter?.type === "AssignmentPattern" ? firstParameter.left : firstParameter;
if (accumulator?.type !== "Identifier") return null;
return { callback, accumulator, initialValue: owner.arguments[1] };
return { callback, accumulator, receiver: method.object, initialValue: owner.arguments[1] };
}
parent = parent.parent;
}
Expand Down Expand Up @@ -72,6 +72,12 @@ export const noReduceAccumulatorCopyRule = defineRule({
messages: {
accumulatorCopy: "Do not copy the reducer accumulator on every iteration; growing copies can cause quadratic work. Mutate a fresh, locally owned accumulator and return it, or use an iterator pipeline/flatMap.",
},
schema: [{
type: "object",
properties: { requireArrayReceiver: { type: "boolean" } },
additionalProperties: false,
}],
defaultOptions: [{ requireArrayReceiver: false }],
},
createOnce(context) {
return {
Expand All @@ -80,6 +86,12 @@ export const noReduceAccumulatorCopyRule = defineRule({
if (method === null) return;
const reducer = enclosingReducer(node);
if (reducer === null) return;
const option = context.options?.[0];
if (
typeof option === "object" && option !== null && !Array.isArray(option) &&
option.requireArrayReceiver === true &&
!isKnownArrayExpression(context.sourceCode, reducer.receiver)
) return;
const accumulator = context.sourceCode.getDeclaredVariables(reducer.callback).find(variable =>
variable.identifiers.some(identifier => identifier.start === reducer.accumulator.start),
);
Expand Down