## Summary
- report a bare underscore parameter when no args ignore pattern is configured
- preserve the optimized underscore-prefix behavior for explicit argsIgnorePattern configuration
- update focused rule coverage and generated documentation
Closes#25626
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 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.
## Summary
- auto-discover `oxfmt.config.mts` and `oxlint.config.mts`
- watch `.mts` configs in the language server and reject `.ts`/`.mts` conflicts
- add focused CLI and LSP coverage
Fixes#24310
Root config's `ignorePatterns` are now resolved relative to the directory containing the config file, instead of CWD.
Also fixes a latent issue: a `--config <path>` containing `..` silently disabled all `ignorePatterns` (the unnormalized parent dir never prefix-matches lint target paths). Explicit config paths are now lexically normalized, same as Oxfmt.
### Behavior change
The new anchoring applies in all cases, including `--config` / LSP `configPath`. This intentionally differs from ESLint, which resolves alternate-config ignore patterns relative to CWD:
- `extends`, `overrides[].files`, and JS plugin specifiers are already config-file-relative, including via `--config`
- rooting only `ignorePatterns` at CWD would give one file two coordinate systems
- Wrappers like Vite+ pass `--config <workspace-root config>` as an implementation detail while CWD stays at the invocation directory, so "`--config` means CWD-relative" does not hold in practice
- #24276
Impact should be small: bare names (`dist`, `*.min.js`) and `**/`-prefixed patterns are unaffected by the anchor.
The updated `config_ignore_patterns/with_oxlintrc` snapshot shows the `--config` case.
## Summary
This PR fixes two issues in `unicorn/prefer-array-find`:
- Oxlint reports a false positive for rest destructuring from
`filter()`, where `find()` is not an equivalent replacement.
- The rule docs are missing supported cases that the implementation
already detects, including array destructuring and `findLast()`-style
patterns. The upstream [eslint-plugin-unicorn
docs](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-array-find.md)
cover these cases, so this brings Oxlint's docs closer to the equivalent
rule.
## False positive
On this line, Oxlint currently reports a false positive diagnostic from
`unicorn/prefer-array-find`:
```js
const [element, ...restElements] = myArray.filter(isEven);
```
That diagnostic is incorrect because the code keeps both the first
matching element and the remaining matching elements. Replacing it with
`find()` would drop `restElements`, so it is not an equivalent
transformation.
The fix is to only report array destructuring when there is no rest
element. This still reports valid cases like:
```js
const [element] = myArray.filter(isEven);
```
## Docs mismatch
Oxlint already detects more than the docs describe. In addition to
`filter(...)[0]` and `.shift()`, the implementation handles simple array
destructuring and last-match cases using `.at(-1)` or `.pop()`.
This PR updates the docs to include those supported patterns and their
`find()` / `findLast()` replacements, matching the upstream
[eslint-plugin-unicorn
docs](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-array-find.md)
more closely for the cases Oxlint currently implements.
## Upstream parity mismatch still exists
[`eslint-plugin-unicorn`](https://github.com/sindresorhus/eslint-plugin-unicorn/blob/main/docs/rules/prefer-array-find.md)
also documents and detects `.filter(fn).slice(-1)[0]`-style last-element
patterns. Oxlint does not currently detect that pattern, so this PR does
not document it. That can be handled as a separate parity follow-up.
## Tests
- `cargo fmt -p oxc_linter`
- `cargo test -p oxc_linter prefer_array_find -- --nocapture`
- Manual local repro with `oxlint -D unicorn/prefer-array-find`
## AI assistance
AI assistance was used to prepare this change. I have reviewed the code
and remain responsible for the submission.
Co-authored-by: Cameron <cameron.clark@hey.com>
This adds more short descriptions to rules that lacked them, completes
all the rules in the following plugins:
- Oxc
- JSDoc
- Vue
- Nextjs
- Promise
- jsx-a11y
About 200 rules still lack short descriptions, so I'll do those in
subsequent PRs to avoid needing review on any massive sets.
---------
Co-authored-by: Cameron <cameron.clark@hey.com>
It sounds stupid to create a snapshot from an already existing file.
Works on https://github.com/oxc-project/oxc/issues/22955 showed they will always match.
The PR changes the tests so it just checks if the existing file matches the output, without snapshots.
Adds verified config schema generation for
`typescript/no-restricted-types`.
Refs #22955.
Signed-off-by: Alexander S. <sysix@sysix-coding.de>
Co-authored-by: Alexander S. <sysix@sysix-coding.de>
This adds short description fields using a codemod script and then
reviewing the result (by myself and with Claude to find any
possibly-weird ones and flag them for me) and then cleaning up some
overly-long or weirdly-formatted rule short descriptions (by myself).
This _does not_ cover all rules, as the codemod was not built to handle
the shared jest/vitest rules or "What it does" sections that were any
more complex than a single sentence. But it handles >75% of them. I'll
work on the remainder in the next few days after we get this part
merged.
I've also made modifications/edits to various rule descriptions and
short descriptions when I noticed as part of this work that they were
overly verbose, inconsistent with the description format of most other
rules, or grammatically incorrect.
<details>
<summary>Codemod script for reference</summary>
```js
#!/usr/bin/env node
// Insert a `short_description = "..."` argument into the
// `declare_oxc_lint!` macro for every rule whose inline-doc "What it does"
// section is a single sentence.
//
// Notes:
// - Only handles rules with INLINE `///` doc comments. Shared-docs rules
// (vitest/jest rules pulling from rules/shared) are intentionally skipped.
// - Markdown links `[text](url)` are flattened to `text`.
// - The insertion goes right after `version = "...",` so `short_description`
// sits at the end of the macro arguments. Indentation mirrors `version`.
// - `declare_oxc_lint!` must accept the `short_description = "..."` argument
// (see crates/oxc_macros/src/declare_oxc_lint.rs); otherwise the generated
// code will not compile.
//
// Usage: node insert_short_descriptions.mjs # dry-run, prints stats + sample diffs
// node insert_short_descriptions.mjs --write # actually modify files
// node insert_short_descriptions.mjs --show <substring> # preview generated insertions
import { readFileSync, writeFileSync, readdirSync, statSync } from "node:fs";
import { join, relative } from "node:path";
const RULES_DIR = "~/code/oxc/crates/oxc_linter/src/rules";
const SHARED_DIR = join(RULES_DIR, "shared");
function walk(dir) {
const out = [];
for (const entry of readdirSync(dir)) {
const p = join(dir, entry);
const s = statSync(p);
if (s.isDirectory()) out.push(...walk(p));
else if (entry.endsWith(".rs")) out.push(p);
}
return out;
}
// --- extraction (inline-only) --------------------------------------------
function ruleDocLines(source) {
const out = [];
for (const line of source.split("\n")) {
const m = line.match(/^\s*\/\/\/ ?(.*)$/);
if (m) out.push(m[1]);
}
return out;
}
function extractWhatItDoesFromMarkdown(lines) {
let start = -1;
for (let i = 0; i < lines.length; i++) {
if (/^\s*###\s+What it does\s*$/i.test(lines[i])) {
start = i + 1;
break;
}
}
if (start === -1) return null;
const body = [];
for (let i = start; i < lines.length; i++) {
if (/^\s*###\s+/.test(lines[i])) break;
body.push(lines[i]);
}
while (body.length && body[0].trim() === "") body.shift();
while (body.length && body[body.length - 1].trim() === "") body.pop();
return body;
}
// A "What it does" body that contains a markdown list or a URL can't be
// faithfully reduced to a one-line sentence — flattening it produces a run-on
// string (the whole list inlined) or a description that leads with a raw URL.
// Skip these rather than emit garbage.
function hasListOrUrl(bodyLines) {
let inFence = false;
for (const line of bodyLines) {
if (/^\s*```/.test(line)) { inFence = !inFence; continue; }
if (inFence) continue;
if (/^\s*([-*+]|\d+\.)\s+/.test(line)) return true; // markdown list item
if (/https?:\/\//.test(line)) return true; // bare/inline URL
}
return false;
}
function countSentences(bodyLines) {
const kept = [];
let inFence = false;
for (const line of bodyLines) {
if (/^\s*```/.test(line)) { inFence = !inFence; continue; }
if (!inFence) kept.push(line);
}
let text = kept.join(" ").trim();
if (!text) return 0;
text = text.replace(/`[^`]*`/g, "X");
text = text.replace(/\b(e\.g|i\.e|etc|vs|cf|approx|Mr|Mrs|Ms|Dr|Jr|Sr)\./gi, "$1");
text = text.replace(/(\d)\.(\d)/g, "$1$2");
const matches = text.match(/[.!?]+(?=\s|$)/g);
return matches ? matches.length : 0;
}
// --- build short_description ---------------------------------------------
function bodyToShortDescription(bodyLines) {
const kept = [];
let inFence = false;
for (const line of bodyLines) {
if (/^\s*```/.test(line)) { inFence = !inFence; continue; }
if (!inFence) kept.push(line);
}
let text = kept.join(" ").replace(/\s+/g, " ").trim();
let prev;
do {
prev = text;
text = text.replace(/\[([^\]]*)\]\(([^)]*)\)/g, "$1");
} while (text !== prev);
return text;
}
function escapeForRustString(s) {
return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
}
// --- macro surgery -------------------------------------------------------
function findMacroSpan(source) {
const idx = source.indexOf("declare_oxc_lint!");
if (idx === -1) return null;
let i = idx + "declare_oxc_lint!".length;
while (i < source.length && /\s/.test(source[i])) i++;
if (source[i] !== "(") return null;
const open = i;
let depth = 1;
i++;
while (i < source.length && depth > 0) {
const c = source[i];
if (c === '"') {
i++;
while (i < source.length && source[i] !== '"') {
if (source[i] === "\\") i++;
i++;
}
} else if (c === "/" && source[i + 1] === "/") {
while (i < source.length && source[i] !== "\n") i++;
} else if (c === "(") {
depth++;
} else if (c === ")") {
depth--;
if (depth === 0) return { open, close: i };
}
i++;
}
return null;
}
function insertShortDescription(source, sentence) {
const span = findMacroSpan(source);
if (!span) return null;
const inner = source.slice(span.open + 1, span.close);
// Don't touch rules that already have the field.
if (/\bshort_description\s*=/.test(inner)) return source;
// Match the last `version = "..."` assignment. Last-match wins so we skip
// any stray mentions in doc comments. The trailing comma (if any) is
// detected separately below — folding `\s*,?` into this regex would let
// `\s*` swallow the following newline when there is no comma, misplacing the
// insertion point.
const versionRe = /\bversion\s*=\s*"(?:[^"\\]|\\.)*"/g;
let lastMatch = null;
let vm;
while ((vm = versionRe.exec(inner)) !== null) lastMatch = vm;
if (!lastMatch) return null;
const matchStartAbs = span.open + 1 + lastMatch.index;
let matchEndAbs = matchStartAbs + lastMatch[0].length;
// Consume an optional trailing comma that sits on the same line (after only
// horizontal whitespace), leaving any newline untouched.
let j = matchEndAbs;
while (source[j] === " " || source[j] === "\t") j++;
const hasTrailingComma = source[j] === ",";
if (hasTrailingComma) matchEndAbs = j + 1;
let lineStart = matchStartAbs;
while (lineStart > 0 && source[lineStart - 1] !== "\n") lineStart--;
const indent = source.slice(lineStart, matchStartAbs);
const isOnOwnLine = /^\s*$/.test(indent);
const escaped = escapeForRustString(sentence);
let insertion;
if (isOnOwnLine) {
const prefix = hasTrailingComma ? "" : ",";
insertion = `${prefix}\n${indent}short_description = "${escaped}",`;
} else {
const prefix = hasTrailingComma ? " " : ", ";
insertion = `${prefix}short_description = "${escaped}",`;
}
return source.slice(0, matchEndAbs) + insertion + source.slice(matchEndAbs);
}
// --- main ----------------------------------------------------------------
const args = process.argv.slice(2);
const flag = (n) => args.includes(n);
const showIdx = args.indexOf("--show");
const showNeedle = showIdx >= 0 ? args[showIdx + 1] : null;
const writeMode = flag("--write");
const files = walk(RULES_DIR).sort();
let processed = 0;
let skippedShared = 0;
let skippedMulti = 0;
let skippedListOrUrl = 0;
let skippedMissing = 0;
let skippedAlreadyHas = 0;
let skippedNoVersion = 0;
let changed = 0;
const samples = [];
for (const file of files) {
if (file.startsWith(SHARED_DIR + "/")) continue;
const src = readFileSync(file, "utf8");
if (!/declare_oxc_lint!/.test(src)) continue;
processed++;
const docLines = ruleDocLines(src);
const body = extractWhatItDoesFromMarkdown(docLines);
if (body === null) { skippedShared++; continue; }
if (body.length === 0) { skippedMissing++; continue; }
if (hasListOrUrl(body)) { skippedListOrUrl++; continue; }
const n = countSentences(body);
if (n !== 1) { skippedMulti++; continue; }
const sentence = bodyToShortDescription(body);
if (!sentence) { skippedMissing++; continue; }
if (/\bshort_description\s*=/.test(src)) { skippedAlreadyHas++; continue; }
const updated = insertShortDescription(src, sentence);
if (updated === null) { skippedNoVersion++; continue; }
if (updated === src) { skippedAlreadyHas++; continue; }
if (showNeedle && file.includes(showNeedle)) {
samples.push({ file, sentence, updated });
} else if (samples.length < 3 && !showNeedle) {
samples.push({ file, sentence, updated });
}
if (writeMode) writeFileSync(file, updated);
changed++;
}
console.log(`Rule files scanned: ${processed}`);
console.log(`Would insert short_description: ${changed}`);
console.log(`Skipped (shared-docs / no inline): ${skippedShared}`);
console.log(`Skipped (multi-sentence / none): ${skippedMulti}`);
console.log(`Skipped (list or URL body): ${skippedListOrUrl}`);
console.log(`Skipped (empty/missing body): ${skippedMissing}`);
console.log(`Skipped (already has field): ${skippedAlreadyHas}`);
console.log(`Skipped (no version= found): ${skippedNoVersion}`);
console.log(writeMode ? `\nWROTE ${changed} files.` : `\nDry run. Pass --write to apply.`);
if (samples.length) {
console.log("\nSamples:\n");
for (const s of samples) {
console.log(`--- ${relative(RULES_DIR, s.file)} ---`);
console.log(` short_description = "${escapeForRustString(s.sentence)}"`);
const span = findMacroSpan(s.updated);
if (span) {
const chunk = s.updated.slice(span.open - 20, span.close + 2);
console.log("");
for (const line of chunk.split("\n")) console.log(" " + line);
}
console.log("");
}
}
```
</details>
---------
Co-authored-by: Cameron Clark <cameron.clark@hey.com>
- Used `DefaultRuleConfig` to parse the `serde_json` value
- Moved the 25 cap, to the `Deserializer`
- added schema validation + deny unknown fields
- removed test cases where the upstream rule auto-generated options for every rule
issue #22955
There are three rules that use the same config and default value:
- `react/no-did-mount-set-state`
- `react/no-did-update-set-state`
- `react/no-will-update-set-state`
this PR also moves that config into a shared `SetStateInFunctionConfig`
type to avoid duplication, similar to the `AlwaysNever` enum
The upstream rule has an option we do not to support:
```
/** Whether to not error when running with a tsconfig that has strictNullChecks turned. */
allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean;
```
https://typescript-eslint.io/rules/no-unnecessary-condition/
The upstream rules does support one more option, but I guess we do not want this:
```
/** Unless this is set to `true`, the rule will error on every file whose `tsconfig.json` does _not_ have the `strictNullChecks` compiler option (or `strict`) set to `true`. */
allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean;
```
https://typescript-eslint.io/rules/prefer-nullish-coalescing
Another little difference: The upstream rule marks as `true | IgnorePrimitivesOptions`, we support the `false` value too.
## Summary
- Remove only `jsx_a11y/no-noninteractive-element-interactions` from the unverified schema skip list.
- Regenerate the oxlint JSON schema, website schema snapshot, and generated TypeScript config types.
- Leave `jsx_a11y/no-interactive-element-to-noninteractive-role` on the skip list for a separate fix.