SIGN IN SIGN UP

fix(linter/unicorn/no-useless-spread): treat typed arrays as a distinct value hint (#26067)

Closes #25868.

## What

`unicorn/no-useless-spread` inferred "returns a plain array" from the
method name
alone, so `[...new Uint8Array(buf).slice(0, 12)]` was reported and
`--fix` removed
the spread. That spread is the conversion to `number[]`, so the
following `.map()`
kept running as `TypedArray.prototype.map` and coerced every callback
result back
into the element type. #25868 has the measurements and the seven
affected methods.

## Approach

Rather than guard the one call site, this adds a
`ValueHint::NewTypedArray` variant
beside the existing `NewArray` / `NewObject` / `NewIterable`.

The reason is chains. A one-off receiver check handles
`new Uint8Array(buf).slice(0)` and misses `new
Uint8Array(buf).slice(0).map(f)`,
where the receiver is itself a call. Carrying the typed-array fact
through
`const_eval` means the existing recursion does that work, and
`[...new Uint8Array(buf).slice(0, 12).map(f)]` passes with no extra
code.

`is_array()` stays false for the new variant, so nothing is reported.
Three places
feed it:

- `NewExpression::const_eval`, via the existing `is_new_typed_array`
- `is_typed_array_from`, for `Uint8Array.from(x)`
- `is_typed_array_method`, for a clone method whose receiver already
evaluates to
  `NewTypedArray`

## One deviation from the fix sketched in the issue

The issue proposed splitting the typed-array names out of
`is_array_from`. That
would regress an existing `fail` vector, because `is_array_from` has a
second
consumer doing a different job.

`mod.rs:362` uses it to decide whether a spread passed *into* `from` is
useless,
which it is for every constructor in that list, since they all accept an
iterable.
`BigUint64Array.from([...iterable])` is in the `fail` vectors today and
should stay
there. Only the *result*-type inference in `const_eval` was wrong.

So `is_array_from` keeps its full list and gains a doc comment saying
which of the
two questions it answers, and `const_eval` drops its call to it.
`Array.from(x)`
keeps evaluating to `NewArray` through `is_array_factory`, which already
matches
`Array.{from,of}` with any argument count.

## Behaviour

| expression | before | after |
|---|---|---|
| `[...new Uint8Array(b).slice(0, 12)]` | reported, fixed | passes |
| `[...new Uint8Array(b).map(f)]` | reported, fixed | passes |
| `[...new Uint8Array(b).filter(f)]` | reported, fixed | passes |
| `[...new Uint8Array(b).toReversed()]` | reported, fixed | passes |
| `[...new Uint8Array(b).toSorted()]` | reported, fixed | passes |
| `[...new Uint8Array(b).with(0, 1)]` | reported, fixed | passes |
| `[...Uint8Array.from([1, 2, 3])]` | reported, fixed | passes |
| `[...new Uint8Array(b).slice(0).map(f)]` | reported, fixed | passes |
| `[...[1, 2, 3].slice(0, 2)]` | reported, fixed | unchanged |
| `[...Array.from(foo)]` | reported, fixed | unchanged |
| `BigUint64Array.from([...iterable])` | reported, fixed | unchanged |
| `[...new Uint8Array([1, 2, 3])]` (#7936) | passes | unchanged |

## Tests

19 `pass` vectors added under a `#25868` comment next to the existing
`#7936` pair:
the seven methods from the issue, a chained case, and all 11 typed array
constructors. The `fail` vectors are untouched, which is the regression
check that
plain arrays and `Array.from` still report.

## Known limits, both deliberate

**Variable receivers still report.** `[...view.slice(0, 12)]` where
`view` is a
`Uint8Array` needs type information. #24107 asked for that general case
and was
closed on the grounds that upstream's `isArray` is type-aware and this
rule is not
moving to tsgolint. This PR stays inside the syntactic subset, where the
receiver is
literally `new <TypedArray>(...)` or `<TypedArray>.from(...)`.

**A shadowed constructor is a false negative.** With `class Uint8Array`
in scope,
`[...new Uint8Array().slice(0)]` is a genuine useless spread and is no
longer
reported. This matches how `is_new_typed_array` and every other name
matcher in the
file already behave, so it introduces no new inconsistency. Happy to add
a
global-reference check if you'd rather have it here.

## Verification

```
cargo check -p oxc_linter                 clean
cargo test -p oxc_linter no_useless_spread  rules::unicorn::no_useless_spread::test ... ok
```

The snapshot is unchanged, so no existing `fail` case flipped.

Control, to show the tests have teeth: reverting `const_eval.rs` to its
previous
state while keeping the new vectors fails the test with 19 diagnostics,
one per new
vector.

## AI usage disclosure

Per the AI usage policy: this patch was drafted with Claude Code,
including the
`ValueHint::NewTypedArray` approach and the test vectors. Everything in
the
verification section was run locally against this branch rather than
asserted, and
the control run is there because a test that passes with and without the
fix would
prove nothing. Happy to take the change in a different direction if
you'd prefer the
narrower receiver guard described in #25868.

---------

Co-authored-by: Cameron <cameron.clark@hey.com>
A
Aadharsh Pannirselvam committed
03ef0f27ba6183039f51a9040a2e198fdcd5c1b8
Parent: bd15905
Committed by GitHub <noreply@github.com> on 8/26/2026, 8:47:24 AM