perf(linter/jest/valid-title): compile disallowedWords regex once#24412
Merged
Conversation
The disallowedWords regex was escaped, joined, and recompiled for every test/describe title. Build it once in from_configuration and store the compiled Option<Regex> on the config instead. 74.8x faster on a fixture dense in titles with a multi-word disallowedWords config (2161ms -> 29ms, paired N=40); output is byte-identical. Implemented by Claude Code. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Merging this PR will not alter performance
Comparing Footnotes
|
camc314
reviewed
Jul 12, 2026
graphite-app Bot
pushed a commit
that referenced
this pull request
Jul 13, 2026
#24420) AI Disclosure: Generated with Claude Code, reviewed and tested by me. I don't think there's a particularly good reason to use a RwLock like this? This can be split into separate PRs if preferred, as it also adds the type for this rule to the generated definitions and JSON Schema. The rule kept compiled `checkTypesPattern` regexes in a global `OnceLock<RwLock<FxHashMap<String, Regex>>>`. Every `run()` on a JSDoc-documented function: - cloned the pattern `String` to use as the map key, - took the RwLock **write** lock even on cache hits, serializing all linter threads through a single lock, and - held the guard across the entire parameter check. The pattern is fixed at configuration time, so the cache is unnecessary: this PR deserializes the option directly into a `Regex` field on the config (same approach as #24412 / #24417) and deletes the cache entirely. ## Benchmark Paired interleaved samples (N=40), release builds, fixture of 64 files × 400 fully-documented JSDoc functions (25,600 `run()` calls, all hitting the regex path, zero violations) with only `jsdoc/require-param` enabled: | Run | Baseline (8de6fca) | This PR | Result | |---|---|---|---| | Rule enabled | 56.95 ms | 18.25 ms | **3.12×**, t=132.8, 40/0 wins | | Parse-only null control (same fixture) | 11.05 ms | 10.75 ms | 1.03×, t=1.5 — noise | Subtracting the ~11 ms parse floor, rule execution drops from ~46 ms to ~7 ms (~6.4×). The win is mostly the removed cross-thread lock contention, so it scales with file count on multi-core machines. ## Correctness - Diagnostics are byte-identical between baseline and this PR on a mixed pass/fail canary, under both the default config and a custom `checkTypesPattern` config (which changes reporting, confirming the option deserializes and takes effect). - `oxc_linter` lib tests (1,169), `oxlint` tests (396), and clippy pass; the generated website rule-doc page is byte-identical (`schemars(with = "String", default)` preserves the documented default). ## Invalid patterns are now a config error, not a panic An invalid `checkTypesPattern` previously panicked lazily, on the first checked function. It now fails config loading with a proper diagnostic: ``` Failed to parse oxlint configuration file. x Invalid configuration for rule `jsdoc/require-param`: | regex parse error: | ^(unclosed | ^ | error: unclosed group ``` To get there, two things changed: - The regex deserializer lives in `utils/config.rs` alongside the existing `deserialize_regex_option` / `deserialize_regex_vec` / `deserialize_required_regex_option` family, and builds via `RegexBuilder` with `D::Error::custom` like they all do. - `from_configuration` propagates the serde error instead of dropping it with `.ok()`. The linter already has the plumbing for this — `config/rules.rs` turns an `Err` into `OverrideRulesError::RuleConfiguration` — the `.ok()` was just discarding it. ### Behavior change worth flagging Now that `from_configuration` no longer swallows errors, this config's `deny_unknown_fields` is load-bearing for the first time. A config carrying an `eslint-plugin-jsdoc` option that oxc doesn't implement (e.g. `unnamedRootBase`) previously fell back to defaults silently; it now fails to load with an "unknown field" error. No lint *results* change, but a user porting an eslint-plugin-jsdoc config that oxc previously tolerated will now see a config error. That surfaced in the ported test suite: eight cases configure `unnamedRootBase` (×5), `enableFixer` (×2), `enableRootFixer`, and `autoIncrementBase`, none of which this rule implements. They're commented out, and the snapshot is regenerated accordingly. If maintainers would rather keep the lenient behavior, the alternative is to validate `checkTypesPattern` up front and keep the `.ok()` fallback for everything else — that removes the panic without touching any test or changing config tolerance. Happy to switch. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
graphite-app Bot
pushed a commit
that referenced
this pull request
Jul 13, 2026
…ce (#24417) `match_argument_value_with_regex` recompiled every `allow` pattern with `Regex::new` for each `require()` call and `TSImportEqualsDeclaration` it checked. This compiles the patterns once in `from_configuration` into a `serde`/`schemars`-skipped `Vec<Regex>` on the config and matches against that. Same shape as #24412 and #24413. ### Benchmark **Fixture:** a generated `requires.ts` with 30,000 one-line require calls of the form ```ts const lib12345 = require('./lib12345'); ``` None of the paths match an `allow` pattern, so every call pays the full allow-matching path and reports on both binaries (the common case for this rule: mostly-disallowed requires with a small allowlist). The allow/skip path itself was parity-checked separately with a canary mixing allowed string literals, an allowed template literal, an allowed `import ... = require(...)`, and reported requires. **Config:** single-rule `.oxlintrc.json` so nothing else contributes to the timing: ```json { "plugins": ["typescript"], "categories": { "correctness": "off" }, "rules": { "typescript/no-require-imports": ["warn", { "allow": ["/package\\.json$", "^some-package$", "\\.jsonc$"] }] } } ``` **Method:** two release binaries (baseline from `main`, this branch), run interleaved in alternating order for 40 paired samples with warmup, timed with `time.monotonic_ns()`. A parse-only config on the same fixture showed no meaningful difference between the binaries (1.018×, t=1.7), so the delta is attributable to the rule rather than binary-layout noise. | | mean | |---|---| | before | 410.80 ms | | after | 32.26 ms | **12.7× faster** (paired t=229, 40/40 wins). The magnitude scales with require-call count × number of `allow` patterns; configs that don't set `allow` are unaffected (the matching path is already guarded by `allow.is_empty()`). Diagnostics output is identical between the two binaries on the fixture (30,000 diagnostics) and the canary. It is very unlikely that this will have much of an impact in most cases, as the option is rare and so are `require()` calls these days. But it's a simple enough improvement. ### Behavior note An invalid `allow` pattern previously panicked (`.unwrap()`) at the first `require()` call in a linted file; it now panics when the configuration is loaded — same panic, strictly earlier. ### Disclosure This change was implemented and benchmarked with AI assistance (Claude Code), and has been reviewed by the submitter. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Boshen
added a commit
that referenced
this pull request
Jul 14, 2026
# Oxlint ### 🚀 Features - 0433a83 linter/eslint/no-inner-declarations: Add `namespaces` option (#24044) (Boshen) - 92f154a oxlint,oxfmt: Auto-discover `.mts` config files (#24357) (camc314) - 8c1d74b linter/import/no-duplicates: Add autofix logic (#24273) (Cole Ellison) ### 🐛 Bug Fixes - 0b086de linter/jest/prefer-lowercase-title: False positive when `lowercaseFirstCharacterOnly` is false (#24414) (Connor Shea) - 097cb95 linter: Allow `vite-plus/test` and `@effect as Vitest source (#24196) (Liang) - 8337835 linter: Error on `ignorePatterns` that cannot match files aoutside the config directory (#24341) (leaysgur) - 9ba30e5 linter/oxc/bad-replace-all-arg: Add note to enhance diagnostic (#24346) (camc314) - 2ce5a33 linter: Resolve `ignorePatterns` relative to the config dir (#24339) (leaysgur) - ab90eed linter/eslint/no-loop-func: Do not error on catch variables (#24316) (Chris Opperwall) - b67f0a6 linter/eslint/no-unused-vars: Count default parameter updates as usage (#24323) (camc314) - d193f8e linter: Detect Junie agent env vars (#24277) (Jeevan Mohan Pawar) - 2aecf60 linter/eslint/no-unreachable: Handle `break` in switch stmts correctly (#24260) (camc314) ### ⚡ Performance - 7f80cac linter/vue/prop-name-casing: Precompile `ignoreProps` regex pattern (#24413) (connorshea) - 6272051 linter/typescript/no-require-imports: Compile allow patterns once (#24417) (connorshea) - fb1edf1 linter: Compute comment fix span only for directive comments (#24419) (connorshea) - 33805b9 linter/jsdoc/require-param: Compile checkTypesPattern regex once (#24420) (connorshea) - 8de6fca linter/jest/valid-title: Compile disallowedWords regex once (#24412) (Connor Shea) - 4a0d8dc linter/eslint/no-underscore-dangle: Avoid String clone per identifier (#24371) (Ian Macalinao) - f3ab04c linter/typescript/consistent-type-imports: Remove redundant Vec per violation (#24370) (Ian Macalinao) - 4d2d78d linter/typescript/prefer-ts-expect-error: Avoid String clone per comment (#24369) (Ian Macalinao) # Oxfmt ### 🚀 Features - 3a7fe74 formatter_css: Update oxc-css-parser to 0.0.7 (#24434) (leaysgur) - 0173cd3 formatter_css: Format Less :extend and merge props (#24358) (leaysgur) - 92f154a oxlint,oxfmt: Auto-discover `.mts` config files (#24357) (camc314) - df250df formatter: Support `quoteProps` for TS enum and methods (#24309) (leaysgur) - a9a5cd6 formatter_core: Expose `SourceText::as_str()` (#24281) (leaysgur) ### 🐛 Bug Fixes - 162bddf formatter: Add required parens for conditional type in type parameter constraint (#24450) (leaysgur) - 2d22a91 formatter: Determine type cast target from span instead of lexical scan (#24447) (leaysgur) - 25306e9 formatter: Do not add extra parens with type cast comment (#24444) (leaysgur) - bd6edfe formatter: Break arrow signature that exactly fills the line when cond body may hug (#24440) (leaysgur) - a99ef41 formatter: Keep quotes on method signature named new (#24432) (leaysgur) - fcc28df formatter_css: Keep glued-braket-value tight (#24352) (leaysgur) - 8337835 linter: Error on `ignorePatterns` that cannot match files aoutside the config directory (#24341) (leaysgur) - b7c7e15 formatter: Add parens for import and private field in new callee chain (#24320) (leaysgur) - 0c8f6e4 formatter: Update detect_code_removal for #24309 (#24314) (leaysgur) - a85aad0 formatter: Fix member-chain and non-null parens (#24312) (leaysgur) - 1c29c73 formatter: Preserve `TSNonNullExpression` in chain expression (#24311) (leaysgur) - 8933c0e formatter: Keep comment inside of empty `switch` block (#24308) (leaysgur) - ec26af2 formatter: Preserve blank lines between JSX attrs (#24290) (leaysgur) - 70bd54d formatter: Keep arrow function body comment (#24287) (leaysgur) - 415fe1e oxfmt: Error on ignorePatterns that cannot match files outside the config directory (#24286) (leaysgur) - eeabc4a formatter_css: Bail on EOF-recovered parse errors (#24282) (leaysgur) - 42ec8de formatter: Keep comments inside surviving parens and suppressed statement terminators (#24253) (leaysgur) - 1343779 formatter: Keep comment inline for empty statements (#24249) (leaysgur) - b996579 formatter: Print ; before trailing comments part 2 (#24246) (leaysgur) - 4f86e8c formatter: Print `;` before trailing comments (#24244) (leaysgur) - 01252e4 formatter: Add or remove parens for `let` declaration (#24215) (leaysgur) ### ⚡ Performance - eeb1913 formatter_core: Avoid per-call `Vec` work-stack in soft-line removal (#23775) (Marius Schulz) - a2f255b formatter: Use `SmallVec` for `MemberChain` collections (#23776) (Marius Schulz) ### 📚 Documentation - b52d0f5 formatter: Add TODO comment about unsound code (#24372) (overlookmotel) Co-authored-by: Boshen <1430279+Boshen@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The
disallowedWordsregex injest/valid-title(shared withvitest/valid-title) was escaped, joined, and recompiled for everytest/describetitle checked. This moves the compilation intofrom_configuration, storing a compiledOption<Regex>on the config.Benchmark
Fixture: a generated
spec.test.jscontaining 30,000 one-line test cases of the formNone of the titles contain a disallowed word, so the fixture measures the pure per-title overhead of the rule (the common case in a real suite); output parity on the reporting path was verified separately with a canary file whose titles do contain disallowed words.
Config: a single-rule
.oxlintrc.jsonso nothing else contributes to the timing:{ "plugins": ["jest"], "categories": { "correctness": "off" }, "rules": { "jest/valid-title": ["warn", { "disallowedWords": ["forbidden", "banned", "xyzzy"] }] } }Method: two release binaries (baseline from
main, this branch), run interleaved in alternating order for 40 paired samples (plus 5 warmup runs each) with--silent, timed withtime.monotonic_ns(). A parse-only config ("rules": {}) run on the same fixture showed no difference between the binaries (0.995×, t=-0.9), so the delta is attributable to the rule and not binary-layout noise.74.8× faster (paired t=651, 40/40 wins). The magnitude scales with title count × number of disallowed words; suites that don't set
disallowedWordsare unaffected (the rule already early-returns).--format=jsonoutput is byte-identical between the two binaries on the fixture and canaries.Disclosure
This change was implemented and benchmarked with AI assistance (Claude Code), and has been reviewed by the submitter.
🤖 Generated with Claude Code