Commit Graph

5299 Commits

Author SHA1 Message Date
camc314 aa38ddfbea fix(linter/unicorn/numeric-separators-style): correct schema defaults (#26393)
## Summary

Fix the generated defaults for `unicorn/numeric-separators-style`. The rule already uses `number.minimumDigits: 5`, matching Unicorn, but the generated documentation and JSON schema advertise `0`. The same schema issue also advertises `groupLength: 0` for every numeric base.

## Why the defaults are wrong

`NumericBaseConfig` is shared by binary, hexadecimal, octal, and decimal configuration. Its derived `Default` initializes both numeric fields to zero. The rule supplies the actual defaults in `NumericSeparatorsStyleConfig::default()` and `NumericNumberConfig::default()`.

Schema generation uses the shared type's defaults for its individual properties. In particular, flattening `NumericBaseConfig` into `NumericNumberConfig` preserves the zero-valued property defaults instead of the decimal defaults. This makes the documented threshold disagree with the rule's existing behavior:

```js
1234;   // Allowed with the default configuration.
12345;  // Reported; the fix is 12_345.
```

## Change

Add a single parameterized schema helper for `NumericBaseConfig`, then select the appropriate defaults with `schemars(schema_with = ...)` on each field. The helper retains the derived schema's property descriptions, types, and constraints, and overrides only the `groupLength` and `minimumDigits` default metadata.

The resulting defaults are:

| Configuration | `groupLength` | `minimumDigits` |
| --- | --- | --- |
| `binary` | 4 | 0 |
| `hexadecimal` | 2 | 0 |
| `octal` | 4 | 0 |
| `number` | 3 | 5 |

The schema annotation on the flattened decimal base also corrects `number.minimumDigits`. `number.fractionGroupLength` remains unchanged, and the documentation continues to display its default as `Infinity`.

Regenerate the JSON configuration schema and the TypeScript configuration types from it. The generated TypeScript types inline the binary, hexadecimal, and octal option objects because those properties now have individual schemas; their accepted configuration shapes are unchanged.

This changes schema and documentation metadata only. Runtime defaults, option parsing, diagnostics, and autofixes are unchanged.

Fixes #26391
2026-09-07 13:48:04 +00:00
overlookmotel 66744f0eb9 feat(parser)!: rename panicked to fatal_error in ParserReturn (#26382)
`ParserReturn` contains a property `panicked`.

The name `panicked` is misleading. The parser did _not_ panic. When `true`, it means that the parser encountered a fatal error and is unable to parse the source, because it contains an unrecoverable syntax error.

Rename this property to `fatal_error`, to better describe what it means.

This is a breaking change, but one that's trivial to fix in downstream code - the property doesn't change its meaning or behavior, just its name.

All the changes in this PR in other crates are just reacting to this change and making everywhere use the term "fatal error" instead of "panicked", so the codebase is consistent.
2026-09-07 09:41:52 +00:00
Mikhail Baev 562dd14e3d fix(jsx-a11y/anchor-has-content): allow anchors passed as custom component props (#26360) 2026-09-06 10:54:29 +00:00
Pearce Ropion 253cdb2f46 fix(linter/eslint/id-length): exempt TS interface/type-literal members with never (#26310)
## Summary

The `id-length` lint rule lets users opt out of checking property name
lengths via a `properties: "never"` setting. This exemption worked
correctly for plain JavaScript object properties, but was silently
ignored for TypeScript interface members and object type literal
members. As a result, users who explicitly disabled property length
checks would still see false-positive warnings on their TypeScript type
definitions.

## Why

TypeScript interfaces and type literals describe object shapes using
their own dedicated syntax, distinct from JavaScript object literals.
The rule's internal logic only recognized the JavaScript form when
deciding whether to skip the length check, so the TypeScript equivalents
were treated as ordinary identifiers instead of property names, even
though users intended the same exemption to apply to both.

## Changes

- The rule now recognizes TypeScript interface and type literal members
as property names, so the `properties: "never"` setting applies to them
consistently with plain JavaScript object properties.
- Added test coverage confirming both the exempted and non-exempted
behavior for TypeScript interface and type literal members.

## Test plan

- Added passing test cases showing the exemption now applies to
TypeScript interface and type literal members.
- Added failing test cases confirming the rule still flags these members
under its default settings.
- Verified the fix manually against the exact reproduction case reported
in the issue, confirming the false-positive warning no longer occurs.
- Full linter test suite passes with no regressions.

## AI disclosure

Per the repo's AI usage policy: this fix was drafted with AI assistance
(Claude), and reviewed, tested, and understood by me before submission.
The linked issue itself was also AI-drafted and reviewed by me.

Resolves #26309

---------

Signed-off-by: Cameron <cameron.clark@hey.com>
Co-authored-by: Cameron <cameron.clark@hey.com>
2026-09-04 09:54:59 +01:00
camc314 5f9cffc753 fix(linter/typescript/no-explicit-any): fix rest parameter to unknown[] (#26279)
## Summary

- replace bare any rest parameter annotations with unknown[] when fixToUnknown is enabled
- preserve existing element-wise fixes for any[] and Array<any>
- add regression coverage for function declarations, arrow functions, call signatures, and method signatures reported in https://github.com/typescript-eslint/typescript-eslint/issues/12813
2026-09-03 09:46:00 +00:00
camc314 4de9fa3d44 fix(linter/react/jsx-no-literals): check nested JSX expression literals (#26253)
## Summary

- report text literals in JSX nested inside expression containers
- preserve upstream element override inheritance through nested JSX and fragments
- add regression coverage for logical, conditional, and plain JSX expressions

Fixes #26208
2026-09-03 09:01:50 +00:00
camc314 a662c40576 fix(linter/eslint/no-useless-assignment): handle destructuring assignments (#25925)
## Summary
- recognize destructuring assignment targets when ordering reads and writes
- record the target write after the assignment right side and target defaults
- add array, object, and defaulted destructuring regression coverage

Fixes #25923 Closes https://github.com/oxc-project/oxc/pull/25924
2026-09-02 15:56:17 +00:00
Hamody We 304bd9aef7 fix(linter/eslint/no-array-constructor): handle ASI hazards in fixer (#26166)
## What

`eslint/no-array-constructor`'s fixer replaced the call with `[...]`
unconditionally, so a `[` could land at the start of a line whose
predecessor has no semicolon:

```js
foo
Array()
```

`oxlint --fix` rewrote that to

```js
foo
[]
```

which is a member access on `foo` and does not parse — `Unexpected
token`. The fix is applied to the file, so the result is a source file
the user can no longer build.

This is not a hypothetical: the linter's own fixer harness catches it.
Adding `("foo\nArray()", …)` to the fix table on `main` fails with

```
Linter fixer produced invalid syntax.
Input code:  foo\nArray()
Fixed code:  foo\n[]
Parse errors: "Unexpected token"
```

The two cases at the bottom of that table were commented out with
*"TODO: These currently produce invalid syntax, need to fix the fixer."*
rather than fixed, so nothing in CI was exercising it.

## Why this shape

`ast_util::could_be_asi_hazard` already answers exactly this question,
and every other fixer that can emit a statement-initial `[` calls it:

| rule | uses helper |
|---|---|
| `unicorn/no-new-array` | ✅ |
| `unicorn/prefer-spread` | ✅ |
| `unicorn/no-negation-in-equality-check` | ✅ |
| `unicorn/prefer-response-static-json` | ✅ |
| `shared/no-negated-condition` | ✅ |
| **`eslint/no-array-constructor`** | ❌ — this PR |

I checked the other fixers that emit a leading `[` (`no-div-regex`,
`no-iterator`, `consistent-empty-array-spread`,
`no-single-promise-in-promise-methods`); none of them can be at
statement start, so this rule was the only gap. That also means no new
machinery — one call to the helper the codebase already trusts.

---

*Found by sweeping the linter for fixers that can emit a
statement-initial `[`, after noticing the `no-array-constructor` TODO.*

**AI disclosure:** this change was prepared with the assistance of
Claude Code. I reviewed the change and ran the verification above before
opening, and I stand behind them as my own.

*Branch note: based on an older `main` because the token used here lacks
the `workflow` scope needed to push newer upstream commits touching
`.github/workflows/` (the fork cannot be synced for the same reason).
`no_array_constructor.rs` is identical on both bases and
`could_be_asi_hazard` is unchanged, so it applies to current `main`
as-is — happy to rebase.*

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-02 16:48:11 +01:00
camc314 b41bb35bca fix(linter): qualify rules in rules output (#26250)
## Summary

- preserve plugin names when tracking default and enabled rules
- prevent same-named rules from inheriting another plugin’s state in Markdown and JSON output
- add regression coverage for all known rule-name collisions

Fixes #26209.
2026-09-02 13:29:56 +00:00
camc314 6a0e19cfaa feat(linter/eslint/no-unmodified-loop-condition): support checkConditionalExpressions option (#26249)
## Summary

- add the ESLint-compatible `checkConditionalExpressions` option to `no-unmodified-loop-condition`, defaulting to `false`
- port tests added by eslint/eslint#21175 and add regression coverage for repeated references within and across binary groups

Closes #26182.
2026-09-02 10:39:45 +00:00
camc314 895c6857e1 fix(linter/typescript/no-extraneous-class): avoid deleting class expressions (#26231)
## Summary

- avoid suggesting deletion for empty class expressions
- keep deletion suggestions for removable class declarations
- add regression coverage for class expressions used as object property values

Fixes #26228
2026-09-01 16:46:44 +00:00
camc314 4ba33c81d1 fix(linter/eslint/no-unused-vars): handle stored arrow references (#26225)
## Summary

- recognize references returned from arrow functions stored on properties
- treat arrow functions passed to calls and constructors as independently callable
- preserve self-use detection for immediately invoked arrow functions
- add regression coverage based on VSC-NeuroPilot/neuropilot
2026-09-01 15:02:56 +00:00
connorshea 464ddd1806 docs(linter): support a shared short description for jest/vitest rules (#26186)
AI Disclosure: Generated with Claude Code, Fable 5. Reviewed by me. This sets up the short_description to be added to the various rules shared between jest and vitest.

`declare_oxc_lint!` now accepts a path to a shared constant for `short_description`, mirroring how `docs = SHARED_DOCUMENTATION` works. Like `documentation`, the handling is gated behind the `ruledocs` feature.

A shared short description is added to `expect-expect`, `no-disabled-tests`, and `valid-expect`.

Rules where the wording legitimately differs per framework (e.g. `no-alias-methods`) can keep per-plugin string literals, which remain supported. Or we can opt to add the raw strings on every one of these rules and drop the shared short desc idea, I don't have a strong preference either way.

Once we decide and merge this, we can add the short descriptions for all the other remaining shared rules. We'll also need to split up the two remaining rules that are problematic (https://github.com/oxc-project/oxc/issues/23186), then we can expose the `short_description` field in the JSON rules formatter for use on the website's rules list view.
2026-08-30 20:40:00 +00:00
connorshea 9db5ad39b5 docs(linter): add short description to vue/no-dupe-keys (#26183)
This rule was the only Vue rule missing a short description, so this fixes that.
2026-08-30 19:49:05 +00:00
billychannnnnn db66f5821d docs(linter): correct export/import mismatch in bar and foo example (#25927)
The example incorrectly used a named import for a default export.
Changed `export default function bar()` to `export function bar()` to
align with the `import { bar }` statement.

Signed-off-by: billychannnnnn <billychannnnnn@foxmail.com>
2026-08-29 19:59:18 +02:00
Adrian Schaedle 33ac4b0915 fix(linter/lsp): prevent tsgolint from holding onto processes (#25570)
Attempts to address:
- oxc-project/oxc#17070
- oxc-project/oxc#21402,
- oxc-project/oxc-vscode#264

I recently started enabling the type-aware rules as part of my editor's
linter (zed). I quickly started hitting resource exhaustion limits on
macOS. Listing processes showed thousands of zombie tsgolint entries
with oxlint as the parent.

Doing a little digging, I found the above issues listing similar
resource exhaustion. oxc-project/oxc#24670 attempted to solve this issue
by calling `child.kill` on the tsgolint process, but this is not enough
on macOS. From [the `Child`
docs](https://doc.rust-lang.org/std/process/struct.Child.html):

> On some systems, calling wait or similar is necessary for the OS to
release resources. A process that terminated but has not been waited on
is still around as a “zombie”. Leaving too many zombies around may
exhaust global resources (for example process IDs).
>
> The standard library does not automatically wait on child processes
(not even if the Child is dropped), it is up to the application
developer to do so. As a consequence, dropping Child handles without
waiting on them first is not recommended in long-running applications.

This change follows this advice and adds a `wait` after the existing
`kill`.
2026-08-29 15:12:42 +02:00
camc314 baf4b1e00a fix(linter/import/no-empty-named-blocks): make empty value import removal a suggestion (#26155)
## Summary

- classify removal of empty value imports as a suggestion because deleting them can drop module evaluation side effects
- keep removal of empty type-only imports as an automatic fix
- preserve the rule’s existing deletion behavior
2026-08-28 16:29:00 +00:00
camc314 77cfaec3f3 fix(linter/eslint/object-shorthand): preserve __proto__ semantics (#26154)
## Summary

- exclude non-computed `__proto__` properties from shorthand and longform conversions
- preserve computed `__proto__` properties as safely fixable
- add regressions for methods, arrows, property shorthand, quoted keys, and `never` mode
2026-08-28 15:40:47 +00:00
camc314 fa3c0825e2 fix(linter/unicorn/prefer-set-size): ignore shadowed Set constructors (#26153)
## Summary

- verify that new Set constructors resolve to the intrinsic global before reporting or fixing
- preserve detection for direct constructions and const-bound Set instances
- add regression coverage for a locally shadowed constructor that returns an array
2026-08-28 15:30:27 +00:00
camc314 e412cf2ce3 fix(linter): clamp invalid JS plugin locations (#26144)
Follow-up to #26138, closes #25867.

External plugins can report a node range past the source text. The default formatter rejects that invalid span and previously panicked. Clamp external diagnostic offsets to the source's UTF-16 length before normalizing reversed ranges.

The end-to-end fixture now covers:
 - reversed locations
 - negative loc values
 - negative node ranges
 - an out-of-range node range in Unicode source text
2026-08-28 13:33:32 +00:00
camc314 d86c1137c4 fix(linter): normalize reversed JS plugin locations (#26138)
## Summary
- normalize reversed JS plugin diagnostic locations to empty spans at their stated start
- add an end-to-end regression fixture for reversed `loc` positions

https://github.com/oxc-project/oxc/issues/25867
2026-08-27 11:11:26 +00:00
camc314 73c09b212e fix(linter/eslint/no-use-before-define): run on JS, JSX files (#26114)
Fixes #26113.

The rule was moved from the TypeScript plugin to the ESLint plugin in 79fc6d7e9f, but its TypeScript-only execution gate was retained. As a result, eslint/no-use-before-define never ran for JavaScript or JSX files.Remove the gate and add a JavaScript regression test for a temporal-dead-zone reference.
2026-08-26 14:55:36 +00:00
Aadharsh Pannirselvam 03ef0f27ba 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>
2026-08-26 08:47:24 +00:00
camc314 bd15905291 fix(linter/react/no-unstable-nested-components): check nested component object property names (#26101)
## Summary

- check the immediate object property before the enclosing JSX attribute when matching `propNamePattern`
- preserve matching through either the object property or JSX attribute
- add coverage for inline arrays of render-property objects

Fixes #26086
2026-08-26 08:23:18 +00:00
camc314 d5be0375e6 docs(linter/typescript/switch-exhaustiveness-check): clarify default case comment pattern (#26100)
## Summary
- clarify that `defaultCaseCommentPattern` recognizes a trailing comment in place of an omitted `default` clause
- document the required location, absence of a live `default`, and the `considerDefaultExhaustiveForUnions` requirement for union switches
- regenerate the JSON schema and TypeScript configuration types

Fixes #26087
2026-08-26 08:14:27 +00:00
Mikhail Baev 60b945d61d feat(linter/nextjs/no-typos): implement suggestion (#26091)
add suggestion for `nextjs/no-typos` rule, upstream rule rn doesnt
provide suggestion, but since we already have the matched suggestion, I
think it should be safe to implement

issue #1929
2026-08-26 08:41:18 +01:00
Hamody We 3910e2b9b1 fix(linter/eslint/no-unassigned-vars): skip Svelte and Vue files (#26042)
## Summary

Closes #26038.

`no-unassigned-vars` is a `correctness` rule, so this fires by default
with no config:

```svelte
<script lang="ts">
  let dialog: HTMLDialogElement;

  function openDialog() {
    dialog.showModal();
  }
</script>

<dialog bind:this={dialog}>Hello</dialog>
```

```text
repro.svelte:2:7: warning eslint(no-unassigned-vars): 'dialog' is always 'undefined' because it's never assigned.
```

## Why the rule cannot get this right today

The rule's decision is `has_read && no reference is_write()`, taken over
the symbol's references in the current script block. Oxlint reaches
`.svelte`/`.vue` through `PartialLoader`, which extracts the `<script>`
blocks and throws the markup away before parsing — so the template's
write is not in the AST the rule sees, and no reference on `dialog` is
ever a write. Every ingredient of the diagnostic is satisfied, and the
diagnostic is still wrong: Svelte assigns the element to `dialog` on
mount.

The same shape exists in Vue, where a `<script setup>` `let` is a
`setup-let` binding that `v-model="x"` and inline handlers such as
`@click="x = 1"` compile to direct assignments against.

This is not a matter of tightening the check — the write is in a part of
the file the linter never parses.

## The change

`prefer-const` hit exactly this and already skips both extensions
([#25148](https://github.com/oxc-project/oxc/pull/25148)):

```rust
fn should_run(&self, ctx: &crate::context::ContextHost) -> bool {
    // ignore svelte/vue: their templates can reassign a binding, which we can't see.
    !ctx.file_extension().is_some_and(|ext| ext == "svelte" || ext == "vue")
}
```

This applies the sibling rule's guard rather than inventing one, and
documents it in the rule docs under an `#### Ignored Files` heading,
matching `prefer-const`'s wording.

`.astro` is deliberately **not** included: an Astro template renders a
frontmatter binding but has no construct that writes back to it, so the
false positive does not arise there. `prefer-const` drew the same line.

## Scope, stated honestly

This trades a false positive for lost coverage: a genuinely unassigned
variable inside a `.svelte`/`.vue` `<script>` block is no longer
reported. That matches the precedent and the reporter's own request, and
a `correctness` false positive that fires on the documented Svelte idiom
costs more than the missed report. Restoring coverage needs
template-aware analysis, which is the language-support effort tracked
separately (#13017, #17991).

## Verification

- `cargo test -p oxc_linter --lib` — **1254 passed, 0 failed**.
- Two new tests, `test_svelte` and `test_vue`, cover the reported repro,
`bind:value`, a two-`<script>` Svelte component, `v-model`, and an
inline `@click` handler.
- **Revert-checked:** with `should_run` removed and the tests kept,
exactly those two tests fail and the rest pass. The failure output
reproduces the reported diagnostic verbatim, including the `let shared`
case from the two-script component — so the tests fail for the right
reason, not because the fixture path changed.
- `cargo clippy -p oxc_linter --lib --all-features` — no new findings
(the two `FromIterator::from_iter` warnings are pre-existing, in
`tsgolint.rs` and `lib.rs`).
- `cargo fmt --check` clean.
- The rule is not part of `docs_rule_pages.snap`, so the doc addition
needs no snapshot regeneration.

Built and tested on Windows with the `1.97.1-x86_64-pc-windows-gnu`
toolchain rather than the pinned `1.98.0`, and with `RUSTFLAGS`
overridden to drop the MSVC-only link args that `.cargo/config.toml`
applies to every `target_os = "windows"` target. Nothing in the
repository was changed for that.

---
Written with AI assistance (Claude Code), reviewed and tested by me
before submitting, per AGENTS.md.

---------

Signed-off-by: hamodywe <iosapk.org@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 08:32:41 +01:00
overlookmotel 047f7ca3b5 fix(linter/plugins): fix interaction between JS plugins and Vue rules (#26080)
While working on #26077, Claude spotted another related bug.

JS plugins have to run last because they need to mutate the AST, and in the process they destroy the stored AST.

For files with multiple sections (Vue, Astro, Svelte), we ran native rules then JS rules for each section in turn. That appeared fine - JS rules do run last. But the problem is that some rules e.g. `vue/valid-define-emits` also access _earlier_ sections. Because JS plugins already ran on those previous sections, their ASTs have been discarded already, leading to incorrect results.

Instead, lint in 3 separate passes which each run on _all_ sections before going on to the next pass:

1. Native rules.
2. JS rules.
3. Unused directives.

i.e.:

- Before: section 1 native, section 1 JS, section 2 native, section 2 JS.
- After: section 1 native, section 2 native, section 1 JS, section 2 JS.

Unused directives check has to be in a separate final pass, as it requires both native rules and JS rules to have run before it.

Looping over sections is cheap compared to the work that happens in each turn of the loops, so I very much doubt this has a measurable impact on perf.

This change does have one visible effect: The order of diagnostics will change for multi-section files. Previously diagnostics would be output in section order, now they're in rule type (native/JS) order. I don't _think_ that matters.
2026-08-25 13:19:59 +00:00
overlookmotel b13215f1e1 refactor(linter): remove source_text field from ContextSubHost (#26079)
`source_text` field was added to `ContextSubHost` in #25280 to work around the code that runs JS plugins (`run_external_rules`) nuking the whole of `Semantic`, replacing `semantic.source_text` with an empty string.

#26077 has removed that behavior, so we can now return to using `semantic.source_text`, and it remains what it should be, even after `run_external_rules`.
2026-08-25 13:00:36 +00:00
overlookmotel 8531b9b2a4 fix(linter/plugins): alter method for obtaining mutable Program when sending AST to JS plugins (#26077)
### What's needed for JS plugins

When running JS plugins, we need to obtain a mutable copy of the AST (`&mut Program`) in order to alter it (update spans to UTF-16 offsets). But `Semantic` only contains an immutable `&Program` reference, and also its `AstNodes` contains immutable references to _all_ nodes in the AST. So we need to do 2 things:

1. Convert the `&Program` from `Semantic` to `&mut Program`.
2. Get rid of all the active references to AST nodes stored in `AstNodes` so that the `&mut Program` doesn't illegally alias with those references.

### Previous solution and its problems

Previously we achieved this with a dodgy hack where we discarded all of `Semantic` (to get rid of the AST node references) and then "laundered" the pointer to the `Program`, upgrading it to write permission, by taking the provenance of the `Allocator`'s cursor pointer (which does have write permission).

However, this relied on the `Program` being in the `Allocator`'s current chunk, so that the `Allocator`'s current cursor and the `Program` are within the same allocation (a requirement for the pointer returned from `NonNull::with_addr` being legal to dereference).

This assumption was always a bit shaky, but when we enabled React Compiler rules by default in last release, it became clear it was completely untenable. React Compiler rules allocate a lot, and the debug assertions which checked that the `Program` was in current chunk started failing - so many allocations had occured that the `Allocator` had grown a fresh chunk to accomodate them.

### How this PR fixes the problem

This PR alters the method by which we obtain the `&mut Program`. It still discards all the references to AST nodes held in `Semantic`, but now performs a bitwise copy of the original `Program` and allocates that copy back into the arena, yielding a `&mut Program`.

This avoids the need for pointer "laudering" and removes the requirement for `Program` to be in the current `Allocator` chunk.

As noted in the comments, this is still not sound. We have no way to prove that no other references to AST nodes exist, stashed somewhere, which would be an aliasing violation (UB).

However, a thorough review of the code shows that we currently do not do this, so there is no actual UB at present.

So, in short, this change does not achieve soundness, but it does at least swap definite proven UB for a _potential risk_ of UB which isn't currently triggered. Not great, but better.

### Side effects

Previously we nuked the whole of `Semantic`, replacing it all with a new empty copy. Instead this PR operates more "surgically", just removing the AST node and `Comment` references that we _need_ to get rid of to avoid aliasing violations, and leaving the rest of `Semantic` alone. This is much cheaper.
2026-08-25 12:53:52 +00:00
camc314 dc464ff1bb fix(linter/unicorn/prefer-math-min-max): avoid unsafe autofix (#26060)
## Summary

- classify the rule replacement as a suggestion instead of a normal autofix
- preserve the diagnostic and suggested Math.min/Math.max replacement
- avoid changing behavior based on value analysis
2026-08-24 13:30:33 +00:00
camc314 17ae11cd9c fix(linter/oxc/double-comparisons): handle grouped logical expressions (#26044)
## Summary

- detect adjacent comparisons across left-associated logical-expression groups
- limit the diagnostic and suggestion span to the comparisons being simplified
- cover mixed precedence, parentheses, grouped `&&`, and reparsed suggestion outputs
2026-08-24 09:15:35 +00:00
Rahul Mishra 8a353a76e9 fix(linter/eslint/no-control-regex): refine help message text (#25996)
`eslint(no-control-regex)` correctly flags this regex
```ts
const fnKeyRe = /^(?:\x1b+)(O|N|\[|\[\[)(?:(\d+)(?:;(\d+))?([~^$])|(?:1;)?(\d+)?([a-zA-Z]))/
```

`'U+001B' is a control character.` and shows in help text, `help: Avoid
matching control characters in regular expressions. If intentional,
consider using a Unicode escape instead.`
but the help text is misleading, all forms of unicode escapes are also
flagged.

```ts
const a = /\x1b/;
const b = /\u001b/;
const c = /\u{1b}/u;
const d = /\u{001b}/u;
```
This behaviour is consistent with upstream eslint rule, just the help
text is misleading, so I have changed the help text to say to disable
this rule when its intentional
2026-08-24 07:47:09 +00:00
Mikhail Baev 70c3e35356 feat(linter/typescript/no-confusing-non-null-assertion): implement suggestion (#26012)
implements suggestion for `typescript/no-confusing-non-null-assertion`,
used tests from upstream. Issue #2180
2026-08-23 18:14:00 +01:00
Cameron 8d94cd1635 fix(linter/eslint/no-useless-rename): preserve type modifiers (#26020)
Preserves inline `type` modifiers when `no-useless-rename` removes a
redundant import or export alias, avoiding semantic changes under
`verbatimModuleSyntax`.

fixes #26004
2026-08-23 17:43:25 +01:00
Boshen 2cde1f6671 fix(rust): address nightly deprecations (#25998)
Addresses deprecations reported by the latest Rust nightly while preserving Rust 1.98 compatibility.

- Rename `unit-bindings` to `unit_bindings`: hyphenated lint names are deprecated in favor of their canonical underscore spelling.
- Replace `isize::max_value()` and `usize::max_value()` with `isize::MAX` and `usize::MAX`: the legacy numeric methods are deprecated in favor of associated constants.
- Set explicit `MAX_INDEX` values on `define_index_type!`: `oxc_index` otherwise generates `<raw>::max_value() as usize`; the explicit modern constants avoid that deprecated expansion without changing the index limits.
- Remove obsolete `clippy::legacy_numeric_constants` expectations: the corrected code no longer triggers that lint, so the expectations would be unfulfilled.
- Allow the AST scanner to consume optional `define_index_type!` configuration: it previously parsed only the struct declaration and rejected the added `MAX_INDEX` option.

AI-assisted.
2026-08-22 11:34:02 +00:00
renovate[bot] d5d575c1de chore(deps): update rust to v1.98.0 (#25945)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [rust](https://rust-lang.org/)
([source](https://redirect.github.com/rust-lang/rust),
[changelog](https://redirect.github.com/rust-lang/rust/blob/main/RELEASES.md))
| toolchain | minor | `1.97.1` → `1.98.0` |

---

### Configuration

📅 **Schedule**: (in timezone Asia/Shanghai)

- Branch creation
  - "before 10am on monday"
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update
again.

---

- [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
this box

---

This PR was generated by [Mend Renovate](https://mend.io/renovate/).
View the [repository job
log](https://developer.mend.io/github/oxc-project/oxc).

<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0NC4zNS40IiwidXBkYXRlZEluVmVyIjoiNDQuMzUuNCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOltdfQ==-->

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Cameron <cameron.clark@hey.com>
2026-08-20 18:04:07 +00:00
Connor Shea 51d36d7228 fix(linter/vue): resolve vue imports via shared import helpers (#25903)
AI Disclosure: Generated with Claude Code, Opus 5. Tested and reviewed
by me.

Why do many code when few do trick?

Follow-up to #25901. Hand-rolled import checks can simply be replaced
with the shared `is_import_symbol` helper, this also helps improve the
accuracy of the code to catch aliased imports for one of them.

### Bug fixed

`is_vue_define_component_reference` (used by
`vue/no-deprecated-delete-set` to decide whether a `this.$set` /
`this.$delete` call sits inside a Vue component) gated on the **local**
name being `defineComponent` before walking up to the
`ImportDeclaration`, so an aliased import was never recognised:

```vue
<script>
import { defineComponent as dc } from 'vue'
dc({
  mounted () {
    this.$set(obj, key, value)
    this.$delete(obj, key)
  }
})
</script>
```

Neither call was reported before; both are now. Covered by a new test
case.

### Pure simplifications

- `is_imported_set_or_del_from_vue` (`no-deprecated-delete-set`) — 20
lines → two `is_import_symbol` calls.
- `is_vue_next_tick_import` (`utils/vue.rs`, used by
`vue/next-tick-style` and `vue/valid-next-tick`) — 18 lines → one call.

Both already matched by symbol id, so behavior is unchanged.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 09:05:56 +01:00
Connor Shea 83a68d2296 fix(linter/react/no-react-children): resolve react imports by symbol (#25901)
AI Disclosure: Generated with Claude Code, Opus 5. Tested and reviewed
by me.

`no-react-children` decided whether an identifier came from `react` with
a hand-rolled helper that matched by **name**:

```rust
fn is_imported_from_react(local_name: &str, ctx: &LintContext) -> bool {
    ctx.module_record().import_entries.iter().any(|entry| {
        entry.module_request.name() == "react" && entry.local_name.name() == local_name
    })
}
```

This replaces it with the existing shared helpers, `is_import_symbol`
and `is_import_from_module`.

### Bugs fixed

Four false positives, where a local binding shadowed the import:

```js
import { Children } from 'react';
function f(x) { const Children = { count: () => 0 }; return Children.count(x); }

import React from 'react';
function f(x) { const React = { Children: { map: () => {} } }; return React.Children.map(x); }

import { Children } from 'react';
export function f(Children) { return Children.count(1); }

import { count as Children } from 'react'; Children.toArray(x)
```

One false negative, an aliased import the local-name check couldn't see:

```js
import { Children as C } from 'react'; C.toArray(x)
```

All five are covered by new test cases.

### Performance

This has a minor downside in perf, but it's significantly less than a
millisecond difference and is probably worth the downside. We had
originally decided not to care about aliases when implementing this
rule, but this change allows us to reuse the existing util code for
checking where the import comes from, and fixes the aforementioned edge
cases, so I think this is a worthwhile change.

Measured with two `--features allocator` builds, paired and interleaved,
using `--debug=timings --threads=1` to isolate the rule's own time from
parse/semantic noise.

| corpus | base | new | change |
| --- | --- | --- | --- |
| sentry | 4.416ms | 4.589ms | +3.9% slower |
| material-ui | 3.111ms | 3.217ms | +3.4% slower |
| bluesky | 0.503ms | 0.540ms | +7.2% slower |
| actualbudget | 2.012ms | 2.105ms | +4.6% slower |
| synthetic, `Children` used heavily | 19.112ms | 15.443ms | 19.2%
faster |

There is a small, consistent regression on real code, and it's
deliberate. Dropping the `ident.name == "Children"` gate means every
`obj.method()` call in a file importing `react` now does two array
lookups instead of one string compare; removing the `O(imports)` scan
only pays off where `Children` is actually used, which is 27 files out
of 8.8K in sentry. The absolute cost is ≤0.17ms, and `no-react-children`
isn't a default rule anyway.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Cameron <cameron.clark@hey.com>
2026-08-19 08:05:03 +00:00
Connor Shea 124e196551 fix(linter): resolve globals by reference, not by name (#25905)
AI Disclosure: Generated with Claude Code, Opus 5. Tested and reviewed
by me.

Ten call sites across nine rules checked whether an identifier is a
specific global with:

```rust
ctx.scoping().root_unresolved_references().contains_key(&ident.name)
```

That asks whether the **name** is unresolved **somewhere in the file**,
not whether **this reference** is global. Any file that shadows the
global in one scope and uses the real global in another gets a false
positive on the shadowed use:

```js
function f(myLogger) {
  const console = myLogger;
  console.log(1);        // reported by `no-console` — this is the bug
}
console.log(2);          // the real global, elsewhere in the file
```

`ctx.is_reference_to_global_variable(ident)` already exists for this and
is used at ~50 other call sites. It is `reference.symbol_id().is_none()`
— one field read instead of hashing the name into the
unresolved-references map — and it honours the user's `globals: { X:
"off" }` config.

### Rules fixed

| rule | shadowed code that was wrongly reported |
| --- | --- |
| `eslint/no-console` | `const console = myLogger; console.log(foo)` |
| `eslint/no-constant-binary-expression` | `const String = MyString;
String(x) ?? y` |
| `eslint/no-new-native-nonconstructor` | `const Symbol = FakeSymbol;
new Symbol()` |
| `eslint/prefer-object-spread` | `const Object = MyObject;
Object.assign({}, a)` |
| `eslint/symbol-description` | `const Symbol = FakeSymbol; Symbol()` |
| `eslint/valid-typeof` | `const undefined = u; typeof x === undefined`
|
| `promise/avoid-new` | `const Promise = FakePromise; new Promise(fn)` |
| `unicorn/new-for-builtins` | `const String = MyString; new
String('x')` |
| `unicorn/prefer-global-this` | `const window = fake; window.foo` |

Each rule gets a pass test in that shape. Both halves matter — without a
second, genuine use of the global the name lookup already returns false,
so a single-scope test would pass before the fix too.

Three of these ship a fix or suggestion (`prefer-global-this`,
`valid-typeof`, `no-console`), so the false positive was rewriting or
deleting code the user owns.

### Real-world impact

Base vs. branch over four corpora (vscode, sentry, material-ui, actual),
each rule isolated with `-A all -A nursery -D <rule> --format=json
--no-ignore --disable-nested-config`, diagnostics normalised to `(file,
span, code, message)`:

**0 added, 23 removed.** Every removal is `prefer-global-this` on a
genuinely shadowed `window`:

- `vscode/src/vs/base/browser/dompurify/dompurify.js` (16) — `let window
= arguments.length > 0 ? arguments[0] : getGlobal()`
- `material-ui/packages-internal/core-docs/src/Demo/DemoSandbox.tsx` (5)
— `function IsolatedDemo({ children, cssVarPrefix, colorSchemeNode,
window })`, a destructured prop
- `sentry/src/sentry/templates/sentry/error-page-embed.js` (2) —
`(function (window, document, JSON) { ... })(window, document, JSON)`,
the classic IIFE parameter

In each file the real global references outside the shadowing scope are
still reported.

### Notes

- `no-console` uses `ident.is_global_reference(ctx.scoping())` rather
than the context helper. An existing fail case pins that `globals: {
console: "off" }` must still report, and the context helper treats
`"off"` as "not a global". The other eight sites use the context helper,
so they now respect that config, but this one doesn't.
- Five upstream ESLint pass cases that were commented out in
`no-constant-binary-expression` now pass and are re-enabled (`function
Boolean(n) { return n; }; Boolean(x) ?? foo` and friends). Two others in
that list stay commented — they are `function foo(undefined) { ... }`,
which is a separate problem: the rule matches `undefined` by name via
`Expression::is_undefined()`. Left a TODO pointing at it.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Cameron <cameron.clark@hey.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Cameron <cameron.clark@hey.com>
2026-08-19 08:03:44 +00:00
connorshea 9b7e15362d docs(linter): set version to 1.79.0 for rules shipped in 1.79.0 (#25902)
The 22 React Compiler rules split out by #25500, plus `eslint/no-unreachable-loop`, all first shipped in oxlint 1.79.0 but were still marked `version = "next"`. I think the release PR must've failed to run this part of the release task?

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-08-19 02:52:17 +00:00
Boshen a701bccac9 fix(linter): remove invalid React compiler doc links (#25900)
The shared React Compiler rule macro generated `react.dev` links for internal categories without public lint pages. Render those attributions without links and verify linked versus unlinked rule documentation.

Validated with `just ready`.

AI-assisted.
2026-08-19 02:19:06 +00:00
Boshen 345f9818c5 fix(react_compiler): skip node_modules by default (#25859)
## Summary
- skip React Compiler transform and lint analysis for `node_modules` by default
- let explicit transform `sources` allowlists opt dependencies back in
- document the behavior and add regression coverage

## Validation
- focused Oxlint and transform-react checks
- Clippy with warnings denied
- `just ready` reached the all-features suite; three Oxlint ANSI-color snapshots differed in this environment

AI assistance: Codex was used to implement and verify this change.
2026-08-18 13:50:31 +00:00
Michaël Bitard 2f40afa45b fix(linter/unicorn/no-array-callback-reference): ignore effect library imports (#25857)
Fix #25856

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-08-18 14:49:54 +01:00
Boshen 7b565a0344 fix(linter): align React Compiler rule categories (#25840)
Align React Compiler rule categories with the ESLint presets in `babel-plugin-react-compiler@0.0.0-experimental-a1856f3-20260507`. Rules that are off upstream no longer use Oxlint's default correctness category, while `incompatible-library` uses correctness because it is recommended upstream.

| Rule name | ESLint preset | Oxlint category | Note |
| --- | --- | --- | --- |
| `error-boundaries` | `recommended` | `correctness` |  |
| `globals` | `recommended` | `correctness` |  |
| `immutability` | `recommended` | `correctness` |  |
| `incompatible-library` | `recommended` | `correctness` |  |
| `preserve-manual-memoization` | `recommended` | `correctness` |  |
| `purity` | `recommended` | `correctness` |  |
| `refs` | `recommended` | `correctness` |  |
| `set-state-in-effect` | `recommended` | `correctness` |  |
| `set-state-in-render` | `recommended` | `correctness` |  |
| `static-components` | `recommended` | `correctness` |  |
| `use-memo` | `recommended` | `correctness` |  |
| `unsupported-syntax` | `recommended` | `restriction` | Compiler support boundary. |
| `config` | `recommended` | Not implemented | Oxlint uses fixed, valid compiler options. |
| `gating` | `recommended` | Not implemented | Oxlint does not expose compiler gating options. |
| `void-use-memo` | `recommended-latest` | `correctness` | Enabled by the newer upstream preset. |
| `no-deriving-state-in-effects` | `off` | `perf` | Performance and derived-state guidance. |
| `invariant` | `off` | `restriction` | Internal compiler invariant. |
| `rule-suppression` | `off` | `restriction` | Compiler policy restriction. |
| `syntax` | `off` | `restriction` | Compiler syntax restriction. |
| `todo` | `off` | `restriction` | Unimplemented-feature diagnostic; a hint upstream. |
| `capitalized-calls` | `off` | `suspicious` | Kept out of default correctness. |
| `exhaustive-effect-dependencies` | `off` | `suspicious` | Dependency diagnostic, not default correctness. |
| `hooks` | `off` | `suspicious` | Overlaps the non-compiler `rules-of-hooks` rule. |
| `memo-dependencies` | `off` | `suspicious` | Overlaps the non-compiler `exhaustive-deps` rule. |
| `fbt` | `off` | Not implemented | This is a Meta-internal FBT category. |
| `memoized-effect-dependencies` | `off` | Not implemented | Upstream's `EffectDependencies` category is absent from the Rust compiler port. |

Validated with `just ready` and the category integration test after rebasing onto main.

AI-assisted: Codex was used to implement and validate this change.
2026-08-18 12:57:01 +00:00
Boshen 228e8e0f85 fix(linter): resolve inactive React compiler rules (#25830)
Enable React Compiler memo dependency validation so `react/memo-dependencies` reports missing and extra values. Remove `react/gating` until Oxlint exposes compiler gating options.

Validated with `just ready`.

AI-assisted.
2026-08-18 16:16:24 +08:00
camc314 f8e488496f fix(linter/eslint/no-useless-constructor): allow parameter properties (#25811)
## Summary
 - avoid reporting forwarding constructors that declare TypeScript parameter properties
 - share the modifier and decorator check across base and derived constructors
 - cover private, public, protected, readonly, and combined parameter-property variants

Closes #25806
2026-08-18 07:44:01 +00:00
Boshen e8303d6fc8 perf(linter): shrink rule serialization dispatch (#25818)
Generate `RuleEnum::to_configuration` arms only for the 33 rules that override the trait default, returning `None` for all other rules.

Release `oxlint`: 12,491,264 → 12,491,200 bytes (-64 bytes). `__text`: 10,589,512 → 10,585,376 bytes (-4,136 bytes).

AI-assisted; reviewed and validated by the contributor.
2026-08-18 07:38:14 +00:00
Boshen c84ede371b perf(estree_tokens): share JS token update entry point (#25826)
Add a shared non-generic JS-style token update entry point so `oxc_linter` and `oxlint` do not each instantiate the ESTree token visitor.

On `aarch64-apple-darwin`, `cargo build -p oxlint --release --features allocator` reduces the `oxlint` binary by 49,616 bytes (48.5 KiB, 0.39%), from 12,756,960 to 12,707,344 bytes.

Validated with `just ready`.

AI assistance was used for implementation and measurement.
2026-08-18 07:19:31 +00:00
Mikhail Baev 9b7394ed8b feat(linter/typescript/no-empty-object-type): implement suggestion (#25833)
Implement suggestion for `typescript/no-empty-object-type` rule, issue
#2180. Tests ported from
[upstream](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/eslint-plugin/tests/rules/no-empty-object-type.test.ts)
2026-08-18 08:18:48 +01:00