Commit Graph

4742 Commits

Author SHA1 Message Date
Hamody We f6b22654d2 fix(linter/eslint/no-redeclare): run in ES modules, skipping only the globals check (#25691)
Closes #25339.

## The problem

`no-redeclare` gated the entire rule on the file not being an ES module:

```rust
fn should_run(&self, ctx: &ContextHost) -> bool {
    // ES modules run in their own scope, and don't conflict with existing globals
    !ctx.source_type().is_module()
}
```

The comment justifies the globals half of the rule, and it is right
about that: an ES module has its own top-level scope, so declaring
`Object` or `self` there shadows nothing, which is why ESLint reports
`builtinGlobals` redeclarations against the global scope rather than
inside a module. But the gate took the rest with it — plain `var a = 3;
var a = 10;`, redeclarations in nested scopes, and the TS type/value
collision check added for #10396 all stopped running too.

**One correction to the issue's framing, from testing it rather than
reading it.** `.ts` is parsed as `ModuleKind::Unambiguous`, which
resolves to `Module` only when ESM syntax is actually present. So a
`.ts` file with no `import`/`export` is a script, and the rule *did* run
there — `var a = 3; var a = 10;` on its own is reported today. What was
dead is every `.ts` file that contains an import or an export, which in
practice is nearly all of them. The effect the issue describes is real;
the boundary is ESM syntax, not the extension.

The rule's own tests hid this either way. Every failing case runs under
`.change_rule_path_extension(".cts")` — CommonJS, never a module — and
the single `.ts` block asserts a *pass*, which it would do whether or
not the rule executed.

## The change

Drop `should_run`, and narrow the exemption to the thing the comment
actually describes:

```rust
// ES modules run in their own scope, so a top-level `Object` or `self` shadows nothing.
// Only the globals half of the rule is skipped for them -- redeclaring a local binding is
// still a redeclaration.
let builtin_globals = if self.builtin_globals && !ctx.source_type().is_module() {
    Some(&GLOBALS_BUILTIN)
} else {
    None
};
```

`builtin_globals` is already the single switch for that half — it drives
`is_builtin`, which chooses between
`no_redeclare_as_builtin_in_diagnostic` and the plain redeclaration
diagnostic — so `None` in modules disables exactly the globals
reporting, for the builtin table and user-configured `globals` alike,
and leaves both redeclaration loops running.

## Tests

A new `.ts` block under the `es-modules` snapshot suffix. Every case
carries ESM syntax so it resolves as a module — otherwise it would test
the script path by accident, which is the trap above:

- **pass** — `export {}; var Object = 0;` and `export {}; var globalThis
= 0;` (a module shadows nothing, so these must stay quiet), plus a
redeclaration-free case and a TS overload pair.
- **fail** — `export {}; var a = 3; var a = 10;`; `export {}; function
f() { var a; var a; }` (nested scope — nothing to do with global scope
at all, and previously silent); the #10396 `type foo = 1; export
function foo(): void; …` case; and `export {}; var Object = 0; var
Object = 0;`.

That last one is the seam this change creates, so it is worth pinning:
inside a module the first `var Object` is no longer reported as
shadowing a builtin, but the second is still reported as a redeclaration
of the first. Without it, narrowing the gate could quietly have turned
builtin-named symbols into a blind spot.

The existing `.cts` blocks and their snapshot are untouched — `.cts` is
CommonJS, so `is_module()` was already false there.

## Verification

Ran locally with `cargo test -p oxc_linter --lib no_redeclare`: passes,
the existing `eslint_no_redeclare.snap` is unchanged, and the new
`eslint_no_redeclare@es-modules.snap` is included. `cargo clippy -p
oxc_linter --lib` is clean.

> AI disclosure, per the contributing policy: I used an AI assistant
while investigating and writing this. I have read and understand the
change, ran the tests myself, and stand behind it.

---------

Co-authored-by: Cameron <cameron.clark@hey.com>
2026-08-17 13:20:33 +00:00
Dunqing 2f5cdb1231 feat(minifier): add property name mangling (#24740)
Oxc can shorten variable names and private class names, but it cannot shorten property names such as `obj._field`. This change adds optional property mangling. It replaces #23772 and gives bundlers a way to use the same property names across several files.

### Feature

- `include` selects names to mangle. `exclude` and `reserved` keep names unchanged.
- The cache can give a property a fixed output name or keep it unchanged.
- The mangler supports member access, object and class keys, destructuring, optional chains, the left side of `in`, and JSX property names.
- `mangle_quoted` also handles strings and template literals used directly as property keys. `/* @__KEY__ */` and `/* #__KEY__ */` can mark one other string or template literal as a property key.
- Short names are assigned in a stable order. More common properties get shorter names. Existing and reserved names are avoided.
- Bundlers can collect names from several programs, create one shared map, and apply it to every program.
- Numeric keys, `__proto__`, `constructor`, `prototype`, TypeScript-only keys, and private names are never changed by this pass.

### Design

Property names are part of JavaScript syntax, not local scope. For this reason, the code lives in `oxc_minifier`. It only reuses the short-name generator from `oxc_mangler`.

For one program, the order is:

```text
collect names → assign names → rewrite once → compress → mangle variables and private names
```

Property names are rewritten before compression because compression can remove information about quotes and comments.

For several programs, a bundler collects names from each program, merges the results, assigns one shared map, and rewrites each program once. Collection can run in parallel. All programs must use the same options.

The shared map keeps property names consistent inside one build. The cache has a different job: it keeps fixed names and reservations across separate minify calls.

### Example

With `include: "^_"` and `mangle_quoted: true`:

```js
obj[`_field`] = 1;
obj._field = 2;
```

becomes:

```js
obj[`e`] = 1;
obj.e = 2;
```

### Limitation

- Property mangling is unsafe when other code still uses the old property name. Public APIs and names shared with external code, host APIs, imported namespace objects, or separate builds must be excluded or reserved.
- Quote handling is per use. Without `mangle_quoted`, `obj._field` may change while `obj["_field"]` does not.
- The mangler cannot find names built at runtime or stored in arbitrary strings. Code that uses `eval`, `Function`, or `with` must reserve any affected names.
- Compression may create a matching property after this pass has finished. For example, `'f' + 'oo_'` may later become `foo_` and will not be mangled.
- TypeScript enum reverse mappings are not supported because the transform creates quoted strings.
- Multi-program support is a caller API. The caller must collect every program that shares mangled properties before assigning names.

The public `oxc-minify` API is stacked in #24741. The downstream multi-chunk integration is shown in rolldown/rolldown#10374.

Supersedes #23772.

AI assistance: OpenAI Codex and Claude were used for research, implementation, review, and testing. The contributor is responsible for reviewing and understanding the changes before submission.
2026-08-17 07:50:51 +00:00
leaysgur 44231fb5f7 fix(formatter): fix comment and indent issues around unions (#25665)
Fixes comment-placement bugs, idempotency and also inconsistency around unions/intersections, type aliases, conditional types, and assignment-likes.

- Fix invariant violations where comments crossed `=`, the value, or `;`
  - removed the relocation logic emulating old Prettier 3.8 behavior
- Normalize a union's leading comments to one canonical form behind the synthesized leading `|`, independent of the source shape
- Comments around a formatter-added `(` keep their source side of the `(`
- Hug conditional-type branch comments behind `?`/`:` with the content one level under the operator, matching conditional expressions
- Extend `= // c` position preservation from type aliases to all of JS
2026-08-16 15:04:28 +00:00
Boshen 892238149b refactor(linter): remove inactive react config rule (#25740)
Remove the inactive `react/config` rule and its generated metadata. Oxlint uses fixed, valid React Compiler options, so the rule cannot produce actionable diagnostics.

AI-assisted: Codex was used to implement and validate this change.
2026-08-16 12:39:27 +00:00
Boshen 8c4552dfa6 feat(linter)!: split react/react-compiler into per-category rules (#25500)
Splits the experimental `react/react-compiler` rule into 24 category-specific rules matching `eslint-plugin-react-hooks` v7.

- Runs React Compiler once per file and routes structured diagnostics to each enabled rule.
- Removes `react/react-compiler` and `reportAllBailouts`, with rules categorized by user-facing lint intent.
- Adds per-rule documentation and deduplicates shared React lint URLs.
- Preserves standardized compiler diagnostics while reporting each finding under its Oxlint rule name.

Breaking change: configurations using `react/react-compiler` must migrate to the category-specific rules.

Written with Claude Code and Codex.
2026-08-15 14:56:58 +00:00
Boshen fca2e0cfed fix(parser): reject initialized lexical declarations in for-in (#25700)
Rejects initialized `let` and `const` declarations in `for...in` during parsing while preserving Annex B's non-strict `var` form.

Adds misc coverage for valid and invalid initializer forms.

Verified with `just ready` and parser conformance.

Closes #25149.

AI-assisted.
2026-08-15 06:58:08 +00:00
Boshen ae9be8f8bd fix(parser): forbid type parameters on quoted constructors (#25696)
Quoted constructor methods bypassed the AST-based constructor validation. Validate type parameters after resolving the method key, preserving the AST shape.

Validation: `just ready`, parser conformance, and allocation checks.

Closes #25692.

AI-assisted with Codex.
2026-08-15 05:02:23 +00:00
camc314 beb724dce2 fix(linter/eslint/no-unused-vars): report bare underscore parameters (#25663)
## Summary

- report a bare underscore parameter when no args ignore pattern is configured
- preserve the optimized underscore-prefix behavior for explicit argsIgnorePattern configuration
- update focused rule coverage and generated documentation

Closes #25626
2026-08-14 13:16:16 +00:00
camc314 44d1be348c fix(linter/typescript/no-var-requires): run rule on JavaScript (#25664)
## Summary
- run typescript/no-var-requires on JavaScript files
- match typescript-eslint behavior and prevent false unused-disable diagnostics

Fixes #25623.
2026-08-14 13:06:00 +00:00
camc314 625401da43 fix(linter/import/no-named-default): flag type-only named defaults (#25661)
## Summary

- flag declaration-level TypeScript type-only named default imports
- preserve inline type specifier behavior to match eslint-plugin-import
- add regression and disable-directive coverage
- regenerate rule dispatch and timing metadata

Closes #25613
2026-08-14 11:22:14 +00:00
leaysgur c07fe7c217 feat(oxfmt): support experimentalOperatorPosition (#25643)
Fixes #16366

---

Option name requires `experimental` prefix for now.

Values are:

- `"start"`
- `"end"`(default)

This is also available on playground.
2026-08-14 08:12:16 +00:00
leaysgur 2adb8fb329 feat(formatter): implement operatorPosition option (#25581)
NOTE: For now, behavior is the same as Prettier.

- Default is `end`
- This works on intersection(`&`) type, but has no effect on union(`|`) type

For future refs: https://github.com/oxc-project/oxc/issues/16367
2026-08-14 08:12:16 +00:00
camc314 4922141d0a feat(mangler): deduplicate private accessor names (#25601)
## Summary

Fix private-name mangling for `get #x()` / `set #x(value)` accessor pairs.

JavaScript treats an accessor pair as one private name, but the mangler previously assigned a candidate per element before collecting into a name-keyed map. The setter overwrote the getter’s mapping, while still consuming an extra candidate.

### Before

```js
class Outer {
  get #x() {}
  set #x(value) {}
  #longName;
  nested() {
    return class {
      #nestedName;
    };
  }
}
```

```js
class Outer {
  get #t() {}
  set #t(e) {}
  #n;
  nested() {
    return class {
      #r;
    };
  }
}
```

### After

```js
class Outer {
  get #e() {}
  set #e(e) {}
  #t;
  nested() {
    return class {
      #n;
    };
  }
}
```

## Implementation

- Deduplicate private names during map construction with `HashMap::entry`.
- Derive nested-class offsets from already-built lexical parent mappings, preserving collision avoidance.
- Pre-size mappings from the number of private elements.
2026-08-14 00:44:46 +00:00
camc314 0c68b7ff8b fix(estree): emit decorators on FormalParameterRest (#25582)
This PR adds support for emitting `decorators` when serializing an ast to ESTree (both with/without raw transfer).

Given the following code:
```
class C { method(@dec ...args) {} }
```

Before we emitted the following ESTree shape:
```json
"params": [
  {
    "type": "RestElement",
    "decorators": [],
    "argument": {
      "type": "Identifier",
      "decorators": [],
      "name": "args",
      "optional": false,
      "typeAnnotation": null,
    },
    "optional": false,
    "typeAnnotation": null,
    "value": null,
  }
],
```

Now we emit:
```json
"params": [
  {
    "type": "RestElement",
    "decorators": [
      {
        "type": "Decorator",
        "expression": { "type": "Identifier", "name": "dec" }
      }
    ],
    "argument": {
      "type": "Identifier",
      "decorators": [],
      "name": "args",
      "optional": false,
      "typeAnnotation": null,
    },
    "optional": false,
    "typeAnnotation": null,
    "value": null,
  },
],
```

fixes https://github.com/oxc-project/oxc/issues/18981
closes https://github.com/oxc-project/oxc/pull/24736
2026-08-13 10:43:14 +00:00
camc314 6e040e494e fix(coverage): ignore unable to resolve parameter decorator TS1239 (#25583) 2026-08-13 10:30:36 +00:00
Boshen cdf1846af2 fix(semantic): allow legacy escapes in JSX attributes (#25576)
Quoted JSX attribute values do not use ECMAScript escape sequences. Skip strict-mode legacy escape validation for JSX attributes and cover the behavior in misc conformance.

AI-assisted by Codex.
2026-08-13 07:06:42 +00:00
sapphi-red 771d79a9e3 fix(mangler): exclude non-manglable symbols from slot assignment (#25539)
Slots were assigned to non-manglable symbols as well. This made the slot re-use non-optimal.

This PR fixes that.

I used AI to find out this bug and to generate the code and then reviewed the code.
2026-08-13 06:39:16 +00:00
leaysgur fed6681eda docs(oxfmt): skip expanding overrides options (#25572)
Follow up on #25544.

Oxlint also omits `overrides[].rules`.
2026-08-13 02:35:53 +00:00
Rahul Mishra 6a07fa6c79 fix(docs): cap generated Markdown heading depth (#25544)
`#` was repeated for each level without a max, which created this
problem
https://oxc.rs/docs/guide/usage/formatter/config-file-reference.html
<img width="1452" height="1154" alt="Screenshot 2026-08-12 at 18-24-43"
src="https://github.com/user-attachments/assets/9f1ac473-2e7b-4514-81a4-d528c7b6c0e6"
/>

capped `#` at max of 6
2026-08-13 11:23:56 +09:00
camc314 cd25e4f56e fix(codegen): surface conformance reference panics (#25552)
## Summary

Make Codegen conformance failures visible when the Rust reference panics.

- Preserve `null` for source text with parser diagnostics.
- Return a N-API error for a caught reference panic.
- Let Vitest fail the affected fixture instead of silently skipping it.
2026-08-12 17:09:00 +00:00
camc314 46d237c9a7 chore(codegen,parser,tasks): pin benchmark fixtures by sha (#25557)
Pin the Codegen benchmark fixture to an immutable `benchmark-files` commit. This makes benchmark inputs reproducible and prevents `main` updates from silently changing results.
2026-08-12 15:56:52 +00:00
Boshen 23a7ad096a fix(parser): stop delimited lists at end of file (#25542)
Stop parsing delimited-list elements after a separator when EOF or an undetermined token follows, preventing malformed tuple syntax from creating reversed spans during error recovery.

Validated with `cargo coverage -- parser --filter oxc-23670 --detail`.

AI assistance was used to prepare this change.

Closes #23670.
2026-08-12 13:58:51 +00:00
overlookmotel a4478e960a feat(codegen): add oxc-codegen package (#25488)
Add `oxc-codegen` package - a printer written in TS.

### Features

- Prints JS, JSX, TS, TSX.
- Matches `oxc_codegen` exactly on all Test262, TypeScript, Acorn-JSX fixtures.
- Very fast.

### Deficiencies

- Pretty-printing only (no compact/minified output).
- No support for printing comments.
- Source map support is semi-there, but largely untested, and poor API (design doc has some ideas on how to improve it).

### This PR adds

- The printer code.
- Conformance testing infra.
- Further tests for edge cases.
- README.
- Design doc - how it differs from `oxc_codegen` and why, what produces the perf.

### This PR doesn't add

- Not integrated into CI or release pipeline.

### Notes

This started as a slop port of `oxc_codegen`. I've iterated on that base, cleaned it up a lot, and at least skimmed through all of it. I think it's in fairly decent shape.

The design doc is I think worth a read. It explains the rationale for the choices, in particular the "1 codebase, 4 builds" approach.

I've put it in a new top-level directory called `packages`. It's not an app, so didn't seem to me to fit alongside `apps/oxlint` and `apps/oxfmt`. It's pure JS, so not like e.g. `napi/parser`. And it felt too heavyweight to put into `npm` dir. But maybe there's a better place for it.

Co-authored-by: Cameron <cameron.clark@hey.com>
2026-08-12 13:41:31 +00:00
Boshen 73acba93fb fix(parser): preserve fatal errors during await reparse (#25541)
Fixes #23681.

Preserve initial fatal parser errors during top-level `await` reparsing, preventing malformed recovery AST nodes from reaching Oxlint.

Validated with parser unit/conformance suites and allocation snapshots. `just ready` only fails three unrelated ANSI snapshot checks in the current environment.

AI-assisted.
2026-08-12 11:25:27 +00:00
camc314 65cda29177 fix(transform_conformance): record missing expected errors (#25306)
Fix the transformer conformance runner incorrectly marking a fixture as passed that specified `throws` when Oxc produces no error.

Failed conformance cases now always record a diagnostic. This exposes previously hidden failures in the Babel snapshot, including the nested ambient-module fixtures.
2026-08-12 10:30:24 +00:00
leaysgur 0fa91283fd refactor(formatter,formatter_tests): move out prettier JS,TS,JSDoc conformance from tasks/ (#25477)
Migration from tasks/prettier_conformance to each crate test using oxc_formatter_tests, for JS/TS and JSDoc.
2026-08-12 00:48:32 +00:00
leaysgur e4437275de refactor(formatter_css,formatter_json,formatter_graphql,formatter_tests): move out prettier CSS,JSON,GraphQL conformance from tasks/ (#25472)
Migration from tasks/prettier_conformance to each crate test using oxc_formatter_tests, for CSS, JSON, and GraphQL.
2026-08-12 00:48:32 +00:00
leaysgur 72c00f32ea refactor(formatter_yaml,formatter_tests): move out prettier YAML conformance from tasks/ (#25469)
Migration from `tasks/prettier_conformance` to each crate test using `oxc_formatter_tests`, for YAML.
2026-08-12 00:48:31 +00:00
leaysgur 6ef3984410 refactor(ci): self-provision prettier suite in oxc_formatter_tests (#25467)
Remove the Prettier related stuff from the conformance, which is purely aimed at JS/TS.

Instead, `oxc_formatter_tests` manage Prettier's test fixtures along with Oxfmt.
2026-08-12 00:40:59 +00:00
oxc-guard 7ebbdddce7 chore(submodules): update submodule SHAs to latest commits (#25505)
Updates submodule dependencies to their latest commits.

## Changes

- test262: `b363f29` → `be13516`
- babel: `06b6eae` → `1eac448`
- estree-conformance: `f7a4e0c` → `8d4e1fa`

This PR is automatically generated by the [update_submodules workflow](https://github.com/oxc-project/oxc/blob/main/.github/workflows/update_submodules.yml).
2026-08-11 16:17:31 +00:00
Boshen 8e9b95f3b6 perf(linter): use table for rule names (#25458)
Replace the generated `RuleEnum::name` match with an ID-indexed static table. This reduced the macOS arm64 release binary by 16,512 bytes (0.118%).

AI assistance was used to analyze binary size and prepare this change. The author reviewed the generated code and measurements.

Co-authored-by: Cameron Clark <cameron.clark@hey.com>
2026-08-10 10:33:55 +00:00
Dunqing 66cfe0a49a chore(coverage): update stale test262 runtime snapshot (#25471)
The automated submodule bump #25261 regenerated `runtime.snap` on a loaded CI runner where two tests hit the global timeout, recording transient `timeout: global` failures (`expressions/arrow-function/name.js`, `function-code/10.4.3-1-13-s.js`) and dropping the pass count from 17306 to 17304. Both tests pass on a normal run, so monitor-oxc's Test262 drift check has failed on every run since ([example](https://github.com/oxc-project/monitor-oxc/actions/runs/31359248351/job/93364683855)).

This restores the snapshot to what `cargo coverage runtime` actually produces. Rather than regenerating locally (which could bake in this machine's own timeouts), the CI-reported diff was applied directly and verified byte-identical to CI's output (`git hash-object` matches the diff's `65e3fa9` blob).

Timeouts can get baked in again whenever an automated regen lands on a slow runner; retrying timed-out tests before recording them would close that hole and is left for a follow-up.

This PR was assisted by Claude.
2026-08-10 10:16:19 +00:00
Boshen 8f784f35d4 perf(linter): reduce rule config dispatch size (#25461)
Generate `from_configuration` match arms only for the 341 rules that override the default implementation. The other 508 rules clone their default value from `RULES`.

This reduces the release oxlint binary from 13,963,312 to 13,946,800 bytes (-16,512 bytes).

AI-assisted.
2026-08-10 08:23:40 +00:00
Boshen 030857b509 refactor(diagnostics): update oxc-miette to v4 (#25395)
Remove the miette `Report` wrapper and update oxc-miette to v4's narrowed diagnostic and source protocol.

AI-assisted by OpenAI Codex.
2026-08-10 07:48:02 +00:00
leaysgur 00f490d7f7 refactor(oxfmt,formatter): split sortImports validation and use type enum (#25432)
Reduce runtime validation.

Also, by using type `enum`, IDE and TS types are also covered.
2026-08-09 15:07:41 +00:00
leaysgur 1a2c64a356 fix(formatter,oxfmt): apply effective print width for JSDoc fence (#25413)
As JSDoc is always contained within the ` * ` fenced comments, the `printWidth` option should not have been used as is.

The dedicated `jsdocPrintWidth` option has not yet been implemented. #24633
2026-08-09 14:49:14 +00:00
leaysgur 465b723d04 refactor(formatter,oxfmt): unify host services as SessionServices on FormatSession (#25388)
- dispatcher
- string_embedder
- tailwind_sorter

Group these 3 together as `SessionServices`, make them available to `FormatSession`, and pass them around.
2026-08-09 14:32:10 +00:00
leaysgur fd02a8956e feat(oxfmt): dispatch yaml-in-css(frontmatter) to oxc_formatter_yaml (#25336)
Using the `Dispatcher` set up so far, detect the YAML front matter and process it using `oxc_formatter_yaml`.
2026-08-09 14:15:05 +00:00
camc314 9d97da14c8 chore(minifier): update minifier allocs snapshot (#25439) 2026-08-09 13:38:14 +00:00
leaysgur b3ad060cfd refactor(formatter): migrate embedded entries to FormatSession (#25332)
Apply `FormatSession` everywhere introduced in #25329.
2026-08-09 13:06:35 +00:00
leaysgur 281124b43b refactor(formatter_core): introduce FormatSession with shared GroupId space (#25329)
In theory, the group id needs to function across embedded boundaries.

To achieve this, a "session" mechanism that can be carried over those boundaries was required.
2026-08-09 12:33:40 +00:00
Armano e0fb2d782a fix(minifier): avoid unsafe return removal in labeled blocks (#25406)
revert #24813 currently there is no way to validate if stmt can be
removed if its inside a block

correct issue with incorrect removal of `return` stmt not at termination
points


```js
(function() {
  label: {
    console.log('before return');
    return;
  }
  console.log('unreachable!');
})();
```

ref: https://github.com/rolldown/rolldown/issues/10555
2026-08-09 12:59:54 +01:00
Cameron 12937b3201 fix(parser): reject comma expressions in class fields (#25385)
## Summary

- parse ordinary class field initializers as `AssignmentExpression`
- reject unparenthesized comma expressions while preserving
parenthesized sequence expressions
- add paired misc conformance fixtures and update the generated
snapshots

## Root cause

Ordinary class field initializers used `parse_expr`, which accepts a
top-level comma and constructs a sequence expression. The ECMAScript
`Initializer` grammar requires an `AssignmentExpression`, so the
unparenthesized comma must remain for the class-element terminator check
to reject.
2026-08-08 10:59:23 +00:00
camc314 9e93ba6908 fix(parser): reset auto-accessor initializer context (#25384)
## Summary

- Align auto-accessor initializer parsing with class-field initializer grammar.
- Add regressions for `in` in a `for` initializer and disallowed inherited `await`/`yield` contexts.

## Root cause

The auto-accessor path only enabled `new.target`. It therefore inherited outer `~In`, `Await`, and `Yield` grammar contexts: `a in b` could not parse in a `for` initializer, while `await` and `yield` could incorrectly parse from enclosing async/generator functions. The fix applies the same `In | NewTarget` addition and `Yield | Await` removal already used for class fields, while retaining AssignmentExpression parsing.

## References

- [ECMAScript `FieldDefinition` grammar](https://tc39.es/ecma262/#prod-FieldDefinition)
- [ECMAScript `Initializer` grammar](https://tc39.es/ecma262/#prod-Initializer)
- [TC39 decorators proposal: class auto-accessors](https://github.com/tc39/proposal-decorators#introduction)
2026-08-07 23:03:24 +00:00
camc314 e4208164e1 perf(parser): optimize trailing comma tracking (#25376)
## Summary

- Track trailing-comma spans only for array and object literals ending in a spread.
- Consume the stored span while covering a rest assignment target.

## Rationale

Avoids parser-state map work for ordinary trailing commas while retaining constant-time diagnostics for invalid rest-target trailing commas.
2026-08-07 21:30:28 +00:00
camc314 a33788e13e feat(ast)!: group class heritage into ClassHeritage (#25360)
## Summary

- Groups `Class::super_class` and `Class::super_type_arguments` into `Class::heritage`.
- Introduces `ClassHeritage` containing the superclass expression and its optional TypeScript type arguments.
- Makes superclass type arguments without a superclass expression unrepresentable.

## Breaking Change

The Rust AST changes from:

```rust
pub struct Class<'a> {
    pub node_id: Cell<NodeId>,
    pub span: Span,
    pub r#type: ClassType,
    pub decorators: Vec<'a, Decorator<'a>>,
    pub id: Option<BindingIdentifier<'a>>,
    #[ts]
    pub type_parameters: Option<Box<'a, TSTypeParameterDeclaration<'a>>>,
    pub super_class: Option<Expression<'a>>,
    #[ts]
    pub super_type_arguments: Option<Box<'a, TSTypeParameterInstantiation<'a>>>,
    #[ts]
    pub implements: Vec<'a, TSClassImplements<'a>>,
    pub body: Box<'a, ClassBody<'a>>,
    #[ts]
    pub r#abstract: bool,
    #[ts]
    pub declare: bool,
    pub scope_id: Cell<Option<ScopeId>>,
}
```

to:

```rust
pub struct Class<'a> {
    pub node_id: Cell<NodeId>,
    pub span: Span,
    pub r#type: ClassType,
    pub decorators: Vec<'a, Decorator<'a>>,
    pub id: Option<BindingIdentifier<'a>>,
    #[ts]
    pub type_parameters: Option<Box<'a, TSTypeParameterDeclaration<'a>>>,
    pub heritage: Option<ClassHeritage<'a>>,
    #[ts]
    pub implements: Vec<'a, TSClassImplements<'a>>,
    pub body: Box<'a, ClassBody<'a>>,
    #[ts]
    pub r#abstract: bool,
    #[ts]
    pub declare: bool,
    pub scope_id: Cell<Option<ScopeId>>,
}

pub struct ClassHeritage<'a> {
    pub expression: Expression<'a>,
    #[ts]
    pub type_arguments: Option<Box<'a, TSTypeParameterInstantiation<'a>>>,
}
```

## Why?

ECMAScript defines the complete `extends` clause as `ClassHeritage`:

```text
ClassHeritage : extends LeftHandSideExpression
```

TypeScript additionally permits type arguments on the heritage expression:

```ts
class Derived extends Base<T> {}
```

Previously, the AST stored the expression and type arguments independently:

```text
Class {
    super_class: Some(Base),
    super_type_arguments: Some(<T>),
    ...
}
```

This allowed an invalid state to be constructed:

```text
Class {
    super_class: None,
    super_type_arguments: Some(<T>),
    ...
}
```

Superclass type arguments cannot exist without an `extends` expression.

The parser already treated these fields as a pair. It parsed a class heritage entry and then split its expression and type arguments into two independent fields. Builders and transforms therefore had to preserve their relationship manually.

Grouping them into `ClassHeritage` makes the syntactic dependency explicit and prevents consumers from observing or constructing contradictory class heritage.

## Class heritage forms

A class without an `extends` clause has no heritage:

```js
class Base {}
```

```text
Class {
    heritage: None,
    ...
}
```

A class extending an expression has heritage without type arguments:

```js
class Derived extends Base {}
```

```text
Class {
    heritage: Some(ClassHeritage {
        expression: Base,
        type_arguments: None,
    }),
    ...
}
```

TypeScript type arguments belong to the same heritage structure:

```ts
class Derived extends Base<T> {}
```

```text
Class {
    heritage: Some(ClassHeritage {
        expression: Base,
        type_arguments: Some(<T>),
    }),
    ...
}
```

The heritage expression may be any valid class heritage expression:

```js
class Derived extends mixin(Base) {}
class NullDerived extends null {}
```

## How to migrate

Code that previously accessed the superclass expression directly:

```rust
if let Some(super_class) = &class.super_class {
    // ...
}
```

can use the compatibility accessor:

```rust
if let Some(super_class) = class.super_class() {
    // ...
}
```

Code that needs both parts should access the grouped heritage:

```rust
if let Some(heritage) = &class.heritage {
    let expression = &heritage.expression;
    let type_arguments = heritage.type_arguments.as_deref();
}
```

Mutating consumers should update the nested fields:

```rust
if let Some(heritage) = &mut class.heritage {
    heritage.type_arguments = None;
}
```

Common migrations:

| Previous representation | New representation |
| --- | --- |
| `class.super_class` | `class.heritage.as_ref().map(\|h\| &h.expression)` or `class.super_class()` |
| `class.super_type_arguments` | `class.heritage.as_ref()?.type_arguments` or `class.super_type_arguments()` |
| `class.super_class.is_some()` | `class.heritage.is_some()` |
| Mutate `class.super_class` | Mutate `class.heritage.expression` |
| Clear `class.super_type_arguments` | Clear `class.heritage.type_arguments` |
| Pass superclass and type arguments separately to a `Class` builder | Construct and pass `ClassHeritage` |
| Clone both fields independently | Clone `class.heritage` |

```ts
superClass: Expression | null;
superTypeArguments?: TSTypeParameterInstantiation | null;
```

No `heritage` property or `ClassHeritage` node is added to the ESTree output.

fixes https://github.com/oxc-project/backlog/issues/216
2026-08-07 14:18:59 +00:00
camc314 5c5cdcd522 feat(ast)!: narrow TSInterfaceHeritage::expression to TSTypeName (#24360)
## Summary

Narrow `TSInterfaceHeritage` from a general JavaScript `Expression` to `TSTypeName`.

```rust
pub struct TSInterfaceHeritage<'a> {
    pub type_name: TSTypeName<'a>,
    pub type_arguments: Option<Box<'a, TSTypeParameterInstantiation<'a>>>,
}
```

## Breaking Change

The Rust AST field changes from:

```rust
expression: Expression<'a>
```

to:

```rust
type_name: TSTypeName<'a>
```

## Why?

The current AST can represent shapes such as:

```ts
interface A extends foo() {}
interface A extends A + B {}
interface A extends new Foo() {}
interface A extends true {}
```

Which is an overly wide type since all of the above variants are invalid.

This PR tightens up the AST, making these invalid variants irrepresentable.

## How to migrate?

This should be a trivial migration:
1. instead of accessing `expression` on `TSInterfaceHeritage`, now access `type_name`
2. change any pattern matching on the `type_name` field to use `TypeName` (this is a smaller subset so should allow deleting code!)

closes https://github.com/oxc-project/backlog/issues/215
2026-08-07 06:00:42 +00:00
Mikhail Baev ccb8fe89db feat(linter/jsdoc): implement no-blank-blocks rule (#25207)
this PR implements `jsdoc/no-blank-blocks` rule, issue #1170
2026-08-05 22:38:24 +01:00
camc314 44fd320324 feat(ast)!: split TS external modules & Namespace Declarations (#25284)
## Summary

- Splits `TSModuleDeclaration` into two dedicated declaration nodes:
  - `TSExternalModuleDeclaration` for string-literal modules and module augmentations.
  - `TSNamespaceDeclaration` for identifier-based `module` and `namespace` declarations.
- Removes `TSModuleDeclaration`, `TSModuleDeclarationName`, `TSModuleDeclarationBody`, and `TSModuleDeclarationKind`.
- Makes invalid combinations of names, bodies, and declaration kinds unrepresentable.
- Ensures only namespace declarations introduce a normal binding.
- Updates all AST consumers, generators, traversal, formatting, code generation, semantic analysis, lint rules, transforms, and serialization.
- Keeps normal and eager-raw ESTree output compatible: both declarations remain `TSModuleDeclaration` nodes.
- Allows experimental lazy raw deserialization to expose the two new node types directly.

## Breaking Change

The Rust AST changes from:

```rust
pub enum Declaration<'a> {
    VariableDeclaration(Box<'a, VariableDeclaration<'a>>) = 32,
    FunctionDeclaration(Box<'a, Function<'a>>) = 33,
    ClassDeclaration(Box<'a, Class<'a>>) = 34,

    TSTypeAliasDeclaration(Box<'a, TSTypeAliasDeclaration<'a>>) = 35,
    TSInterfaceDeclaration(Box<'a, TSInterfaceDeclaration<'a>>) = 36,
    TSEnumDeclaration(Box<'a, TSEnumDeclaration<'a>>) = 37,
    TSModuleDeclaration(Box<'a, TSModuleDeclaration<'a>>) = 38,
    TSGlobalDeclaration(Box<'a, TSGlobalDeclaration<'a>>) = 39,
    TSImportEqualsDeclaration(Box<'a, TSImportEqualsDeclaration<'a>>) = 40,
}

pub struct TSModuleDeclaration<'a> {
    pub node_id: Cell<NodeId>,
    pub span: Span,
    pub id: TSModuleDeclarationName<'a>,
    pub body: Option<TSModuleDeclarationBody<'a>>,
    pub kind: TSModuleDeclarationKind,
    pub declare: bool,
    pub scope_id: Cell<Option<ScopeId>>,
}
```

to:

```rust
pub enum Declaration<'a> {
    VariableDeclaration(Box<'a, VariableDeclaration<'a>>) = 32,
    FunctionDeclaration(Box<'a, Function<'a>>) = 33,
    ClassDeclaration(Box<'a, Class<'a>>) = 34,

    TSTypeAliasDeclaration(Box<'a, TSTypeAliasDeclaration<'a>>) = 35,
    TSInterfaceDeclaration(Box<'a, TSInterfaceDeclaration<'a>>) = 36,
    TSEnumDeclaration(Box<'a, TSEnumDeclaration<'a>>) = 37,
    TSExternalModuleDeclaration(Box<'a, TSExternalModuleDeclaration<'a>>) = 38,
    TSNamespaceDeclaration(Box<'a, TSNamespaceDeclaration<'a>>) = 39,
    TSGlobalDeclaration(Box<'a, TSGlobalDeclaration<'a>>) = 40,
    TSImportEqualsDeclaration(Box<'a, TSImportEqualsDeclaration<'a>>) = 41,
}

pub struct TSExternalModuleDeclaration<'a> {
    pub node_id: Cell<NodeId>,
    pub span: Span,
    pub id: StringLiteral<'a>,
    pub body: Option<Box<'a, TSModuleBlock<'a>>>,
    pub declare: bool,
    pub scope_id: Cell<Option<ScopeId>>,
}

pub struct TSNamespaceDeclaration<'a> {
    pub node_id: Cell<NodeId>,
    pub span: Span,
    pub id: BindingIdentifier<'a>,
    pub body: TSNamespaceDeclarationBody<'a>,
    pub kind: TSNamespaceDeclarationKind,
    pub declare: bool,
    pub scope_id: Cell<Option<ScopeId>>,
}

pub enum TSNamespaceDeclarationBody<'a> {
    TSNamespaceDeclaration(Box<'a, TSNamespaceDeclaration<'a>>) = 0,
    TSModuleBlock(Box<'a, TSModuleBlock<'a>>) = 1,
}

pub enum TSNamespaceDeclarationKind {
    Module = 0,
    Namespace = 1,
}
```

The corresponding inherited `Statement` variants, `AstKind` variants, builders, and visitor methods have also changed.

## Why?

`TSModuleDeclaration` previously represented two declaration families with different syntax and semantics:

```ts
declare module "foo" {}
declare module "*.css";

module Foo {}
namespace Foo {}
namespace Foo.Bar {}
```

The shared representation allowed invalid states such as:

- A namespace with a string-literal name.
- An external module with an identifier name.
- An identifier namespace without a body.
- A dotted external module.
- An external module containing another module declaration as its body.
- A declaration whose `kind` disagreed with its name.

Consumers also had to inspect `id` before determining whether the node introduced a binding or represented an external module.

The two declaration families have stronger independent invariants:

### External modules

- Always have a string-literal name.
- Never introduce a normal binding identifier.
- May omit their body.
- Cannot use dotted names.
- Use module-body import and export rules.

### Namespace declarations

- Always have a binding identifier.
- Always have a body.
- May contain nested namespace declarations for dotted names.
- Explicitly preserve the `module` or `namespace` spelling.
- Introduce a namespace/module symbol.
- Use namespace-body restrictions.

Dedicated nodes encode these differences directly.

## Declaration forms

External modules use `TSExternalModuleDeclaration`:

```ts
declare module "foo" {}
declare module "*.css";
module "foo" {}
```

Identifier-based declarations use `TSNamespaceDeclaration`:

```ts
module Foo {}
namespace Foo {}
```

Dotted namespaces are represented as nested namespace declarations:

```ts
namespace Foo.Bar {}
```

```rust
TSNamespaceDeclaration {
    id: Foo,
    body: TSNamespaceDeclarationBody::TSNamespaceDeclaration(
        TSNamespaceDeclaration {
            id: Bar,
            body: TSNamespaceDeclarationBody::TSModuleBlock(...),
            // ...
        }
    ),
    // ...
}
```

Only `TSExternalModuleDeclaration::body` is optional.

## ESTree compatibility

Normal serialization and eager raw deserialization continue to emit the existing ESTree shape:

```ts
interface TSModuleDeclaration {
  type: "TSModuleDeclaration";
  id: BindingIdentifier | StringLiteral | TSQualifiedName;
  body: TSModuleBlock | null;
  kind: "module" | "namespace";
  declare: boolean;
  global: false;
}
```

Dotted namespaces continue to serialize as a single `TSModuleDeclaration` with a `TSQualifiedName`.

Experimental lazy raw deserialization is not compatibility-preserving. It now exposes `TSExternalModuleDeclaration` and `TSNamespaceDeclaration` directly, with separate constructors, layouts, and visitor type IDs.

## How to migrate

Code that previously matched all forms through one variant:

```rust
if let Declaration::TSModuleDeclaration(declaration) = declaration {
    // ...
}
```

should now match the required declaration family:

```rust
match declaration {
    Declaration::TSExternalModuleDeclaration(declaration) => {
        // String-literal external module
    }
    Declaration::TSNamespaceDeclaration(declaration) => {
        // Identifier-based module or namespace
    }
    _ => {}
}
```

The same applies to `Statement` and `AstKind`:

```rust
Statement::TSExternalModuleDeclaration(declaration)
Statement::TSNamespaceDeclaration(declaration)

AstKind::TSExternalModuleDeclaration(declaration)
AstKind::TSNamespaceDeclaration(declaration)
```

Common field migrations:

| Previous representation | New representation |
| --- | --- |
| `TSModuleDeclarationName::StringLiteral(id)` | `TSExternalModuleDeclaration::id` |
| `TSModuleDeclarationName::Identifier(id)` | `TSNamespaceDeclaration::id` |
| `body: None` | Only valid on `TSExternalModuleDeclaration` |
| `TSModuleDeclarationBody::TSModuleBlock` | External `body` or `TSNamespaceDeclarationBody::TSModuleBlock` |
| `TSModuleDeclarationBody::TSModuleDeclaration` | `TSNamespaceDeclarationBody::TSNamespaceDeclaration` |
| `TSModuleDeclarationKind::Module` | External module, or `TSNamespaceDeclarationKind::Module` |
| `TSModuleDeclarationKind::Namespace` | `TSNamespaceDeclarationKind::Namespace` |
| Inspect `id` to determine binding behavior | Match `TSNamespaceDeclaration` |
| `declaration.id()` for string modules | Now returns `None` |
| `visit_ts_module_declaration` | `visit_ts_external_module_declaration` or `visit_ts_namespace_declaration` |

When constructing nodes, use the generated `TSExternalModuleDeclaration` or `TSNamespaceDeclaration` builders rather than recreating the previous shared representation.

fixes https://github.com/oxc-project/backlog/issues/218
2026-08-05 14:37:35 +00:00
Cole Ellison d4a897ce22 feat(linter/eslint): implement one-var rule (#24470)
## Summary

Adds a native Oxlint implementation of [ESLint's `one-var`
rule](https://eslint.org/docs/latest/rules/one-var). This implementation
is a port of the [eslint rule
source](https://github.com/eslint/eslint/blob/main/lib/rules/one-var.js),
test suite, and autofix logic.

Related #479

## AI Disclosure

GPT 5.6 Sol was used to assist with implementation of this rule.

---------

Co-authored-by: Cameron <cameron.clark@hey.com>
2026-08-05 15:10:29 +01:00