Commit Graph

38 Commits

Author SHA1 Message Date
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 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
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
camc314 cc49951c5b feat(linter_codegen): ignore local types in node type detection (#24984) 2026-07-27 13:28:23 +00:00
renovate[bot] 75ce81de95 chore(deps): update rust crate syn to v3 (#24684)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [syn](https://redirect.github.com/dtolnay/syn) |
workspace.dependencies | major | `2` → `3` |

---

### Release Notes

<details>
<summary>dtolnay/syn (syn)</summary>

###
[`v3.0.0`](https://redirect.github.com/dtolnay/syn/releases/tag/3.0.0)

[Compare
Source](https://redirect.github.com/dtolnay/syn/compare/2.0.119...3.0.0)

This release contains adjustments to the syntax tree to account for
ongoing Rust language development from the 3 years since syn 2.0.0 and
to anticipate some in-flight Rust language RFCs.

These include: default values in fields, pinned type sugar, raw
lifetimes, generator blocks and functions, unnamed enum variants,
attributes in tuple types and tuple patterns, named arguments in
parenthesized generic argument lists, lightweight clones, const traits,
const function pointers, mutability restricted fields, supertrait auto
implementation, final associated functions, trait implementability
restrictions, const blocks in path arguments, item-level const blocks,
return type notation, never patterns, function delegation, mutable
by-reference bindings, in-place initialization, field projections,
explicitly dyn-compatible traits, view types, file-level frontmatter,
generic const arguments, guard patterns, lazy type aliases, explicitly
safe foreign items, super let, unsafe fields, pattern types,
heterogeneous try-blocks, function contracts, async function trait
bounds, static closure coroutine syntax, unsafe binder types, move
expressions, for-await loops, and postfix keywords.

<br>

<p align="center"><a href="https://docs.rs/syn/3/syn">[API documentation
for 3.0]</a></p>

### Breaking changes
#### Modifiers

To reserve more room for language evolution, there are 10 new
non-exhaustive structs in the syntax tree having the following
commonality:

- Name ending in `Modifiers`. {`BlockModifiers`, `ClosureModifiers`,
`ConstModifiers`, `FieldModifiers`, `FnModifiers`, `ImplModifiers`,
`LocalModifiers`, `TraitBoundModifiers`, `TraitModifiers`,
`TypeModifiers`}

- Each implements `Default`. The default value is guaranteed to comprise
no tokens.

- Non-exhaustive. Can only be instantiated by Syn's parser or by
creating and then mutating `▁▁Modifiers::default()`.

- Does not implement `Parse`. When parsing, they are parsed by the
enclosing syntax tree node.

- Does not implement `ToTokens`. In some cases the syntax that these
nodes might hold in the future is not necessarily contiguous tokens.

- Provides `.require_empty() -> Result<()>` which returns a meaningfully
spanned error if the modifiers are different from the empty default.
This enables a caller to reject syntax it does not recognize without
knowing what that syntax may be.

#### Types

- `Type::BareFn` has been renamed to `Type::FnPtr` to mirror the
compiler's terminology. Together with this, `BareVariadic` is renamed to
`FnPtrVariadic`.

- The mutually exclusive `const_token` and `mutability` fields of
`Type::Ptr` have been unified into an enum of type `PointerMutability`,
which was already previously used by `Expr::RawAddr`.

- Every `Type` variant now holds attributes, which can represent the
attributes of element types inside a tuple type, or attributes for a
function return type.

- `BareFnArg` is renamed to `NamedArg` and is used in
`ParenthesizedGenericArguments`, in addition to the existing use in
`Type::FnPtr`.

#### Expressions

- In `Expr::Closure`, the fields `or1_token` and `or2_token` have been
renamed to `inputs_begin` and `inputs_end` to indicate the beginning and
ending `|` token of the closure inputs.

#### Statements

- Attributes are now preserved on all expression kinds in statement
position. Previously, parsing would silently lose attributes on some
expression statements without a dedicated Expr variant, such as
tail-call expressions.

#### Patterns

- The `guard` field of `Arm` is replaced by a new `Pat::Guard` variant
held in the arm's `pat`.

#### Items

- The `unsafety` field of `Signature`, which represented the presence or
absence of `unsafe`, is replaced by a 3-way `Safety` enum which may be
safe, unsafe, or default. `ForeignItem::Static` also gets a `Safety`.

- Some of the fields of `Receiver` have been split to a non-exhaustive
`ReceiverKind` enum to create room for proposed new kinds of method
receivers, such as pinned.

- Type aliases now hold a `WhereClausePlacement` to distinguish between
early placement (which is the default for item-level type aliases and is
*deprecated* in associated type aliases) and late placement (which is
the default for associated type aliases and is *unstable* in item-level
type aliases). Parsing and printing a syntax tree will now preserve the
where-clause placement rather than rewriting it to the default placement
for the item kind.

#### Generics

- `WherePredicate::Lifetime` and `WherePredicate::Type` have a new field
holding the attributes on the where-predicates.

- `GenericParam::Type` and `GenericParam::Const` now match the rest of
the syntax tree in holding their optional default using a single Option
of tuple, rather than a pair of Option.

#### Literals

- The `Parse` implementation of `Lifetime` no longer permits keyword
lifetimes, matching a change in Rust 1.81 to deny such lifetimes
pre-expansion. `Lifetime::parse_any` may be used to parse lifetimes
permissive of keywords.

- `LitInt` and `LitFloat` no longer implement
`From<proc_macro2::Literal>`. Use `Lit::new(literal)` and then match
`Lit::Int` or `Lit::Float`.

- The `StrStyle` enum was unused and has been deleted.

#### More

- `File` contains a new `Option<Frontmatter>`, but does not yet parse
frontmatter in `syn::parse_file` until that language feature progresses
closer to stabilization.

- Some enums no longer provide `From` impls. Construct the variant by
name instead, such as `Expr::Array(e)` in place of `e.into()`.

- The `pop` method of `Punctuated<T, P>` now returns `Option<T>`
discarding the trailing punctuation if any, rather than `Option<Pair<T,
P>>`. A new method `pop_pair` provides the old behavior.

- Syntax tree traversal (`visit`, `visit_mut`, `fold`) no longer
provides a method for processing `Span`. This was already incompletely
supported and never walked most of the spans in a syntax tree.

- The `Speculative` and `AnyDelimiter` traits have been sealed and
cannot be implemented for types outside of Syn's `ParseStream`.

</details>

---

### Configuration

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

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

🚦 **Automerge**: Enabled.

♻ **Rebasing**: Whenever PR is behind base branch, 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:eyJjcmVhdGVkSW5WZXIiOiI0My4yNjUuMSIsInVwZGF0ZWRJblZlciI6IjQzLjI2NS4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Cameron Clark <cameron.clark@hey.com>
2026-07-19 20:57:12 +01:00
camchenry fd79ef9ecc refactor(linter_codegen): allow 'to_configuration' as safe function (#24085)
The `to_configuration` method didn't exist when we first added the linter codegen, but it should have been added later. This allows for more optimization opportunities for the AST node type optimization.
2026-07-02 17:43:38 +00:00
Boshen 8e0dd658ce perf(linter): emit RuleEnum dispatch match once instead of per timing branch (#23499)
## What

`RuleEnum::run` / `run_once` / `run_on_jest_node` are generated with a
`const TIMINGS: bool` parameter. Each body inlined the full **~834-arm
`match self { … }` twice** — once inside the `timing_stat.time(|| …)`
closure (the `--debug timings` path) and once in the plain `else`
branch:

```rust
pub(crate) fn run<'a, const TIMINGS: bool>(&self, node, ctx, timing_stat) {
    if TIMINGS {
        timing_stat.expect("…").time(|| match self { /* 834 arms */ });
    } else {
        match self { /* 834 arms again */ }
    }
}
```

Because `TIMINGS` is *also* a const generic, each dispatcher is
monomorphized for both `true` and `false`, so the giant dispatch table
was emitted up to **four times** per function across the three
dispatchers.

## Change

Hoist the `match` into a single non-generic `#[inline(never)]`
`*_dispatch` helper per dispatcher and call it from both branches, so
the table is emitted **once** and shared across both `TIMINGS`
monomorphizations. Generated from
`tasks/linter_codegen/src/rules_enum.rs` (the regenerated
`crates/oxc_linter/src/generated/rules_enum.rs` is included).

## Performance

`--debug timings` is a cold diagnostic path. The hot (no-timing) path
now does **one direct call** into the shared dispatcher instead of an
inlined match — the jump table itself is unchanged, so per-node/per-rule
throughput is unaffected. The win is binary size (the dispatch tables
are large; this removes the duplicated copies). Worth confirming with
`cargo bloat --release --crates` on the release artifact.

## Verification

- `cargo test -p oxc_linter` — 1168 rule tests pass.
- `cargo clippy -p oxc_linter` — clean.
- `oxlint` normal run and `oxlint --debug timings` both produce correct
output, with the per-rule timing breakdown intact.

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

Co-authored-by: Cameron Clark <cameron.clark@hey.com>
2026-06-18 15:45:03 +01:00
Connor Shea d09ccc9a7b chore(linter): Add info and short_description fields to declare_oxc_lint! macro. (#22511)
Simplified redo of #22295. Calling this a chore because I don't think it
warrants mention in the changelog for now as this is just basic
scaffolding changes, but we can mark it as a feature if preferred.

AI Disclosure: generated with Claude Code, reviewed by me.

The `short_description` field will be a one-sentence summary of the rule
that can be used in documentation, and especially the rules list on the
website. The idea would be to enable the rules list to include the short
description as a subtitle under each rule, similar to this:

<img width="1120" height="618" alt="image"
src="https://github.com/user-attachments/assets/ff9e4d63-b61f-474e-9455-d3567c96d51c"
/>

The rule macros will now look like this:

```rs
declare_oxc_lint!(
    /// Longer rule description here.
    MyRule,
    eslint,
    nursery,
    pending,
    config = MyRule,
    version = "next",
    info {
        short_description = "FIXME: One-sentence description of the rule.",
    },
);
```

We could do `info = {` here instead of `info {`, up to the reviewers.

I'll move `version` into the `info` field in a subsequent PR. I didn't
do it here because I wanted to keep this simple and avoid problems of
merge conflicts with the release tomorrow. The `info` field was
suggested by camchenry to avoid continuing to bloat the size of the rule
meta struct / rules enum file.

In a subsequent PR I'll add the short_description to most rules using
the same script as I used in #22295. Again, just keeping this simple /
avoiding merge conflicts.

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Cameron Clark <cameron.clark@hey.com>
2026-06-10 14:20:12 +01:00
camchenry b46d4dee60 feat(linter): add --debug options and add per-rule timing info (#22282)
- part of https://github.com/oxc-project/oxc/issues/22138

This PR implements two separate but related things:

- A new `--debug` option for the `oxlint` CLI that accepts a comma separated list of flags like `--debug foo,bar,baz`. This allows adding more debug functionality in the future without needing to add lots of individual CLI options.
- A `--debug timings` option that enables per-rule timing information to be recorded and output. This enables determining what the slowest rules are in a single run of the linter in the CLI.

### Rule timings

The primary goal of this altered approach to timing linter rules was: **when timings are not enabled, it doesn't cost any performance.**. To achieve that, we've added a `TIMINGS` const generic argument to all of the core `run`, `run_on_jest_node`, `run_once`, etc. functions in the linter. This creates more code overall, because instead of having a single version of each function, now we have two: one where the compiler optimized it with `TIMINGS = false` and one where `TIMINGS = true`. We then switch between these two paths at runtime by passing `--debug timings`.

So, was this successful? In short, yes. There is no significant difference between `oxlint` in `main` and this PR when it comes to performance, unless you enable per-rule timings (see https://github.com/oxc-project/oxc/pull/22282#issuecomment-4421061041). When you enable timing information, there is a 20-30% overhead in performance. This is because we have to record lots of time information and bookkeeping in hash maps to output the final information. This could be optimized down the line possibly, but it's not a primary goal.

This is also kept fairly generic so that in future PRs we can add support for JS plugin timings and `tsgolint` rule timings as well, if those are available.

Overall, I think the increased maintenance burden is worth it. Even looking at the example output below, I can already see ways to improve specific lint rules for improved performance. It will be invaluable for users to be able to record timing information and submit it to us without requiring them to pull up a profiler and submit a flamegraph profile. This doesn't entirely replace a profiler, but it allows us to get useful information without needing to recompile a binary or install additional tools.

### Example output

Example output from `--debug timings` on the vscode repository (using dev mode, not release build, in case this seems really slow):

```
Found 7790 warnings and 19 errors.
Finished in 16.6s on 6911 files with 95 rules using 8 threads.

Rule timings:
Rule                                                      Time (ms)  Relative    Calls  Source
-------------------------------------------------------  ----------  --------  -------  ------
eslint/no-unused-vars                                      4119.700     22.5%     6681  native
eslint/no-unreachable                                      4116.910     22.5%     6900  native
eslint/no-loss-of-precision                                1329.986      7.3%   168327  native
oxc/bad-array-method-on-arguments                           849.118      4.6%  9749545  native
eslint/no-useless-escape                                    633.403      3.5%   469909  native
eslint/no-nonoctal-decimal-escape                           602.055      3.3%   447866  native
eslint/no-this-before-super                                 467.429      2.6%     3912  native
typescript/no-unsafe-declaration-merging                    429.882      2.4%    20334  native
eslint/no-obj-calls                                         423.232      2.3%   602268  native
eslint/no-useless-backreference                             319.706      1.7%   605544  native
eslint/no-dupe-keys                                         284.629      1.6%   109038  native
eslint/no-unsafe-optional-chaining                          246.120      1.3%  2036313  native
eslint/no-control-regex                                     216.737      1.2%   605544  native
eslint/no-misleading-character-class                        212.035      1.2%   605544  native
eslint/constructor-super                                    172.978      0.9%    11088  native
eslint/no-caller                                            171.227      0.9%   946489  native
eslint/no-const-assign                                      161.911      0.9%   167786  native
unicorn/no-thenable                                         160.821      0.9%   851478  native
eslint/no-iterator                                          155.057      0.8%   975576  native
eslint/no-shadow-restricted-names                           146.540      0.8%     6900  native
eslint/no-dupe-class-members                                135.977      0.7%     4144  native
unicorn/prefer-set-size                                     135.732      0.7%   946489  native
typescript/no-extra-non-null-assertion                      134.364      0.7%  1523512  native
oxc/bad-replace-all-arg                                     123.456      0.7%   551933  native
eslint/no-eval                                              117.968      0.6%   774258  native
oxc/only-used-in-recursion                                  108.291      0.6%   150511  native
unicorn/no-await-in-promise-methods                         103.633      0.6%   551933  native
oxc/bad-min-max-func                                        102.570      0.6%   551933  native
eslint/no-invalid-regexp                                    101.664      0.6%   602268  native
eslint/no-dupe-else-if                                       97.380      0.5%    84274  native
unicorn/prefer-string-starts-ends-with                       94.667      0.5%   551933  native
unicorn/no-invalid-remove-event-listener                     93.116      0.5%   551933  native
oxc/bad-char-at-comparison                                   91.417      0.5%   551933  native
unicorn/no-single-promise-in-promise-methods                 90.413      0.5%   551933  native
eslint/no-extra-boolean-cast                                 90.250      0.5%   613702  native
oxc/number-arg-out-of-range                                  87.906      0.5%   551933  native
eslint/no-import-assign                                      82.042      0.4%    73720  native
unicorn/no-useless-length-check                              81.488      0.4%    42367  native
unicorn/no-useless-spread                                    78.767      0.4%   178577  native
typescript/no-unnecessary-parameter-property-assignment      76.019      0.4%    62949  native
eslint/use-isnan                                             75.399      0.4%   650300  native
eslint/no-unsafe-finally                                     70.008      0.4%    93520  native
unicorn/no-invalid-fetch-options                             68.221      0.4%   602268  native
oxc/const-comparisons                                        54.044      0.3%   131693  native
typescript/no-wrapper-object-types                           52.855      0.3%   221028  native
eslint/no-constant-binary-expression                         48.784      0.3%   131693  native
eslint/no-irregular-whitespace                               39.979      0.2%   476825  native
eslint/no-unassigned-vars                                    34.773      0.2%   167786  native
eslint/no-unused-expressions                                 34.643      0.2%   329941  native
eslint/no-sparse-arrays                                      31.352      0.2%    69539  native
typescript/no-this-alias                                     29.074      0.2%   233601  native
eslint/no-useless-rename                                     28.954      0.2%   142726  native
eslint/no-constant-condition                                 25.769      0.1%   103602  native
typescript/triple-slash-reference                            22.440      0.1%     6798  native
eslint/no-setter-return                                      22.301      0.1%    83251  native
eslint/no-cond-assign                                        22.198      0.1%   172385  native
eslint/no-compare-neg-zero                                   22.107      0.1%    89326  native
eslint/no-global-assign                                      21.885      0.1%     6900  native
typescript/prefer-as-const                                   21.868      0.1%   204458  native
eslint/no-self-assign                                        18.860      0.1%    68783  native
eslint/require-yield                                         17.908      0.1%    80796  native
eslint/no-unused-private-class-members                       17.573      0.1%     4144  native
eslint/no-func-assign                                        17.390      0.1%    80796  native
typescript/prefer-namespace-keyword                          17.112      0.1%      783  native
typescript/no-misused-new                                    15.415      0.1%    30084  native
eslint/no-async-promise-executor                             15.132      0.1%    50335  native
oxc/missing-throw                                            14.423      0.1%    50335  native
oxc/erasing-op                                               13.472      0.1%    89326  native
oxc/bad-comparison-sequence                                  11.513      0.1%    89326  native
eslint/no-delete-var                                          9.966      0.1%    43952  native
oxc/bad-object-literal-comparison                             9.699      0.1%    89326  native
unicorn/no-new-array                                          9.666      0.1%    50335  native
eslint/no-duplicate-case                                      9.498      0.1%     1520  native
oxc/double-comparisons                                        9.466      0.1%    42367  native
eslint/no-unsafe-negation                                     9.424      0.1%    89326  native
eslint/no-class-assign                                        8.982      0.0%    11088  native
unicorn/no-useless-fallback-in-spread                         8.865      0.0%    42367  native
oxc/uninvoked-array-callback                                  8.857      0.0%    50335  native
eslint/no-ex-assign                                           8.852      0.0%     2729  native
typescript/no-duplicate-enum-values                           8.704      0.0%     1507  native
eslint/valid-typeof                                           8.612      0.0%    43952  native
eslint/no-new-native-nonconstructor                           7.647      0.0%    50335  native
eslint/no-empty-character-class                               5.828      0.0%     3276  native
typescript/no-useless-empty-export                            4.494      0.0%    27137  native
unicorn/no-empty-file                                         4.235      0.0%     6900  native
unicorn/no-unnecessary-await                                  3.885      0.0%    31924  native
eslint/no-unused-labels                                       2.308      0.0%     6900  native
eslint/for-direction                                          1.938      0.0%     2837  native
eslint/no-useless-catch                                       1.788      0.0%     3853  native
eslint/getter-return                                          1.754      0.0%     1019  native
typescript/no-non-null-asserted-optional-chain                1.734      0.0%     7549  native
eslint/no-empty-pattern                                       1.226      0.0%     5829  native
eslint/no-debugger                                            0.074      0.0%        1  native
```
2026-05-14 02:38:23 +00:00
Sysix 8c638c505e refactor(linter): move valid-title rule to shared and crate own rule for vitest (#18467) 2026-04-25 16:23:55 +00:00
camchenry 43adae9e0c docs(linter): add version section to rules page (#21601) 2026-04-21 03:56:55 +00:00
Old Autumn d15dad2b4e docs(linter): export rule version metadata (#21588)
## Summary

This PR covers the Oxc-side export path for rule `version` metadata from
#19890.

It makes `version` available to non-`ruledocs` consumers, then threads
that metadata through the existing export surfaces used by docs/tooling:

- add `version` to `RuleTableRow`
- include `version` in `oxlint` rules JSON output
- include `version` in generated rule doc frontmatter for
`website_linter`
- add regression coverage for the JSON and doc-page export paths

This replaces the earlier stacked fork PR now that its two prerequisites
have already landed:

- #21391: metadata backfill + guardrails
- #21392: release automation

## Notes

- This PR is intentionally scoped to the `oxc` repository only.
- The website rendering change should remain a follow-up in
`oxc-project/website`.
- Compared with the earlier stacked branch, this version is rebased onto
current `main` and aligns with the now-merged compile-time `version`
invariant.

## Testing

```bash
cargo fmt --all
cargo lintgen
cargo test -p oxlint output_formatter::json::test::all_rules_json_includes_version_metadata -- --nocapture
cargo test -p oxlint lint::test::test_rules_json_output -- --nocapture
cargo test -p website_linter
cargo test -p oxc_linter --test rule_configuration_documentation_test --features ruledocs
cargo test -p oxc_linter --features ruledocs
```

## AI Usage Disclosure

I used AI assistance to help inspect the prior stacked PR state, rebase
the export slice onto the current `main`, and prepare the replacement PR
text. I manually reviewed the resulting changes, ran the checks listed
above, and remain responsible for the final code and submission.
2026-04-20 23:04:05 -04:00
camchenry c1eeae3e12 docs(linter): add version to rule.json (#21547)
- part of https://github.com/oxc-project/oxc/issues/19890

Adds the `version` field to be exported when outputting the rules data in JSON format. This is gated behind the `ruledocs` functionality since I haven't heard any requests to make the version information available in the CLI yet, and this will keep the binary size small.
2026-04-20 08:21:09 +00:00
camchenry 81f0039280 fix(linter_codegen): compute rule IDs relative to previous rule (#19350)
The main cause of merge conflicts in new rule PRs lately has been due to the calculation of the rule ID. Currently, adding a new rule causes the ID of every subsequent rule to increment by one, causing a large amount of lines to change.

With these changes, the rule ID is calculated relative to the ID of the previous rule. The actual ID values are calculated elsewhere. This means that adding a new rule doesn't result in a diff, because most IDs are unchanged since the rule order is not changed.

Here is an example `git diff` from running `just new-eslint-rule` with a new rule after these changes:

```diff
diff --git a/crates/oxc_linter/src/generated/rule_runner_impls.rs b/crates/oxc_linter/src/generated/rule_runner_impls.rs
index 3d3da94c59..3143f56b72 100644
--- a/crates/oxc_linter/src/generated/rule_runner_impls.rs
+++ b/crates/oxc_linter/src/generated/rule_runner_impls.rs
@@ -205,6 +205,11 @@ impl RuleRunner for crate::rules::import::unambiguous::Unambiguous {
     const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::RunOnce;
 }

+impl RuleRunner for crate::rules::eslint::consistent_this::ConsistentThis {
+    const NODE_TYPES: Option<&AstTypesBitset> = None;
+    const RUN_FUNCTIONS: RuleRunFunctionsImplemented = RuleRunFunctionsImplemented::Run;
+}
+
 impl RuleRunner for crate::rules::eslint::accessor_pairs::AccessorPairs {
     const NODE_TYPES: Option<&AstTypesBitset> = Some(&AstTypesBitset::from_types(&[
         AstType::CallExpression,
diff --git a/crates/oxc_linter/src/generated/rules_enum.rs b/crates/oxc_linter/src/generated/rules_enum.rs
index 61c5e19c50..b94238878e 100644
--- a/crates/oxc_linter/src/generated/rules_enum.rs
+++ b/crates/oxc_linter/src/generated/rules_enum.rs
@@ -15,6 +15,7 @@ pub use crate::rules::eslint::block_scoped_var::BlockScopedVar as EslintBlockSco
 pub use crate::rules::eslint::capitalized_comments::CapitalizedComments as EslintCapitalizedComments;
 pub use crate::rules::eslint::class_methods_use_this::ClassMethodsUseThis as EslintClassMethodsUseThis;
 pub use crate::rules::eslint::complexity::Complexity as EslintComplexity;
+pub use crate::rules::eslint::consistent_this::ConsistentThis as EslintConsistentThis;
 pub use crate::rules::eslint::constructor_super::ConstructorSuper as EslintConstructorSuper;
 pub use crate::rules::eslint::curly::Curly as EslintCurly;
 pub use crate::rules::eslint::default_case::DefaultCase as EslintDefaultCase;
@@ -720,6 +721,7 @@ pub enum RuleEnum {
     ImportNoWebpackLoaderSyntax(ImportNoWebpackLoaderSyntax),
     ImportPreferDefaultExport(ImportPreferDefaultExport),
     ImportUnambiguous(ImportUnambiguous),
+    EslintConsistentThis(EslintConsistentThis),
     EslintAccessorPairs(EslintAccessorPairs),
     EslintArrayCallbackReturn(EslintArrayCallbackReturn),
     EslintArrowBodyStyle(EslintArrowBodyStyle),
@@ -1396,7 +1398,8 @@ const IMPORT_NO_UNASSIGNED_IMPORT_ID: usize = IMPORT_NO_SELF_IMPORT_ID + 1usize;
 const IMPORT_NO_WEBPACK_LOADER_SYNTAX_ID: usize = IMPORT_NO_UNASSIGNED_IMPORT_ID + 1usize;
 const IMPORT_PREFER_DEFAULT_EXPORT_ID: usize = IMPORT_NO_WEBPACK_LOADER_SYNTAX_ID + 1usize;
 const IMPORT_UNAMBIGUOUS_ID: usize = IMPORT_PREFER_DEFAULT_EXPORT_ID + 1usize;
-const ESLINT_ACCESSOR_PAIRS_ID: usize = IMPORT_UNAMBIGUOUS_ID + 1usize;
+const ESLINT_CONSISTENT_THIS_ID: usize = IMPORT_UNAMBIGUOUS_ID + 1usize;
+const ESLINT_ACCESSOR_PAIRS_ID: usize = ESLINT_CONSISTENT_THIS_ID + 1usize;
 const ESLINT_ARRAY_CALLBACK_RETURN_ID: usize = ESLINT_ACCESSOR_PAIRS_ID + 1usize;
 const ESLINT_ARROW_BODY_STYLE_ID: usize = ESLINT_ARRAY_CALLBACK_RETURN_ID + 1usize;
 const ESLINT_BLOCK_SCOPED_VAR_ID: usize = ESLINT_ARROW_BODY_STYLE_ID + 1usize;
@@ -2151,6 +2154,7 @@ impl RuleEnum {
             Self::ImportNoWebpackLoaderSyntax(_) => IMPORT_NO_WEBPACK_LOADER_SYNTAX_ID,
             Self::ImportPreferDefaultExport(_) => IMPORT_PREFER_DEFAULT_EXPORT_ID,
             Self::ImportUnambiguous(_) => IMPORT_UNAMBIGUOUS_ID,
+            Self::EslintConsistentThis(_) => ESLINT_CONSISTENT_THIS_ID,
             Self::EslintAccessorPairs(_) => ESLINT_ACCESSOR_PAIRS_ID,
             Self::EslintArrayCallbackReturn(_) => ESLINT_ARRAY_CALLBACK_RETURN_ID,
             Self::EslintArrowBodyStyle(_) => ESLINT_ARROW_BODY_STYLE_ID,
@@ -2920,6 +2924,7 @@ impl RuleEnum {
             Self::ImportNoWebpackLoaderSyntax(_) => ImportNoWebpackLoaderSyntax::NAME,
             Self::ImportPreferDefaultExport(_) => ImportPreferDefaultExport::NAME,
             Self::ImportUnambiguous(_) => ImportUnambiguous::NAME,
+            Self::EslintConsistentThis(_) => EslintConsistentThis::NAME,
             Self::EslintAccessorPairs(_) => EslintAccessorPairs::NAME,
             Self::EslintArrayCallbackReturn(_) => EslintArrayCallbackReturn::NAME,
             Self::EslintArrowBodyStyle(_) => EslintArrowBodyStyle::NAME,
@@ -3683,6 +3688,7 @@ impl RuleEnum {
             Self::ImportNoWebpackLoaderSyntax(_) => ImportNoWebpackLoaderSyntax::CATEGORY,
             Self::ImportPreferDefaultExport(_) => ImportPreferDefaultExport::CATEGORY,
             Self::ImportUnambiguous(_) => ImportUnambiguous::CATEGORY,
+            Self::EslintConsistentThis(_) => EslintConsistentThis::CATEGORY,
             Self::EslintAccessorPairs(_) => EslintAccessorPairs::CATEGORY,
             Self::EslintArrayCallbackReturn(_) => EslintArrayCallbackReturn::CATEGORY,
             Self::EslintArrowBodyStyle(_) => EslintArrowBodyStyle::CATEGORY,
@@ -4487,6 +4493,7 @@ impl RuleEnum {
             Self::ImportNoWebpackLoaderSyntax(_) => ImportNoWebpackLoaderSyntax::FIX,
             Self::ImportPreferDefaultExport(_) => ImportPreferDefaultExport::FIX,
             Self::ImportUnambiguous(_) => ImportUnambiguous::FIX,
+            Self::EslintConsistentThis(_) => EslintConsistentThis::FIX,
             Self::EslintAccessorPairs(_) => EslintAccessorPairs::FIX,
             Self::EslintArrayCallbackReturn(_) => EslintArrayCallbackReturn::FIX,
             Self::EslintArrowBodyStyle(_) => EslintArrowBodyStyle::FIX,
@@ -5255,6 +5262,7 @@ impl RuleEnum {
             Self::ImportNoWebpackLoaderSyntax(_) => ImportNoWebpackLoaderSyntax::documentation(),
             Self::ImportPreferDefaultExport(_) => ImportPreferDefaultExport::documentation(),
             Self::ImportUnambiguous(_) => ImportUnambiguous::documentation(),
+            Self::EslintConsistentThis(_) => EslintConsistentThis::documentation(),
             Self::EslintAccessorPairs(_) => EslintAccessorPairs::documentation(),
             Self::EslintArrayCallbackReturn(_) => EslintArrayCallbackReturn::documentation(),
             Self::EslintArrowBodyStyle(_) => EslintArrowBodyStyle::documentation(),
@@ -6248,6 +6256,8 @@ impl RuleEnum {
             }
             Self::ImportUnambiguous(_) => ImportUnambiguous::config_schema(generator)
                 .or_else(|| ImportUnambiguous::schema(generator)),
+            Self::EslintConsistentThis(_) => EslintConsistentThis::config_schema(generator)
+                .or_else(|| EslintConsistentThis::schema(generator)),
             Self::EslintAccessorPairs(_) => EslintAccessorPairs::config_schema(generator)
                 .or_else(|| EslintAccessorPairs::schema(generator)),
             Self::EslintArrayCallbackReturn(_) => {
@@ -8093,6 +8103,7 @@ impl RuleEnum {
             Self::ImportNoWebpackLoaderSyntax(_) => "import",
             Self::ImportPreferDefaultExport(_) => "import",
             Self::ImportUnambiguous(_) => "import",
+            Self::EslintConsistentThis(_) => "eslint",
             Self::EslintAccessorPairs(_) => "eslint",
             Self::EslintArrayCallbackReturn(_) => "eslint",
             Self::EslintArrowBodyStyle(_) => "eslint",
@@ -8831,6 +8842,9 @@ impl RuleEnum {
             Self::ImportUnambiguous(_) => {
                 Ok(Self::ImportUnambiguous(ImportUnambiguous::from_configuration(value)?))
             }
+            Self::EslintConsistentThis(_) => {
+                Ok(Self::EslintConsistentThis(EslintConsistentThis::from_configuration(value)?))
+            }
             Self::EslintAccessorPairs(_) => {
                 Ok(Self::EslintAccessorPairs(EslintAccessorPairs::from_configuration(value)?))
             }
@@ -10918,6 +10932,7 @@ impl RuleEnum {
             Self::ImportNoWebpackLoaderSyntax(rule) => rule.to_configuration(),
             Self::ImportPreferDefaultExport(rule) => rule.to_configuration(),
             Self::ImportUnambiguous(rule) => rule.to_configuration(),
+            Self::EslintConsistentThis(rule) => rule.to_configuration(),
             Self::EslintAccessorPairs(rule) => rule.to_configuration(),
             Self::EslintArrayCallbackReturn(rule) => rule.to_configuration(),
             Self::EslintArrowBodyStyle(rule) => rule.to_configuration(),
@@ -11597,6 +11612,7 @@ impl RuleEnum {
             Self::ImportNoWebpackLoaderSyntax(rule) => rule.run(node, ctx),
             Self::ImportPreferDefaultExport(rule) => rule.run(node, ctx),
             Self::ImportUnambiguous(rule) => rule.run(node, ctx),
+            Self::EslintConsistentThis(rule) => rule.run(node, ctx),
             Self::EslintAccessorPairs(rule) => rule.run(node, ctx),
             Self::EslintArrayCallbackReturn(rule) => rule.run(node, ctx),
             Self::EslintArrowBodyStyle(rule) => rule.run(node, ctx),
@@ -12272,6 +12288,7 @@ impl RuleEnum {
             Self::ImportNoWebpackLoaderSyntax(rule) => rule.run_once(ctx),
             Self::ImportPreferDefaultExport(rule) => rule.run_once(ctx),
             Self::ImportUnambiguous(rule) => rule.run_once(ctx),
+            Self::EslintConsistentThis(rule) => rule.run_once(ctx),
             Self::EslintAccessorPairs(rule) => rule.run_once(ctx),
             Self::EslintArrayCallbackReturn(rule) => rule.run_once(ctx),
             Self::EslintArrowBodyStyle(rule) => rule.run_once(ctx),
@@ -12951,6 +12968,7 @@ impl RuleEnum {
             Self::ImportNoWebpackLoaderSyntax(rule) => rule.run_on_jest_node(jest_node, ctx),
             Self::ImportPreferDefaultExport(rule) => rule.run_on_jest_node(jest_node, ctx),
             Self::ImportUnambiguous(rule) => rule.run_on_jest_node(jest_node, ctx),
+            Self::EslintConsistentThis(rule) => rule.run_on_jest_node(jest_node, ctx),
             Self::EslintAccessorPairs(rule) => rule.run_on_jest_node(jest_node, ctx),
             Self::EslintArrayCallbackReturn(rule) => rule.run_on_jest_node(jest_node, ctx),
             Self::EslintArrowBodyStyle(rule) => rule.run_on_jest_node(jest_node, ctx),
@@ -13712,6 +13730,7 @@ impl RuleEnum {
             Self::ImportNoWebpackLoaderSyntax(rule) => rule.should_run(ctx),
             Self::ImportPreferDefaultExport(rule) => rule.should_run(ctx),
             Self::ImportUnambiguous(rule) => rule.should_run(ctx),
+            Self::EslintConsistentThis(rule) => rule.should_run(ctx),
             Self::EslintAccessorPairs(rule) => rule.should_run(ctx),
             Self::EslintArrayCallbackReturn(rule) => rule.should_run(ctx),
             Self::EslintArrowBodyStyle(rule) => rule.should_run(ctx),
@@ -14393,6 +14412,7 @@ impl RuleEnum {
             Self::ImportNoWebpackLoaderSyntax(_) => ImportNoWebpackLoaderSyntax::IS_TSGOLINT_RULE,
             Self::ImportPreferDefaultExport(_) => ImportPreferDefaultExport::IS_TSGOLINT_RULE,
             Self::ImportUnambiguous(_) => ImportUnambiguous::IS_TSGOLINT_RULE,
+            Self::EslintConsistentThis(_) => EslintConsistentThis::IS_TSGOLINT_RULE,
             Self::EslintAccessorPairs(_) => EslintAccessorPairs::IS_TSGOLINT_RULE,
             Self::EslintArrayCallbackReturn(_) => EslintArrayCallbackReturn::IS_TSGOLINT_RULE,
             Self::EslintArrowBodyStyle(_) => EslintArrowBodyStyle::IS_TSGOLINT_RULE,
@@ -15335,6 +15355,7 @@ impl RuleEnum {
             Self::ImportNoWebpackLoaderSyntax(_) => ImportNoWebpackLoaderSyntax::HAS_CONFIG,
             Self::ImportPreferDefaultExport(_) => ImportPreferDefaultExport::HAS_CONFIG,
             Self::ImportUnambiguous(_) => ImportUnambiguous::HAS_CONFIG,
+            Self::EslintConsistentThis(_) => EslintConsistentThis::HAS_CONFIG,
             Self::EslintAccessorPairs(_) => EslintAccessorPairs::HAS_CONFIG,
             Self::EslintArrayCallbackReturn(_) => EslintArrayCallbackReturn::HAS_CONFIG,
             Self::EslintArrowBodyStyle(_) => EslintArrowBodyStyle::HAS_CONFIG,
@@ -16162,6 +16183,7 @@ impl RuleEnum {
             Self::ImportNoWebpackLoaderSyntax(rule) => rule.types_info(),
             Self::ImportPreferDefaultExport(rule) => rule.types_info(),
             Self::ImportUnambiguous(rule) => rule.types_info(),
+            Self::EslintConsistentThis(rule) => rule.types_info(),
             Self::EslintAccessorPairs(rule) => rule.types_info(),
             Self::EslintArrayCallbackReturn(rule) => rule.types_info(),
             Self::EslintArrowBodyStyle(rule) => rule.types_info(),
@@ -16837,6 +16859,7 @@ impl RuleEnum {
             Self::ImportNoWebpackLoaderSyntax(rule) => rule.run_info(),
             Self::ImportPreferDefaultExport(rule) => rule.run_info(),
             Self::ImportUnambiguous(rule) => rule.run_info(),
+            Self::EslintConsistentThis(rule) => rule.run_info(),
             Self::EslintAccessorPairs(rule) => rule.run_info(),
             Self::EslintArrayCallbackReturn(rule) => rule.run_info(),
             Self::EslintArrowBodyStyle(rule) => rule.run_info(),
@@ -17534,6 +17557,7 @@ pub static RULES: std::sync::LazyLock<Vec<RuleEnum>> = std::sync::LazyLock::new(
         RuleEnum::ImportNoWebpackLoaderSyntax(ImportNoWebpackLoaderSyntax::default()),
         RuleEnum::ImportPreferDefaultExport(ImportPreferDefaultExport::default()),
         RuleEnum::ImportUnambiguous(ImportUnambiguous::default()),
+        RuleEnum::EslintConsistentThis(EslintConsistentThis::default()),
         RuleEnum::EslintAccessorPairs(EslintAccessorPairs::default()),
         RuleEnum::EslintArrayCallbackReturn(EslintArrayCallbackReturn::default()),
         RuleEnum::EslintArrowBodyStyle(EslintArrowBodyStyle::default()),
```
2026-02-13 01:53:54 +00:00
connorshea 8166f80b41 chore(linter): Misc improvements to help prevent merge conflicts for generated files in oxlint (#18813)
- The header on rules_enum.rs was being stripped by accident, which was confusing for users. So I fixed that.
- Mark the generated files as generated in `.gitattributes`, for GitHub to understand the diffs are mostly pointless and can be collapsed by default (the CI job enforces that these files be up to date anyway).
- Mark the generated rule files as `merge=ours`. See https://git-scm.com/book/ms/v2/Customizing-Git-Git-Attributes for more info on what that means, but it should make the merge conflicts less problematic.
- Updated `just ready` to include the lintgen command.
- Added `cargo lintgen` to the AGENTS.md file.

AI Disclosure: Used Claude to find the reason for the lack of header comment because I had no idea why it was happening.
2026-02-01 20:18:39 +00:00
connorshea 41f92d15b6 fix(linter): Error when given config options for a lint rule that has no config options defined. (#18809)
Enforce that rules with no config struct/enum raise an error when provided with config options.

For example, the `no-debugger` rule does not take any configuration options. It would silently allow them, but that was an invalid behavior.

```json
{
  "rules": {
    "eslint/no-debugger": ["error", { "some": "option" }]
  }
}
```

With this PR, we get this error:

```
Failed to parse oxlint configuration file.

  x Invalid configuration for rule `no-debugger`:
  |   This rule does not accept configuration options.
```

This prevents misconfiguration from causing problems / confusing behaviors if you try to define a config option on a rule which supports no config options. This is another part of the work for #17854.

There were still a handful of rules not yet migrated over to the `config` defined inside the lint macro. I've worked around that problem by giving them dummy configs for now, and added a test accordingly.

AI Disclosure: Built with help from Raptor Mini + Copilot / Claude Code + Opus. Reviewed and verified by me.
2026-02-01 17:47:47 +00:00
Boshen f424ac675a refactor(linter): replace declare_all_lint_rules! macro with codegen (#18076)
## Summary

- Replace the `declare_all_lint_rules\!` proc-macro with checked-in codegen
- The generated `rules_enum.rs` file (~16K lines) provides better IDE support since the code is visible to rust-analyzer
- Run `cargo run -p oxc_linter_codegen` to regenerate
- Uses `quote\!` macro for clean, maintainable code generation

### Changes

- **New**: `tasks/linter_codegen/src/rules_enum.rs` - generates `RuleEnum` using `quote\!` macro
- **Modified**: `tasks/linter_codegen/src/main.rs` - added `generate_rules_enum_file()`
- **Modified**: `tasks/linter_codegen/src/rules.rs` - updated parser to scan module declarations
- **Modified**: `tasks/linter_codegen/Cargo.toml` - added `quote` and `proc-macro2` dependencies
- **Modified**: `crates/oxc_linter/src/rules.rs` - replaced macro invocation with re-exports, updated module docs
- **Modified**: `crates/oxc_linter/src/lib.rs` - added `pub mod rules_enum` to generated module, added clippy expects
- **Deleted**: `crates/oxc_macros/src/declare_all_lint_rules.rs` - removed proc-macro

### Adding new rules

1. Add the rule module to `crates/oxc_linter/src/rules.rs`
2. Run `cargo run -p oxc_linter_codegen` to regenerate `RuleEnum`

### Build time comparison

| Mode | main | this PR | Change |
|------|------|---------|--------|
| Dev | 26.80s | 31.39s | +17% |
| Release | 3m 32s | 3m 37s | +2% |

The slight increase is expected since rustc now parses the 1.1MB generated file instead of expanding a proc-macro. The tradeoff is better IDE support.

## Test plan

- [x] All 916 linter tests pass
- [x] `just lint` passes
- [x] `just ready` passes
- [x] Verified codegen works: `cargo run -p oxc_linter_codegen`

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-01-21 02:48:30 +00:00
Boshen 8f98fa0d4e test(rust): disable empty test and doctest targets (#17409)
## Summary

- Disable `test = false` and `doctest = false` for crates that have no tests
- Reduces CI noise from "0 passed; 0 failed" output
- Saves compilation time for empty test targets

**Affected crates:**
- `oxc_ast_macros` - lib test
- `oxc_language_server` - bin test, lib doctest
- `oxc_napi` - lib test
- `oxc_syntax` - lib doctest
- `oxc_compat_data` - bin test
- `oxc_coverage` - lib test
- `oxc_linter_codegen` - bin test
- `oxc_prettier_conformance` - lib test
- `oxc_transform_conformance` - lib test
- `vscode_docs` - bin test
- `website_common` - lib test, lib doctest

## Test plan

- [x] `cargo test` passes with no empty test suites

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2025-12-28 11:44:19 +00:00
camchenry b27c5b926c perf(linter): add codegen support for regex nodes (#14874)
Adds linter codegen support for recognizing `run` functions which have only a call to `run_regex_node` in them. In that case, we return the `AstKind`s that we parsed out from the `run_regex_node` file and return that as the node types to check for.
2025-10-28 23:44:27 +00:00
camchenry f830bea102 perf(linter): add codegen support for is_member_expression_kind in match (#14677)
`is_member_expression_kind` is a known method that has a fixed number of AST variants that it will return `true` for. We already parse this out at the beginning of linter codegen, so we know what variants this will check for. So, we can extend the node types instead of simply bailing out early.

<img width="713" height="376" alt="image" src="https://github.com/user-attachments/assets/d8e91df3-7ea8-4883-825a-196041e0dbcd" />
2025-10-17 10:20:40 +00:00
camchenry 400a99a2ad perf(linter): switch if !matches!(node.kind(), ...) to match block (#14650)
Adding support for `matches!` in the linter codegen would be somewhat tedious as we'd need to start parsing macros. Instead, there is a similar idiom that we already support: diverging `match` on `node.kind()`. So, I've updated the linter codegen to support `|` in match arms, plus updated rules to use this new pattern instead of `if !matches!(...)`.

<img width="1392" height="664" alt="CleanShot 2025-10-16 at 12 12 45@2x" src="https://github.com/user-attachments/assets/75762f93-152a-49e4-83ab-46240d7b2b62" />
2025-10-16 16:29:39 +00:00
camchenry 73f2cbb247 perf(linter): support getting as_member_expression_kind() variants (#14642)
Previously, we bailed out on `node.kind().as_member_expression_kind()` diverging `let` statements. Now, we can compute those member expression kinds as they should be relatively constant.

+1-3% on the linter benchmarks, plus we just dropped sub-millisecond on the Radix UI benchmark!

<img width="719" height="427" alt="Screenshot 2025-10-15 at 5 11 00 PM" src="https://github.com/user-attachments/assets/a15d4a28-ad79-447a-9025-69368474a40e" />
2025-10-16 01:57:10 +00:00
camchenry 12a99340ee perf(linter): detect diverging match blocks for node type skipping (#14631)
Adds a new kind of code analysis to the linter codegen. This new detector looks for diverging statements in lint rules, like:

```rust
let something = match node.kind() {
  AstKind::CallExpression(_) => { ... }
  _ => return,
}
```

The important part is that the expression _must_ diverge (i.e., it must have something like a `return` in its wildcard block). That way, we know only the matched node types in the match arms are used in the rule.
2025-10-15 20:56:46 +00:00
camchenry 48e3348952 perf(linter): update linter codegen to support wildcard with empty tuple (#14611)
Returning `()` in the wildcard of a match block like `match node.kind() { ... }` is effectively the same as `_ => {}`, we just didn't support this syntax yet.

+1-2% on the linter benchmarks.

<img width="717" height="373" alt="Screenshot 2025-10-14 at 11 51 51 PM" src="https://github.com/user-attachments/assets/5c5b03f0-15b2-4a2f-b7e5-ab57c61634b5" />
2025-10-15 09:16:33 +00:00
camchenry 90f5f89bdf refactor(linter): remove run_on_symbol (#14610)
- closes https://github.com/oxc-project/backlog/issues/191

Previous PRs removed most usage of this run function, and this migrates the rest to use `run_once`. The motivation for this is not performance (this is likely a slight regression, actually), but should be a slight benefit to binary size and general maintenance. We now only have 3 ways of running lint rules, which makes the linter runtime code simpler.
2025-10-15 09:16:32 +00:00
camchenry 463be73ffe perf(linter): support as_* for node type codegen (#14582)
Enables the linter codegen to recognize code like `let foo = node.kind().as_ast_kind() else { return };` and generate the relevant AST kinds.

<img width="724" height="429" alt="Screenshot 2025-10-13 at 11 30 46 PM" src="https://github.com/user-attachments/assets/8b71b44a-c22a-48c4-80c7-6d96f4960ca0" />
2025-10-14 08:21:26 +00:00
camchenry aec0c08961 perf(linter): allow analyzing node types in match blocks with guards (#14459)
Previously, we skipped guard conditions to be extra cautious. After thinking about it and using the runtime optimizations in practice, I think that we can allow patterns with guards, and we just assume that it may always be taken. So this should result in the largest set of AST kinds possible for a rule and will not narrow it down dynamically.
2025-10-13 12:02:26 +00:00
camchenry 75a529c611 feat(linter): add run info about what functions rules implement (#13839)
Adds linter codegen to specify run information about what functions a given rule implements. Looks for the `impl` block for the given rule and looks at all of the rules and checks what functions are implemented. multiple `impl` blocks cannot be specified for the same struct, so we are guaranteed to be able to know this just from the one block.

On its own, this information doesn't do much for us, but it will allow us to improve the linter runtime performance by optimizing how rules are run based on which functions are implemented and which are no-ops.
2025-10-12 10:22:32 +00:00
camc314 4d3c6e1294 fix(linter): support let-chains in codegen node type detection (#14006)
fixes the potential regression introduced by https://github.com/oxc-project/oxc/commit/c778cba9d5e828b7d021fe32297e798954e35575
2025-09-22 16:52:36 +00:00
camchenry 06363e0f69 perf(linter): add node type codegen support for match node.kind() (#13736)
- part of github.com/oxc-project/oxc/issues/12223

Continuation of PRs that generate node types for lint rules. This implements the last main syntax pattern in lint rules which looks like this:

```rust
match node.kind() {
  AstKind::Function(_) => ...,
  AstKind::ArrowFunctionExpression(_) => ...,
  _ => {},
}
```

For this example, we would generate `[Function, ArrowFunctionExpression]`. We do not support guards at the moment, or any non tuple-struct patterns. The wildcard block must be empty, and the match statement must be the only one in the `run` function. This is so that we can be absolutely sure the list of node types is correct, so we tend to be more conservative in what we allow.
2025-09-22 14:26:51 +00:00
camchenry 90c8286e2d perf(linter): detect node types from let..else statements (#13690)
- part of https://github.com/oxc-project/oxc/issues/12223

Adds basic detection for syntax like `let AstKind::Something(...) = node.kind() else { return };` in lint rules for generating rule runner trait impls.

## Correctness

Spot checking it on my laptop shows same number of diagnostics for `kibana` and `vscode`:

<img width="640" height="180" alt="Screenshot 2025-09-11 at 5 35 47 PM" src="https://github.com/user-attachments/assets/43eb90a7-c3a9-46ed-8b73-e077a1d8d7f3" />

<img width="632" height="179" alt="Screenshot 2025-09-11 at 5 36 08 PM" src="https://github.com/user-attachments/assets/93652148-e27a-4d8c-9121-307624f2ee27" />

## Benchmarks

~15% faster on `oven-sh/bun`:

<img width="880" height="369" alt="Screenshot 2025-09-11 at 5 26 49 PM" src="https://github.com/user-attachments/assets/69a4c806-003e-4ff0-a679-ab9c6616dfc4" />

~9% faster on `microsoft/vscode`:

<img width="833" height="331" alt="Screenshot 2025-09-11 at 5 27 50 PM" src="https://github.com/user-attachments/assets/a6a42f96-ecab-448e-919c-550e6b4bdc26" />

~11% faster on `posthog/posthog`:

<img width="814" height="323" alt="Screenshot 2025-09-11 at 5 28 23 PM" src="https://github.com/user-attachments/assets/64daac0d-5bff-40df-aee7-ee843ba84351" />

~8% faster on `elastic/kibana`:

<img width="797" height="324" alt="Screenshot 2025-09-11 at 5 31 00 PM" src="https://github.com/user-attachments/assets/3ca57a2b-0e69-4460-903f-baa36de69123" />
2025-09-12 04:03:57 +00:00
camchenry 53f5218eec refactor(linter_codegen): split into separate files (#13657)
Pure refactoring. Just splitting up the linter codegen into individual files to make it a little more organized. This will make it easier to add more "detectors" (I guess that's what we'll call them for now) for additional types of syntax for generating rule runner impls.
2025-09-11 13:48:12 +00:00
camchenry 476729bc2d refactor(linter): simplify RuleRunner trait definition (#13637)
Suggestion from @overlookmotel. Simplifies the `RuleRunner` trait down to a single `Option` constant. `None` indicates we should run the lint rule on any node type, and `Some(AstTypesBitset)` means we should use the specific node types given for filtering lint rules.
2025-09-10 04:25:48 +00:00
camchenry f9bff647ee refactor(linter_codegen): improve code style for collecting nodes (#13636)
Almost pure refactor. There are some slight styling changes in the rule runner impls file, but other than that, this is just some clean up.

- Make the rule runner template string a multiline string, so it is easier to read and edit
- Move functions into the if-else detector so we don't need to pass around mutable sets of strings
- Replaced BTreeSet with a `NodeTypeSet` that abstracts over the set manipulation somewhat and offers more utilities specific to the linter codegen

Co-authored-by: camc314 <18101008+camc314@users.noreply.github.com>
2025-09-10 11:56:44 +08:00
overlookmotel ed77c31c97 refactor(linter_codegen): simplify matching "or" patterns (#13576)
Follow-on after #13138, as per [this comment](https://github.com/oxc-project/oxc/pull/13138#discussion_r2328545173).

Pure refactor. Break out of loop early on first failure, as following iterations have no effect.
2025-09-08 04:42:26 +00:00
overlookmotel 06f44a3a19 refactor(linter_codegen): simplify sorting rules (#13575)
Follow-on after #13138, as per [this comment](https://github.com/oxc-project/oxc/pull/13138#discussion_r2328541818).

Pure refactor. Simplify sorting rules, by leaning on derived traits instead of manual implementation.
2025-09-08 04:36:26 +00:00
overlookmotel e110476f4f style(linter): reformat code (#13573)
Follow-on after #13138. Just code reformatting (inc codegen-generated code).
2025-09-08 04:21:21 +00:00
camchenry a744aff1d7 perf(linter): skip rules that do not have any relevant node types (#13138)
- part of https://github.com/oxc-project/oxc/issues/12223
- supersedes stack: https://github.com/oxc-project/oxc/pull/12228

This PR adds the ability to add node type information to indicate what node types a lint rule acts on. The lint rule runner can use this information to optimize running lint rules by skipping nodes, files, or rules that don't apply.

This is a simple version that is focused on creating the framework for automatically generating the rule runner code. This is not the end of optimizations. Currently only 102 rules have node type information, while 471 rules have no node type information and will not be skipped. Adding node type information for more rules will improve performance. We may also be able to skip more nodes and files by being clever about how we use this information. For example, filtering out any rules which do not run on any node type, if the file contains no relevant node types for the rule.

- Adds a new `RuleRunner` trait which includes information needed for indicating whether a rule has node type info or not
- Adds a `oxc_linter_codegen` task which is used to generate the implementation of `RuleRunner` trait for all lint rules. This is mostly written by AI, so worth a bit of skepticism, but I've tried to manually verify the results and it looks good (just lacking more complex detection).
  - Currently, only rules that contain a single top-level `if` statement (or chain of `else if`), or a single `match` statement will have their node type info generated. This should give us a high level of confidence, since these patterns are relatively easy to verify and identify, but it's only a subset of all rules. Next steps will be to add support for `let...else`
- Adjusts the main linting loop to use the node type information to skip rules for node types that don't apply
- Updates CI to include a check for any uncommitted linter codegen changes

---

## Benchmarks

In the latest version of this PR, CodSpeed shows an up to +13% increase in linter performance across large and small files. In the real-world, I've seen a ~5-15% improvement in performance, but in some cases close to zero for very small files. In either case, there appears to be little downside and a lot of potential upside here.

<img width="769" height="242" alt="Screenshot 2025-08-24 at 9 29 30 PM" src="https://github.com/user-attachments/assets/d2c430c4-17a9-4606-85d5-c66c734975d9" />
<img width="757" height="308" alt="Screenshot 2025-08-24 at 9 30 24 PM" src="https://github.com/user-attachments/assets/5508464f-de55-4f4e-9ddd-58f215c4ce85" />
<img width="976" height="368" alt="Screenshot 2025-08-24 at 9 34 52 PM" src="https://github.com/user-attachments/assets/203f83c7-46f3-49f1-a1a8-e18f7ec8483b" />
2025-09-08 03:22:26 +00:00