## Summary
- Introduces `ArrowFunctionBody` to represent concise expression and block bodies explicitly.
- Removes the `ArrowFunctionExpression::expression` boolean.
- Makes invalid arrow body combinations unrepresentable.
## Breaking Change
The Rust AST changes from:
```rust
pub struct ArrowFunctionExpression<'a> {
pub expression: bool,
pub body: Box<'a, FunctionBody<'a>>,
}
```
to:
```rust
pub enum ArrowFunctionBody<'a> {
FunctionBody(Box<'a, FunctionBody<'a>>) = 64,
INHERIT(Expression<'a>),
}
pub struct ArrowFunctionExpression<'a> {
pub body: ArrowFunctionBody<'a>,
}
```
Concise arrow bodies are now stored directly as expressions instead of synthetic `FunctionBody` and `ExpressionStatement` nodes. Block bodies continue to use `FunctionBody`.
## Why?
The previous representation required consumers to maintain this implicit invariant:
```text
expression == true
→ no directives
→ exactly one statement
→ that statement is an ExpressionStatement
```
Invalid combinations could cause panics, skipped work, or fallback behavior. The enum encodes the grammar directly and makes those invalid states unrepresentable.
Concise expressions also now have `ArrowFunctionExpression` as their direct traversal ancestor, without synthetic `FunctionBody` or `ExpressionStatement` nodes.
## How to migrate
Use the arrow body accessors or match on `ArrowFunctionBody` rather than inspecting `expression` and synthetic statements.
Common field migrations:
| Previous representation | New representation |
| --- | --- |
| `arrow.expression` | `arrow.is_expression()` |
| `arrow.get_expression()` | `arrow.get_expression()` |
| `&arrow.body.statements` | `arrow.get_function_body().map(\|body\| &body.statements)` |
| `&mut arrow.body.statements` | `arrow.get_function_body_mut().map(\|body\| &mut body.statements)` |
| Synthetic expression statement in `arrow.body` | Direct `ArrowFunctionBody` expression variant |
| `arrow.body = function_body` | `arrow.body = ArrowFunctionBody::FunctionBody(function_body)` |
| Expression-to-block conversion | Create a `FunctionBody` containing a return statement |
| Block-to-expression conversion | Assign the expression directly to `arrow.body` |
| Builder `(expression, body)` arguments | Pass a single `ArrowFunctionBody` |
closes https://github.com/oxc-project/backlog/issues/211
In GitHub Actions the annotation parameters (`file=`, `line=`, `col=`)
only feed the annotations panel, so the log line itself has no location:
```
Error: Function 'foo' is declared but never used.
```
A teammate hit this in my repo's CI and could not tell which file broke
the lint job. My workaround was re-running oxlint with
`--format=default` on failure, same as csvn in #21574.
This prefixes the message with the location, same layout as [Ruff's
GitHub
renderer](https://github.com/astral-sh/ruff/blob/main/crates/ruff_db/src/diagnostic/render/github.rs):
```
::error file=test.js,line=5,endLine=5,col=1,endColumn=10,title=eslint(no-debugger)::test.js:5:1: `debugger` statement is not allowed
```
The location does get duplicated in the annotation bubble, which is
already anchored to the line. Ruff accepted that trade for a readable
log. The filename is escaped twice on purpose, property rules for
`file=` and data rules for the message text, with a unit test covering
the difference.
Related to #21574 but this does not implement multi-format output.
Updated the unit tests, the CLI snapshots, and the github e2e fixture.
`cargo test -p oxlint` passes (396) and so does the apps/oxlint vitest
suite.
I used Claude Code to help investigate and draft this. I reviewed the
change and ran the tests myself.
This PR refactors the "ignore fixes" code, to be created on diagnostic creation.
With an extra flag in `LintOptions` and `LintRunner`, the caller can include automatically the fix for ignoring the diagnostic.
This flag is only enabled on `oxlint --lsp` side and is not effected by normal CLI usage.
Important changes:
- The "fixes" are working now with `Spans` instead of `Position`
- The calculation of the right span is not depending on the partial source text, instead of the full source text
- new `FixKind` for the `ignore` fixes.
Generated by GPT-5.3-Codex 🤖 Reviewed by myself
The `-W, --warn` help text says "Deny the rule or category (emit a
warning)", copied from the `-D, --deny` entry.
With that wording, `--warn` and `--deny` look like they do the same
thing in `--help`, but `-W` reports a warning and `-D` reports an error.
Evidence why I think this is not intentional, and instead a typo:
- The other two entries start with their own flag's verb ("Allow the
rule...", "Deny the rule..."). Only the `-W` entry starts with its
sibling's verb, in a sentence identical to the `-D` one except the
parenthetical.
- These flags mirror rustc and clippy's `-A`/`-W`/`-D`, where warn and
deny are two different levels and deny specifically means "make it an
error".
- In this same file, `-W` maps to `AllowWarnDeny::Warn` and `-D` maps to
`AllowWarnDeny::Deny` (`into_tuple`). The internal name for what `-W`
does is warn, not deny.
What changed:
- The `-W` help text is now "Warn on the rule or category (emit a
warning)"
- Updated the two website_linter snapshots, so the generated CLI docs
page picks up the fix
I used Claude Code to help find and fix this.
Co-authored-by: Cameron <cameron.clark@hey.com>
The start for fixing https://github.com/oxc-project/oxc/issues/24570
At the moment the "ignore this line" / "ignore this section" logic is living inside `oxlint/lsp` directory.
Because we now need to know the AST parents, the code should live inside `oxc_linter`.
This PR refactors the 2 methods into a new `oxc_linter` file.
Part of #16608, closes#25046.
---
Even with a simple regexp, I don't want to impact the performance of the vast majority of ts-in-vue paths.
With this approach, only tsx-in-vue use cases will be affected, and even that will be kept to a minimum since it can be handled entirely on the Rust side. (JS <-> Rust roundtrip is also slow)
Audited every formatter crate against the trailing-comment width discipline.
- same-line trailing line comments ride `line_suffix` and are excluded from the fits measurement
- while trailing block comments print inline and count
Corrected the prose that misstated it.
Fixes#24843
---
Tracing back to the root cause, this is also related to what had diverged because of Prettier's inconsistent behavior.
Since I cannot determine for sure how it should be output in more complex cases (like that issue) at this point, decided to revert to being Prettier-compatible.
Also fixes#24927
- rework comment printing to follow the rules and align with other formatters
- bump `oxc-graphql-parser`
- to track comments inside a node's span more accurately
The generated config reference only described the JSON schema's `string[]` form. Clarify that `oxlint.config.ts` accepts imported `OxlintConfig[]`, add an example and public JSDoc, and regenerate the derived artifacts.
Closes#20238.
Validated with the website generator suite, targeted TypeScript checks, and the relevant Oxlint tests. The full `just ready` run reached an unrelated parallel suppression-fixture cleanup race; the failing test passes in isolation.
AI-assisted.
Part of #22955. Follow-up to the resolved#23617.
## What
`eslint/no-restricted-globals` stayed in `NO_VERIFIED_VALID_SCHEMA`
after #23617 was resolved, because the derived schema described the
internal `NoRestrictedGlobalsConfig` struct (a `globals` hash map)
rather than the user-facing positional-array format the rule actually
parses.
This PR adds a hand-written `JsonSchema` following the existing
`no-restricted-imports` pattern, describing the three accepted
positional item forms:
1. `"event"` — a global name string
2. `{ "name": "event", "message": "…" }` — a name/message object
3. `{ "globals": [ … ], "checkGlobalObject": true, "globalObjects": [ …
] }` — the object form added in #23617
Generated TS declaration:
```ts
"no-restricted-globals"?:
RuleNoConfig | [AllowWarnDeny, NoRestrictedGlobalsConfigEnum, ...NoRestrictedGlobalsConfigEnum[]];
export type NoRestrictedGlobalsConfigEnum = string | RestrictedGlobal | NoRestrictedGlobalsObjectConfig;
```
Verified against the #22955 checklist: reflects all accepted user
configuration forms, denies unknown fields on object forms, and produces
no `[k: string]: unknown` in the TS declaration. `from_configuration`
and rule behavior are unchanged; rule tests pass.
Note: this branch removes only the `no-restricted-globals` entry from
`NO_VERIFIED_VALID_SCHEMA`; #24586 (separate PR) removes the
`filename-case` entry, so whichever merges second will have a trivial
conflict in that array.
## Disclosure
This contribution was developed with assistance from Claude Code
(Anthropic); I have reviewed and tested all changes.
---------
Signed-off-by: Alexander S. <sysix@sysix-coding.de>
Co-authored-by: Alexander S. <sysix@sysix-coding.de>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
## Summary
- Unskip `jest/prefer-lowercase-title` and
`vitest/prefer-lowercase-title` from `NO_VERIFIED_VALID_SCHEMA`.
- PreferLowercaseTitleConfig already derives `Deserialize` +
`JsonSchema` with `deny_unknown_fields` and docs for all public options,
so the generated schema/types can describe the real config.
- Regenerates `npm/oxlint/configuration_schema.json` and
`apps/oxlint/src-js/package/config.generated.ts`.
## Motivation
Part of #22955. These two plugin names shared a clean config struct
already used at runtime; they were still emit-as-`DummyRule` only
because of the broader #23186 split note (mainly about `valid-title`
option divergence). Leaving `valid-title` skipped.
Generated config now exposes:
- `allowedPrefixes: string[]` (default `[]`)
- `ignore: string[]` (default `[]`)
- `ignoreTopLevelDescribe: boolean` (default `false`)
- `lowercaseFirstCharacterOnly: boolean` (default `true`)
- `additionalProperties: false` / no `[k: string]: unknown`
## Verification
- `cargo test -p website_linter` — 8 passed
- `cargo test -p oxc_linter --features ruledocs
test_rules_with_custom_configuration_have_schema` — passed
- `cargo test -p oxc_linter prefer_lowercase_title --lib` — 2 passed
(jest + vitest)
- Inspected generated schema/TS: not DummyRule; no index signature
## AI disclosure
Assisted by AI (Hermes/Sera). I reviewed the existing
PreferLowercaseTitleConfig against the GFI checklist, regenerated
schema/types locally, and ran the tests above.
---
**Agent-Owner:** `sera` · **Platform:** hermes · **Claim:** `sera`
---
<!-- fleet-pr-claim: agent_id=sera platform=hermes -->
**Agent-Owner:** `sera` · **Platform:** hermes · **Claim-TTL:** 24h
---------
Co-authored-by: Bartok9 <259807879+Bartok9@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
## Summary
Explicit CLI paths (e.g. `oxlint some/relative/dir`) were only
absolutized as a side effect of the ignore-file pre-filtering step,
which `--no-ignore` skips entirely. That let relative paths flow through
to `Walk`, and from there to every downstream consumer - including the
type-aware tsgolint integration, which invokes typescript-go's
`ComputeConfigFileName` expecting absolute paths and panics ("vfs: path
... is not absolute") on a relative one.
This is the companion fix requested in
[oxc-project/tsgolint#1088](https://github.com/oxc-project/tsgolint/pull/1088),
where a maintainer asked for the absolute-path invariant to be enforced
on the oxc side rather than defensively inside tsgolint itself.
## Repro
`oxlint --no-ignore -c <config with options.typeAware=true>
node_modules/some-pkg` (a relative path) panicked with the vfs error
above. The same command with an absolute path worked fine, confirming
the gap was specific to `--no-ignore` skipping absolutization.
## Fix
Absolutizes CLI paths unconditionally in a new branch that mirrors the
existing ignore-block's absolutization logic, but only runs for
`--no-ignore` (the existing branch already absolutizes as part of its
own filtering - left untouched here to avoid changing its
ignore-matching behavior). Both paths share a small `absolutize`
closure, which also skips the redundant extra allocation
`std::path::absolute` would otherwise do on top of `cwd.join()` when the
path is already absolute (a common case for CI/automation scripts that
already pass absolute paths).
## Testing
- Repro'd against a real `node_modules` package (`lucide-react`) with
`--no-ignore` + type-aware enabled + a relative path: panicked before
this fix, 0 panics after (including a genuine type-aware finding,
confirming tsconfig resolution now succeeds end-to-end).
- Confirmed the standard case (absolute path, no `--no-ignore`) is
unaffected.
- `cargo test -p oxlint --lib`: one pre-existing failure,
`lint::suppression::test_only_non_fixed_diagnostics_are_reported`
(`STATUS_STACK_BUFFER_OVERRUN`) - confirmed via revert-and-rebuild that
it crashes identically without this change, so it's unrelated (appears
specific to this GNU-toolchain-on-Windows build environment, not
something CI building the MSVC target would hit).
## Disclosure
Found and fixed with Claude's (Anthropic) assistance, following up on
[oxc-project/tsgolint#1088](https://github.com/oxc-project/tsgolint/pull/1088)
- reviewed and verified locally (repro before/after, test suite) before
submitting.
---------
Signed-off-by: Cameron <cameron.clark@hey.com>
Co-authored-by: Cameron <cameron.clark@hey.com>
Introduce `exact_line_breaks(count)` IR builder.
This produces a line element that prints exactly `count` line breaks, exempt from the printer's hardline collapsing / empty-line capping.
Blank runs inside verbatim values (YAML block/flow scalars, GraphQL block strings) are the value itself and must not be normalized.
Previously this was emulated by emitting raw `\n` text followed by a re-arming `hard_line_break()`..., hack.
Also removed the printer behavior that trims end of line whitespaces.
Pure refactor, except for fixing a TS type, and correcting some comments. Runtime behavior is unaltered.
- `LABEL_PATTERN` captures the directive type as well as the label, so the multi-line check is a
single comparison, and `type` no longer comes from `label.slice(7)`.
- Numbered capture groups instead of named ones, so the match check is just `match === null`.
- `match` is no longer reused for a second, unrelated regex.
- `justification` is always set, so it's not optional - correct the type def accordingly.
- Simplify splitting off `justification`.
- Doc comment referenced `match_disable_directive` / `match_enable_directive`. The Rust function is
`match_directive` - correct the comment.
`LINE_DIRECTIVE_PATTERN` had a stray hyphen (`disable-(?:-next)?-line`), so it only matched
`disable--line` / `disable--next-line` - strings `LABEL_PATTERN` can never produce. Therefore it was dead code - remove it.
If the regex _was_ correctly written, the behavior would be wrong. Looks like this is a hangover from the code being taken from ESLint.
Add more debug assertions to debug build-only code which checks the validity of comments and tokens.
Make sure all comments and tokens have spans which are wihin bounds of source text. In particular, this will surface any problems where the source text contains a BOM, which causes us to adjust spans on Rust side when trimming off the BOM.
Tiny perf optimization to linter plugins.
`if (x === false)` is longer than `if (!x)`, but is more performant. The 1st compiles down to a simple equality check between `<value x>` and `kFalse` constant, whereas the 2nd is more like `coerceToBoolean(<value x>) === kFalse` - a more expensive operation.
This follows our convention in all JS plugins code of always using explicit `=== false` / `=== true` comparisons.
Pure refactor. Use debug asserts instead of type assertions in code for deserializing comments and tokns. This gives better error messages if we break the invariants in tests. Makes no difference in release build.
Small optimization to `context.sourceCode.getDisableDirectives`.
`Shebang` comment can only be the first comment in a file, so there's no need to check "is it a `Shebang`?" for every comment in a loop. Instead, just check the first comment before the loop.
Also add type annotation for the function's return type.
## Summary
- contextually type oxlint and oxfmt `defineConfig` object arguments as
their config interfaces
- keep the identity-helper return type so literal inference is preserved
- restore field-level JSDoc hover for config object properties in
editors
## Validation
Manual expansion of `just ready` because `just` is not installed in my
local shell:
- `pnpm install`
- `typos`
- `cargo lintgen`
- `cargo shear --fix --check-test-targets` (returned 0; emitted an
unrelated existing redundant-ignore warning for `oxc_lexer` in workspace
metadata)
- `cargo fmt`
- `node --run fmt`
- `cargo ck`
- `env -u NO_COLOR cargo test --all-features`
- `cargo lint -- --deny warnings`
- `RUSTDOCFLAGS='-D warnings' cargo doc --no-deps
--document-private-items --all-features`
- `cargo run -p oxc_ast_tools`
TypeScript language-service probe using VS Code's bundled TypeScript
SDK:
- old `defineConfig<T extends Config>(config: T): T` returned no docs
for object fields
- new `defineConfig<T extends Config>(config: T & Config): T` returned
JSDoc for `oxlint.ignorePatterns` and `oxfmt.semi`
- invalid property/type probes still reported TS2561 and TS2322
- return literal inference stayed unchanged for representative
oxlint/oxfmt config objects
## AI Assistance
This PR was prepared with AI assistance. I reviewed the change,
validation approach, and PR contents before submission.
Co-authored-by: cixiangtao <cixiangtao@kanyun.com>
Co-authored-by: Cameron <cameron.clark@hey.com>
Follow-on after #24557. Handle when `ImportMeta` / `NewTarget` has an empty span (`SPAN`). This code path is not exercised at present, but it seems a good idea to be prepared for when we may serialize AST after transformation.
Closes the migration tracked in #23043.
The new builder API (methods on the AST types themselves) has been in place for a while, all Oxc crates are migrated, and every crate which uses `AstBuilder` already enables `disable_old_builder` in its tests. The module docs said "after a few weeks, we will remove `AstBuilder`'s own methods entirely" — this does that.
## What's removed
- `crates/oxc_ast/src/generated/ast_builder.rs` and `builder/methods.rs` — `AstBuilder`'s own node and primitive constructors.
- `AstBuilderGenerator`, which produced the former.
- The `disable_old_builder` Cargo feature, plus the dev-dependency hacks it required across 33 manifests. Eight crates depended on `oxc_ast` *purely* to turn the feature on; those deps and their `cargo-shear` ignore entries are gone too.
## One thing worth reviewing
`AstBuilderGenerator` owned the `#[builder(default)]` / `#[builder(skip)]` attr registration that `BuilderMethodsGenerator` only *reads* — its doc comment said as much. Deleting it naively would have silently dropped `skip`/`default` handling for the 12 AST types using those attrs, so `attrs()` and `parse_attr()` move to `BuilderMethodsGenerator`.
The evidence this was faithful: after regenerating, `builder_methods.rs` and every other generated file are **byte-identical**. The only generated change in the diff is the deletion.
## Breaking changes
- `AstBuilder`'s own methods are gone. Use the AST type methods: `Statement::new_expression_statement(span, expr, &builder)` instead of `builder.statement_expression(span, expr)`.
- `AstBuilder` is no longer `Copy`/`Clone`, and its `allocator` field is private — use the `allocator` method from `GetAllocator`.
- `AstBuilder` and `NONE` are no longer re-exported from the crate root; import them from `oxc_ast::builder`. (The `disable_old_builder` feature already gated these off, so nothing in-tree used the root path.)
- The `disable_old_builder` feature no longer exists — remove it from any manifest that sets it.
A migration note covering all of the above is in `crates/oxc_ast/src/builder/mod.rs`.
---
Implemented with AI assistance (Claude Code); design, review, and verification driven by the author per the repo AI-usage policy.
Fixes#24612
- Update `fast-glob` to `1.1.0` which exposes `validate()` api
- Use it in `oxc_config`, apps will be validated through `deserialize`
- Oxfmt supports globs for CLI paths, also validated
After many trial and errors, this seem to be the best way to remove
break stmts in switch cases,
This change targets only last break in last case of switch smts if its
unlabelled
this change differs from #23914 as its no longer targets all branches as
that should not be no longer necessary if all cases are correctly
rewritten
https://github.com/oxc-project/oxc/issues/24672
-----
cases like `switch (a) { case 3: if (b) break; }` are not changed
-----
ref: #17544
Some clients can send code actions for documents, which are not opened and already analyzed with a lint process.
A good example is the IntelliJ plugin, which allows the "fix all" code action from the file explorer.
> This PR refactors the language-server → tool code-action API to pass a single request struct (including an “is document open” flag), enabling the oxlint LSP linter to avoid re-running lint for code-action requests when the document is already open (and presumably already linted/cached).
This is mostly needed for https://github.com/oxc-project/oxc/pull/23768
This implements
[`no-top-level-await`](https://github.com/eslint-community/eslint-plugin-n/blob/master/docs/rules/no-top-level-await.md)
from the Node ESLint plugin (#493).
AI Disclosure: Generated with Claude Code. Reviewed and tested by me.
I have an alternative implementation using a Program visitor but it made
no difference performance-wise on vscode or in benchmarking outside
extreme cases, so I don't think it's really worth the trouble vs the
simpler implementation here. It's still in the commit history if that
implementation is preferred.
There is one notable difference between this and the original
implementation, which is that the `convertPath` option is not
implemented, and the checking of `package.json`/`.npmignore` is not
implemented (it was used for checking if the given module was actually
exposed in the library) as there's no way to check against those in
oxlint. There's an argument to be made that this makes the rule not
worth implementing, and I'm open to that argument if we'd prefer to mark
this rule as unsupported instead.
All tests were ported from the original rule (excluding those reliant on
the mentioned config options/checks that were skipped). I've also had
Claude add a few additional test cases (see commit
40418f7e63) on top to cover behavior that
wasn't tested previously. I have confirmed that all of these tests are
identical to the behavior of the upstream rule, but I can remove them if
we don't want to have any divergence from the upstream test suite.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`OxcDiagnostic` is an unboxed ~300-byte struct constructed at thousands of callsites. Boxing its inner data behind an 8-byte handle means every callsite, every `Result<T, OxcDiagnostic>`, and every move copies 8 bytes instead of ~300, so the compiler emits far less inlined construction/copy code.
## Binary size (stripped, real release builds, aarch64-apple-darwin)
| Binary | Before | After | Delta |
|---|---:|---:|---:|
| `oxlint` | 14,177,616 B | 13,665,680 B | **−499.9 KiB (−3.61%)** |
| napi transform (`.node`) | 3,482,624 B | 3,433,040 B | **−48.4 KiB (−1.42%)** |
Most of the win lands in `oxc_linter`, whose ~1,500 diagnostic-construction sites inline into rule bodies; `oxc_react_compiler` also contributes.
The `error`/`warn` constructors delegate to an outlined `#[inline(never)] fn new`, so the `Box` allocation + field initialization exist once in the binary rather than inlined at every construction site.
This reverses #22406, which unboxed the inner to save a heap allocation. That is now safe: the parser no longer constructs diagnostics on its hot path (#24663).
Supersedes the draft #24656, which additionally carried a now-redundant copy of the parser deferral that has since landed via #24663.