Skip to content

bug(prefer-array-from-map): fixer converts mapper function to .fill(), breaking side effects and uniqueness #83

@tony-scio

Description

@tony-scio

The prefer-array-from-map fixer incorrectly converts Array.from({ length: N }, mapper) to Array.from({ length: N }).fill(mapper()). This is semantically wrong in two ways:

1. Mapper with side effects — called once instead of N times

// Before — creates N independent worker coroutines
await Promise.all(Array.from({ length: concurrency }, () => worker()));

// After autofix — BROKEN
await Promise.all(Array.from({ length: concurrency }).fill(worker()));
// worker() is called ONCE, and the same promise fills all slots.
// Instead of N concurrent workers, you get 1 worker and N-1 duplicates.

2. Mapper producing unique values — all slots get same reference

// Before — each element gets unique random values
const bars = Array.from({ length: barCount }, () => ({
  delay: Math.random(),
  scale: Math.random() * 0.6 + 0.4,
}));

// After autofix — BROKEN
const bars = Array.from({ length: barCount }).fill({
  delay: Math.random(),
  scale: Math.random() * 0.6 + 0.4,
});
// All elements share the same object reference with the same random values.
// Mutating one mutates all.

Root cause

.fill(expr) evaluates expr once and fills every slot with that single value. Array.from(iterable, mapper) calls mapper once per element. These are not equivalent when the mapper has side effects or returns unique values.

Suggested fix

The rule should not flag Array.from({ length: N }, mapper) (with an object literal as the first argument) at all, since there's no [...{ length: N }] equivalent — it's a fundamentally different use of Array.from that creates arrays of a given length with a factory function.

Suggested test case (valid — should not flag)

// Mapper with side effects
'const workers = Array.from({ length: 4 }, () => startWorker())'

// Mapper producing unique objects
'const items = Array.from({ length: 10 }, () => ({ id: Math.random() }))'

// Mapper using index
'const indices = Array.from({ length: 5 }, (_, i) => i * 2)'

Note: #71 addressed a related issue (non-constant callbacks) but the fix appears incomplete — these cases still trigger and autofix incorrectly.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions