Skip to content

feat(linter): Update 125 rules to raise errors when provided with invalid config options.#18104

Merged
graphite-app[bot] merged 1 commit intomainfrom
error-time
Jan 21, 2026
Merged

feat(linter): Update 125 rules to raise errors when provided with invalid config options.#18104
graphite-app[bot] merged 1 commit intomainfrom
error-time

Conversation

@connorshea
Copy link
Member

@connorshea connorshea commented Jan 17, 2026

Ran oxc-ecosystem-ci and it passed fine, no diff vs. main: https://github.com/oxc-project/oxc-ecosystem-ci/actions/runs/21199291772

image

Trial by fire, I guess. If this is too big of a change, we can split it up into batches of 25 each.

This builds on top of the changes from #17600 and various changes we've made over the last few months to adopt DefaultRuleConfig and other validations for the rule configuration logic.

Note that I was not able to make this change for jest/prefer-lowercase-title or import/no-cycle, as both have some tests using incorrect types in their tests, and so those tests begin failing if I try to expose invalid config options.

This should probably be fixed by removing the relevant tests that allow those values, as the values already are treated as invalid, or by updating the config struct for the rules to allow the currently-invalid values if we really want to support them. But that is a separate concern.

Note that eslint/prefer-promise-reject-errors is also not updated by this PR, as that is already covered by #18103.

Part of the goal of this change is to implement some of #17854, and also to prevent users from shooting themselves in the foot with a bad config hallucinated by AI tooling.

I used AI to create a quick little script and a bunch of invalid oxlint configs for some rules from this PR (and other PRs I opened today), see https://github.com/connorshea/oxlint-issue-repro-template/tree/invalid-configs. The result is printed below.

Invalid configs output
============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/eslint-no-bitwise.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/eslint-no-bitwise.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `no-bitwise`: invalid type: string "not-an-array", expected a sequence, received `{ "allow": "not-an-array" }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/eslint-no-cond-assign.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/eslint-no-cond-assign.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `no-cond-assign`: unknown variant `exceptions`, expected `except-parens` or `always`, received `{ "exceptions": 123 }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/eslint-no-unused-expressions.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/eslint-no-unused-expressions.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `no-unused-expressions`: invalid type: string "not-boolean", expected a boolean, received `{ "allowShortCircuit": "not-boolean" }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/eslint-unicode-bom.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/eslint-unicode-bom.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `unicode-bom`: unknown variant `require`, expected `always` or `never`, received `{ "require": 0 }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/import-consistent-type-specifier-style.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/import-consistent-type-specifier-style.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `import/consistent-type-specifier-style`: unknown variant `specifierStyle`, expected `prefer-top-level` or `prefer-inline`, received `{ "specifierStyle": 42 }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/import-no-absolute-path.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/import-no-absolute-path.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `import/no-absolute-path`: unknown field `allow`, expected one of `esmodule`, `commonjs`, `amd`, received `{ "allow": "yes" }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/import-no-duplicates.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/import-no-duplicates.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `import/no-duplicates`: unknown field `preferInlineType`, expected `prefer-inline` or `preferInline`, received `{ "preferInlineType": "maybe" }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/import-no-unassigned-import.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/import-no-unassigned-import.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `import/no-unassigned-import`: invalid type: integer `123`, expected a sequence, received `{ "allow": 123 }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/import-prefer-default-export.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/import-prefer-default-export.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `import/prefer-default-export`: invalid type: integer `123`, expected string or map, received `{ "target": 123 }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/jsdoc-require-returns.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/jsdoc-require-returns.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `jsdoc/require-returns`: invalid type: integer `999`, expected a sequence, received `{ "exemptedBy": 999 }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/jsx-fragments.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/jsx-fragments.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `react/jsx-fragments`: data did not match any variant of untagged enum JsxFragments, received `{ "eventHandlerPrefix": 123 }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/react-jsx-no-target-blank.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/react-jsx-no-target-blank.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `react/jsx-no-target-blank`: unknown field `enforce_dynamic_links`, expected one of `enforceDynamicLinks`, `warnOnSpreadAttributes`, `allowReferrer`, `links`, `forms`, received `{ "enforce_dynamic_links": 123 }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/react-prefer-es6-class.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/react-prefer-es6-class.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `react/prefer-es6-class`: unknown variant `allow`, expected `always` or `never`, received `{ "allow": 1 }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/react-state-in-constructor.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/react-state-in-constructor.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `react/state-in-constructor`: unknown variant `mode`, expected `always` or `never`, received `{ "mode": 123 }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/typescript-consistent-indexed-object-style.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/typescript-consistent-indexed-object-style.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `typescript/consistent-indexed-object-style`: unknown variant `style`, expected `record` or `index-signature`, received `{ "style": true }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/typescript-consistent-type-definitions.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/typescript-consistent-type-definitions.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `typescript/consistent-type-definitions`: unknown variant `style`, expected `interface` or `type`, received `{ "style": 456 }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/typescript-no-explicit-any.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/typescript-no-explicit-any.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `typescript/no-explicit-any`: invalid type: string "maybe", expected a boolean, received `{ "fixToUnknown": "maybe" }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/typescript-prefer-nullish-coalescing.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/typescript-prefer-nullish-coalescing.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `typescript/prefer-nullish-coalescing`: data did not match any variant of untagged enum IgnorePrimitives, received `{ "ignorePrimitives": "not-an-object" }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/typescript-return-await.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/typescript-return-await.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `typescript/return-await`: unknown variant `allowIn`, expected one of `in-try-catch`, `always`, `error-handling-correctness-only`, `never`, received `{ "allowIn": 1 }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/unicorn-explicit-length-check.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/unicorn-explicit-length-check.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `unicorn/explicit-length-check`: unknown field `allowedTypes`, expected `non-zero`, received `{ "allowedTypes": 456 }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/unicorn-no-array-reverse.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/unicorn-no-array-reverse.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `unicorn/no-array-reverse`: unknown field `allow_as_expression`, expected `allowExpressionStatement`, received `{ "allow_as_expression": "nope" }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/unicorn-switch-case-braces.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/unicorn-switch-case-braces.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `unicorn/switch-case-braces`: unknown variant `caseBraces`, expected `always` or `avoid`, received `{ "caseBraces": 123 }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/vue-define-emits-declaration.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/vue-define-emits-declaration.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `vue/define-emits-declaration`: unknown variant `declaration`, expected one of `type-based`, `type-literal`, `runtime`, received `{ "declaration": 0 }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/vue-define-props-declaration.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/vue-define-props-declaration.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `vue/define-props-declaration`: unknown variant `declaration`, expected `type-based` or `runtime`, received `{ "declaration": false }`
------------------------------------------------------------
Exit code: 1

Done.

AI Disclosure: This changeset was produced using Node.js scripts which I generated using AI, to find specific patterns in the codebase and then perform the replacements in the code accordingly, the two scripts are included below. I have gone through the 125 changes and confirmed that all 125 are identical to one another, and are the intended end-result code.

Basically, they look for rules which have code like this, and then perform the updates only on those rules:

    fn from_configuration(value: serde_json::Value) -> Result<Self, serde_json::error::Error> {
        Ok(serde_json::from_value::<DefaultRuleConfig<Self>>(value)
            .unwrap_or_default()
            .into_inner())
    }

This avoids needing to go through manually and decide which of these rules to update, and also ensures that all 125 of these rules were safe to update - as the configuration has already been updated to use DefaultRuleConfig, which means the unit tests have validated that the shape of the JSON schema for the rule matches the actual values ingested by the rule.

Node scripts for editing the rule files

add-deny-unknown-fields.js:

#!/usr/bin/env node
/*
Add `deny_unknown_fields` to `#[serde(...)]` attributes in files under `rules/`.

Usage:
  node scripts/add_deny_unknown_fields.js [--apply] [--base PATH]

- Scans for Rust files under any `rules/` directory.
- For files that contain the token `DefaultRuleConfig` and a `#[serde(...)]`
  attribute containing `rename_all = "camelCase"` and `default`, it will
  add `deny_unknown_fields` to that serde attribute if missing.

This is conservative and only modifies attributes that match those
conditions and currently do not include `deny_unknown_fields`.

Examples:
  # Preview changes (dry-run):
  node scripts/add_deny_unknown_fields.js

  # Apply changes in-place:
  node scripts/add_deny_unknown_fields.js --apply
*/

const fs = require("fs").promises;
const path = require("path");

const ATTR_RE = /(#\s*\[\s*serde\(([^)]*)\)\s*\])/gs; // s flag to allow newlines (node 12+)
const RENAME_RE = /rename_all\s*=\s*"camelCase"/;

async function findRuleFiles(base) {
  const results = [];
  const rulesDir = path.join(base, "crates", "oxc_linter", "src", "rules");
  try {
    const stat = await fs.stat(rulesDir);
    if (!stat.isDirectory()) return results;
  } catch (e) {
    // No rules directory found; return empty list
    return results;
  }

  async function walk(dir) {
    const entries = await fs.readdir(dir, { withFileTypes: true });
    for (const ent of entries) {
      const res = path.join(dir, ent.name);
      if (ent.isDirectory()) {
        // skip node_modules and target for speed
        if (ent.name === "node_modules" || ent.name === "target") continue;
        await walk(res);
      } else if (ent.isFile() && res.endsWith(".rs")) {
        results.push(res);
      }
    }
  }
  await walk(rulesDir);
  return results;
}

function updateSerdeAttribute(full, inner) {
  // inner is the content inside serde(...)
  const hasRename = RENAME_RE.test(inner);
  const hasDefault = /\bdefault\b/.test(inner);
  const hasDeny = /deny_unknown_fields/.test(inner);
  if (hasRename && hasDefault && !hasDeny) {
    // normalize tokens by splitting on commas
    const tokens = inner
      .split(",")
      .map((t) => t.trim())
      .filter(Boolean);
    if (!tokens.includes("deny_unknown_fields")) tokens.push("deny_unknown_fields");
    const newInner = tokens.join(", ");
    const newFull = full.replace("(" + inner + ")", "(" + newInner + ")");
    return { newFull, newInner };
  }
  return null;
}

async function processFile(filePath, apply) {
  const text = await fs.readFile(filePath, "utf8");

  const FROM_CONF_SNIPPET = `fn from_configuration(value: serde_json::Value) -> Result<Self, serde_json::error::Error> {
        Ok(serde_json::from_value::<DefaultRuleConfig<Self>>(value)
            .unwrap_or_default()
            .into_inner())
    }`;

  const FROM_CONF_SNIPPET_ALT = `fn from_configuration(value: Value) -> Result<Self, serde_json::error::Error> {
        Ok(serde_json::from_value::<DefaultRuleConfig<Self>>(value)
            .unwrap_or_default()
            .into_inner())
    }`;

  // Only process files that contain the exact from_configuration method above
  if (!text.includes(FROM_CONF_SNIPPET) && !text.includes(FROM_CONF_SNIPPET_ALT)) return false;

  let changed = false;
  let out = text;
  const edits = [];
  let m;
  while ((m = ATTR_RE.exec(text)) !== null) {
    const full = m[1];
    const inner = m[2];
    const upd = updateSerdeAttribute(full, inner);
    if (upd) {
      edits.push({ full, newFull: upd.newFull });
    }
  }

  if (edits.length > 0) {
    for (const e of edits) {
      out = out.replace(e.full, e.newFull);
      changed = true;
      console.log(`Will update: ${filePath}:`);
      console.log(`  - ${e.full.trim()}`);
      console.log(`  + ${e.newFull.trim()}`);
      console.log("");
    }
    if (apply) {
      await fs.writeFile(filePath, out, "utf8");
      console.log(`Applied changes to ${filePath}\n`);
    }
  }
  return changed;
}

async function main() {
  const args = process.argv.slice(2);
  const apply = args.includes("--apply");
  const quiet = args.includes("--quiet");
  const baseIndex = args.findIndex((a) => a === "--base");
  let base = process.cwd();
  if (baseIndex !== -1 && args.length > baseIndex + 1) base = path.resolve(args[baseIndex + 1]);

  try {
    const files = await findRuleFiles(base);
    if (!files.length) {
      if (!quiet) console.log("No files found in crates/oxc_linter/src/rules/.");
      return;
    }

    let changedAny = false;
    const editedFiles = [];
    for (const f of files) {
      try {
        const changed = await processFile(f, apply);
        if (changed) {
          changedAny = true;
          if (apply) editedFiles.push(f);
        }
      } catch (err) {
        console.error(`Error processing ${f}: ${err}`);
      }
    }

    if (!changedAny && !quiet)
      console.log("No matching serde attributes found that needed changes.");
    if (changedAny && !apply) console.log("\nRun with --apply to make the changes.");

    if (apply && editedFiles.length > 0) {
      if (!quiet) {
        console.log("\nEdited files:");
        for (const ef of editedFiles) console.log(`  - ${ef}`);
      }
    } else if (apply && !quiet) {
      console.log("\nNo files were changed.");
    }
  } catch (err) {
    console.error(err);
    process.exit(2);
  }
}

if (require.main === module) main();

update-from-configuration.js:

#!/usr/bin/env node
/*
Update `from_configuration` implementations in rule files that include `deny_unknown_fields`.

Usage:
  node scripts/update_from_configuration.js [--apply] [--base PATH]

- Scans for Rust files under `crates/oxc_linter/src/rules`.
- For files that contain the token `deny_unknown_fields` and the exact
  `from_configuration` method form that uses `unwrap_or_default().into_inner()`,
  it replaces that method body with a shorter `map(DefaultRuleConfig::into_inner)`
  call as described in the repo's style.

Examples:
  # Preview changes (dry-run):
  node scripts/update_from_configuration.js

  # Apply changes in-place:
  node scripts/update_from_configuration.js --apply
*/

const fs = require("fs").promises;
const path = require("path");

async function findRuleFiles(base) {
  const results = [];
  const rulesDir = path.join(base, "crates", "oxc_linter", "src", "rules");
  try {
    const stat = await fs.stat(rulesDir);
    if (!stat.isDirectory()) return results;
  } catch (e) {
    return results;
  }

  async function walk(dir) {
    const entries = await fs.readdir(dir, { withFileTypes: true });
    for (const ent of entries) {
      const res = path.join(dir, ent.name);
      if (ent.isDirectory()) {
        if (ent.name === "node_modules" || ent.name === "target") continue;
        await walk(res);
      } else if (ent.isFile() && res.endsWith(".rs")) {
        results.push(res);
      }
    }
  }
  await walk(rulesDir);
  return results;
}

async function processFile(filePath, apply) {
  const text = await fs.readFile(filePath, "utf8");

  // Only handle files that mention deny_unknown_fields
  if (!text.includes("deny_unknown_fields")) return false;

  const FROM_CONF_SNIPPET = `fn from_configuration(value: serde_json::Value) -> Result<Self, serde_json::error::Error> {
        Ok(serde_json::from_value::<DefaultRuleConfig<Self>>(value)
            .unwrap_or_default()
            .into_inner())
    }`;

  const FROM_CONF_SNIPPET_ALT = `fn from_configuration(value: Value) -> Result<Self, serde_json::error::Error> {
        Ok(serde_json::from_value::<DefaultRuleConfig<Self>>(value)
            .unwrap_or_default()
            .into_inner())
    }`;

  const REPLACEMENT = `fn from_configuration(value: serde_json::Value) -> Result<Self, serde_json::error::Error> {
        serde_json::from_value::<DefaultRuleConfig<Self>>(value).map(DefaultRuleConfig::into_inner)
    }`;

  const REPLACEMENT_ALT = `fn from_configuration(value: Value) -> Result<Self, serde_json::error::Error> {
        serde_json::from_value::<DefaultRuleConfig<Self>>(value).map(DefaultRuleConfig::into_inner)
    }`;

  if (!text.includes(FROM_CONF_SNIPPET) && !text.includes(FROM_CONF_SNIPPET_ALT)) return false;

  let out = text;
  let changed = false;

  if (out.includes(FROM_CONF_SNIPPET)) {
    out = out.replace(FROM_CONF_SNIPPET, REPLACEMENT);
    changed = true;
    console.log(`Will update: ${filePath} - replace serde_json::Value form`);
  }

  if (out.includes(FROM_CONF_SNIPPET_ALT)) {
    out = out.replace(FROM_CONF_SNIPPET_ALT, REPLACEMENT_ALT);
    changed = true;
    console.log(`Will update: ${filePath} - replace Value form`);
  }

  if (changed && apply) {
    await fs.writeFile(filePath, out, "utf8");
    console.log(`Applied changes to ${filePath}\n`);
  }

  return changed;
}

async function main() {
  const args = process.argv.slice(2);
  const apply = args.includes("--apply");
  const quiet = args.includes("--quiet");
  const baseIndex = args.findIndex((a) => a === "--base");
  let base = process.cwd();
  if (baseIndex !== -1 && args.length > baseIndex + 1) base = path.resolve(args[baseIndex + 1]);

  try {
    const files = await findRuleFiles(base);
    if (!files.length) {
      if (!quiet) console.log("No files found in crates/oxc_linter/src/rules/.");
      return;
    }

    let changedAny = false;
    const editedFiles = [];
    for (const f of files) {
      try {
        const changed = await processFile(f, apply);
        if (changed) {
          changedAny = true;
          if (apply) editedFiles.push(f);
        }
      } catch (err) {
        console.error(`Error processing ${f}: ${err}`);
      }
    }

    if (!changedAny && !quiet)
      console.log("No matching from_configuration methods found that needed changes.");
    if (changedAny && !apply) console.log("\nRun with --apply to make the changes.");

    if (apply && editedFiles.length > 0) {
      if (!quiet) {
        console.log("\nEdited files:");
        for (const ef of editedFiles) console.log(`  - ${ef}`);
      }
    } else if (apply && !quiet) {
      console.log("\nNo files were changed.");
    }
  } catch (err) {
    console.error(err);
    process.exit(2);
  }
}

if (require.main === module) main();

@connorshea connorshea requested a review from camc314 as a code owner January 17, 2026 00:03
Copilot AI review requested due to automatic review settings January 17, 2026 00:03
@github-actions github-actions bot added A-linter Area - Linter C-enhancement Category - New feature or request labels Jan 17, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates 125 linter rules to raise errors when provided with invalid configuration options. The changes add deny_unknown_fields to serde configuration structs and refactor the from_configuration method to properly propagate deserialization errors instead of silently ignoring them.

Changes:

  • Added deny_unknown_fields to serde attributes on configuration structs across 125 rule files
  • Refactored from_configuration methods from unwrap_or_default().into_inner() to map(DefaultRuleConfig::into_inner) to properly propagate errors
  • Changes were made using automated Node.js scripts to ensure consistency and correctness

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@codspeed-hq
Copy link

codspeed-hq bot commented Jan 17, 2026

CodSpeed Performance Report

Merging this PR will not alter performance

Comparing error-time (6815a64) with main (2eec951)

Summary

✅ 4 untouched benchmarks
⏩ 41 skipped benchmarks1

Footnotes

  1. 41 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

@connorshea
Copy link
Member Author

Ran oxlint-ecosystem-ci: https://github.com/oxc-project/oxc-ecosystem-ci/actions/runs/21084875120

9 errors, same as the most recent result on main, so this does not cause any new regressions :)

connorshea added a commit that referenced this pull request Jan 17, 2026
… an invalid config option.

This was not handled by #18104 as the rule was using a slightly different pattern for the from_configuration method compared to other rules.

Also add docs, why not.
connorshea added a commit that referenced this pull request Jan 17, 2026
… when passed invalid config options.

This was missed by the script in #18104 due to using kebab-case for the serde config.
connorshea added a commit that referenced this pull request Jan 17, 2026
…ons.

This was missed by the script in #18104 because that script only handles struct-based configs, not enum-based configs. So this handles some of the enum rules.
connorshea added a commit that referenced this pull request Jan 17, 2026
This updates a few miscellaneous rules to error on invalid config options. These were missed by the script from #18104 for various reasons.
graphite-app bot pushed a commit that referenced this pull request Jan 17, 2026
… invalid config options. (#18105)

This was not handled by #18104 as the rule was using a slightly different pattern for the from_configuration method compared to other rules, so it was missed by the scripts I used.

Also add docs, why not.
@github-actions github-actions bot added the A-cli Area - CLI label Jan 17, 2026
graphite-app bot pushed a commit that referenced this pull request Jan 20, 2026
Cherry-picked out of #18104 since I want to keep that PR focused on one thing.

This adds a snapshot test to ensure that this complex config is valid and doesn't result in any errors for the end-user. This is basically just protecting against us breaking the config validation system by accident, and covers a variety of different config shapes.
graphite-app bot pushed a commit that referenced this pull request Jan 21, 2026
…ons. (#18109)

These were not handled by the script in #18104 because that script only handles struct-based configs, not enum-based configs. With this, I've manually gone through and updated the rules with enum-based configs to use this new pattern.
graphite-app bot pushed a commit that referenced this pull request Jan 21, 2026
…18113)

This updates a few miscellaneous rules to error on invalid config options. These were missed by the script from #18104 for various reasons (uncommon serde attributes, from_configuration using a param name other than `value`, etc).
graphite-app bot pushed a commit that referenced this pull request Jan 21, 2026
… when passed invalid config options. (#18107)

This was missed by the script in #18104 due to using kebab-case for the serde config.
@connorshea connorshea requested a review from Copilot January 21, 2026 06:07
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 125 out of 125 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@connorshea connorshea added the 0-merge Merge with Graphite Merge Queue label Jan 21, 2026
Copy link
Member Author

connorshea commented Jan 21, 2026

Merge activity

…alid config options. (#18104)

Ran oxc-ecosystem-ci and it passed fine, no diff vs. main: https://github.com/oxc-project/oxc-ecosystem-ci/actions/runs/21199291772

Trial by fire, I guess. If this is too big of a change, we can split it up into batches of 25 each.

This builds on top of the changes from #17600 and various changes we've made over the last few months to adopt DefaultRuleConfig and other validations for the rule configuration logic.

Note that I was not able to make this change for `jest/prefer-lowercase-title` or `import/no-cycle`, as both have some tests using incorrect types in their tests, and so those tests begin failing if I try to expose invalid config options.

This should probably be fixed by removing the relevant tests that allow those values, as the values already are treated as invalid, or by updating the config struct for the rules to allow the currently-invalid values if we really want to support them. But that is a separate concern.

Note that `eslint/prefer-promise-reject-errors` is also not updated by this PR, as that is already covered by #18103.

Part of the goal of this change is to implement some of #17854, and also to prevent users from shooting themselves in the foot with a bad config hallucinated by AI tooling.

I used AI to create a quick little script and a bunch of invalid oxlint configs for some rules from this PR (and other PRs I opened today), see https://github.com/connorshea/oxlint-issue-repro-template/tree/invalid-configs. The result is printed below.

<details>

<summary>Invalid configs output</summary>

```
============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/eslint-no-bitwise.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/eslint-no-bitwise.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `no-bitwise`: invalid type: string "not-an-array", expected a sequence, received `{ "allow": "not-an-array" }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/eslint-no-cond-assign.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/eslint-no-cond-assign.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `no-cond-assign`: unknown variant `exceptions`, expected `except-parens` or `always`, received `{ "exceptions": 123 }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/eslint-no-unused-expressions.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/eslint-no-unused-expressions.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `no-unused-expressions`: invalid type: string "not-boolean", expected a boolean, received `{ "allowShortCircuit": "not-boolean" }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/eslint-unicode-bom.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/eslint-unicode-bom.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `unicode-bom`: unknown variant `require`, expected `always` or `never`, received `{ "require": 0 }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/import-consistent-type-specifier-style.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/import-consistent-type-specifier-style.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `import/consistent-type-specifier-style`: unknown variant `specifierStyle`, expected `prefer-top-level` or `prefer-inline`, received `{ "specifierStyle": 42 }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/import-no-absolute-path.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/import-no-absolute-path.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `import/no-absolute-path`: unknown field `allow`, expected one of `esmodule`, `commonjs`, `amd`, received `{ "allow": "yes" }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/import-no-duplicates.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/import-no-duplicates.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `import/no-duplicates`: unknown field `preferInlineType`, expected `prefer-inline` or `preferInline`, received `{ "preferInlineType": "maybe" }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/import-no-unassigned-import.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/import-no-unassigned-import.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `import/no-unassigned-import`: invalid type: integer `123`, expected a sequence, received `{ "allow": 123 }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/import-prefer-default-export.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/import-prefer-default-export.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `import/prefer-default-export`: invalid type: integer `123`, expected string or map, received `{ "target": 123 }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/jsdoc-require-returns.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/jsdoc-require-returns.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `jsdoc/require-returns`: invalid type: integer `999`, expected a sequence, received `{ "exemptedBy": 999 }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/jsx-fragments.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/jsx-fragments.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `react/jsx-fragments`: data did not match any variant of untagged enum JsxFragments, received `{ "eventHandlerPrefix": 123 }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/react-jsx-no-target-blank.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/react-jsx-no-target-blank.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `react/jsx-no-target-blank`: unknown field `enforce_dynamic_links`, expected one of `enforceDynamicLinks`, `warnOnSpreadAttributes`, `allowReferrer`, `links`, `forms`, received `{ "enforce_dynamic_links": 123 }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/react-prefer-es6-class.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/react-prefer-es6-class.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `react/prefer-es6-class`: unknown variant `allow`, expected `always` or `never`, received `{ "allow": 1 }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/react-state-in-constructor.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/react-state-in-constructor.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `react/state-in-constructor`: unknown variant `mode`, expected `always` or `never`, received `{ "mode": 123 }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/typescript-consistent-indexed-object-style.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/typescript-consistent-indexed-object-style.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `typescript/consistent-indexed-object-style`: unknown variant `style`, expected `record` or `index-signature`, received `{ "style": true }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/typescript-consistent-type-definitions.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/typescript-consistent-type-definitions.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `typescript/consistent-type-definitions`: unknown variant `style`, expected `interface` or `type`, received `{ "style": 456 }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/typescript-no-explicit-any.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/typescript-no-explicit-any.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `typescript/no-explicit-any`: invalid type: string "maybe", expected a boolean, received `{ "fixToUnknown": "maybe" }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/typescript-prefer-nullish-coalescing.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/typescript-prefer-nullish-coalescing.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `typescript/prefer-nullish-coalescing`: data did not match any variant of untagged enum IgnorePrimitives, received `{ "ignorePrimitives": "not-an-object" }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/typescript-return-await.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/typescript-return-await.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `typescript/return-await`: unknown variant `allowIn`, expected one of `in-try-catch`, `always`, `error-handling-correctness-only`, `never`, received `{ "allowIn": 1 }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/unicorn-explicit-length-check.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/unicorn-explicit-length-check.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `unicorn/explicit-length-check`: unknown field `allowedTypes`, expected `non-zero`, received `{ "allowedTypes": 456 }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/unicorn-no-array-reverse.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/unicorn-no-array-reverse.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `unicorn/no-array-reverse`: unknown field `allow_as_expression`, expected `allowExpressionStatement`, received `{ "allow_as_expression": "nope" }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/unicorn-switch-case-braces.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/unicorn-switch-case-braces.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `unicorn/switch-case-braces`: unknown variant `caseBraces`, expected `always` or `avoid`, received `{ "caseBraces": 123 }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/vue-define-emits-declaration.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/vue-define-emits-declaration.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `vue/define-emits-declaration`: unknown variant `declaration`, expected one of `type-based`, `type-literal`, `runtime`, received `{ "declaration": 0 }`
------------------------------------------------------------
Exit code: 1

============================================================
Config: <code-dir>/oxlint-invalid-configs/configs/vue-define-props-declaration.json
Command: node <code-dir>/oxc/apps/oxlint/dist/cli.js -c=<code-dir>/oxlint-invalid-configs/configs/vue-define-props-declaration.json
------------------------------------------------------------
   Failed to parse oxlint configuration file.
     x Invalid configuration for rule `vue/define-props-declaration`: unknown variant `declaration`, expected `type-based` or `runtime`, received `{ "declaration": false }`
------------------------------------------------------------
Exit code: 1

Done.
```
</details>

------

AI Disclosure: This changeset was produced using Node.js scripts which I generated using AI, to find specific patterns in the codebase and then perform the replacements in the code accordingly, the two scripts are included below. I have gone through the 125 changes and confirmed that all 125 are identical to one another, and are the intended end-result code.

Basically, they look for rules which have code like this, and then perform the updates only on those rules:

```rs
    fn from_configuration(value: serde_json::Value) -> Result<Self, serde_json::error::Error> {
        Ok(serde_json::from_value::<DefaultRuleConfig<Self>>(value)
            .unwrap_or_default()
            .into_inner())
    }
```

This avoids needing to go through manually and decide which of these rules to update, and also ensures that all 125 of these rules were safe to update - as the configuration has already been updated to use DefaultRuleConfig, which means the unit tests have validated that the shape of the JSON schema for the rule matches the actual values ingested by the rule.

<details>
<summary>Node scripts for editing the rule files</summary>

`add-deny-unknown-fields.js`:

```js
#!/usr/bin/env node
/*
Add `deny_unknown_fields` to `#[serde(...)]` attributes in files under `rules/`.

Usage:
  node scripts/add_deny_unknown_fields.js [--apply] [--base PATH]

- Scans for Rust files under any `rules/` directory.
- For files that contain the token `DefaultRuleConfig` and a `#[serde(...)]`
  attribute containing `rename_all = "camelCase"` and `default`, it will
  add `deny_unknown_fields` to that serde attribute if missing.

This is conservative and only modifies attributes that match those
conditions and currently do not include `deny_unknown_fields`.

Examples:
  # Preview changes (dry-run):
  node scripts/add_deny_unknown_fields.js

  # Apply changes in-place:
  node scripts/add_deny_unknown_fields.js --apply
*/

const fs = require("fs").promises;
const path = require("path");

const ATTR_RE = /(#\s*\[\s*serde\(([^)]*)\)\s*\])/gs; // s flag to allow newlines (node 12+)
const RENAME_RE = /rename_all\s*=\s*"camelCase"/;

async function findRuleFiles(base) {
  const results = [];
  const rulesDir = path.join(base, "crates", "oxc_linter", "src", "rules");
  try {
    const stat = await fs.stat(rulesDir);
    if (!stat.isDirectory()) return results;
  } catch (e) {
    // No rules directory found; return empty list
    return results;
  }

  async function walk(dir) {
    const entries = await fs.readdir(dir, { withFileTypes: true });
    for (const ent of entries) {
      const res = path.join(dir, ent.name);
      if (ent.isDirectory()) {
        // skip node_modules and target for speed
        if (ent.name === "node_modules" || ent.name === "target") continue;
        await walk(res);
      } else if (ent.isFile() && res.endsWith(".rs")) {
        results.push(res);
      }
    }
  }
  await walk(rulesDir);
  return results;
}

function updateSerdeAttribute(full, inner) {
  // inner is the content inside serde(...)
  const hasRename = RENAME_RE.test(inner);
  const hasDefault = /\bdefault\b/.test(inner);
  const hasDeny = /deny_unknown_fields/.test(inner);
  if (hasRename && hasDefault && !hasDeny) {
    // normalize tokens by splitting on commas
    const tokens = inner
      .split(",")
      .map((t) => t.trim())
      .filter(Boolean);
    if (!tokens.includes("deny_unknown_fields")) tokens.push("deny_unknown_fields");
    const newInner = tokens.join(", ");
    const newFull = full.replace("(" + inner + ")", "(" + newInner + ")");
    return { newFull, newInner };
  }
  return null;
}

async function processFile(filePath, apply) {
  const text = await fs.readFile(filePath, "utf8");

  const FROM_CONF_SNIPPET = `fn from_configuration(value: serde_json::Value) -> Result<Self, serde_json::error::Error> {
        Ok(serde_json::from_value::<DefaultRuleConfig<Self>>(value)
            .unwrap_or_default()
            .into_inner())
    }`;

  const FROM_CONF_SNIPPET_ALT = `fn from_configuration(value: Value) -> Result<Self, serde_json::error::Error> {
        Ok(serde_json::from_value::<DefaultRuleConfig<Self>>(value)
            .unwrap_or_default()
            .into_inner())
    }`;

  // Only process files that contain the exact from_configuration method above
  if (!text.includes(FROM_CONF_SNIPPET) && !text.includes(FROM_CONF_SNIPPET_ALT)) return false;

  let changed = false;
  let out = text;
  const edits = [];
  let m;
  while ((m = ATTR_RE.exec(text)) !== null) {
    const full = m[1];
    const inner = m[2];
    const upd = updateSerdeAttribute(full, inner);
    if (upd) {
      edits.push({ full, newFull: upd.newFull });
    }
  }

  if (edits.length > 0) {
    for (const e of edits) {
      out = out.replace(e.full, e.newFull);
      changed = true;
      console.log(`Will update: ${filePath}:`);
      console.log(`  - ${e.full.trim()}`);
      console.log(`  + ${e.newFull.trim()}`);
      console.log("");
    }
    if (apply) {
      await fs.writeFile(filePath, out, "utf8");
      console.log(`Applied changes to ${filePath}\n`);
    }
  }
  return changed;
}

async function main() {
  const args = process.argv.slice(2);
  const apply = args.includes("--apply");
  const quiet = args.includes("--quiet");
  const baseIndex = args.findIndex((a) => a === "--base");
  let base = process.cwd();
  if (baseIndex !== -1 && args.length > baseIndex + 1) base = path.resolve(args[baseIndex + 1]);

  try {
    const files = await findRuleFiles(base);
    if (!files.length) {
      if (!quiet) console.log("No files found in crates/oxc_linter/src/rules/.");
      return;
    }

    let changedAny = false;
    const editedFiles = [];
    for (const f of files) {
      try {
        const changed = await processFile(f, apply);
        if (changed) {
          changedAny = true;
          if (apply) editedFiles.push(f);
        }
      } catch (err) {
        console.error(`Error processing ${f}: ${err}`);
      }
    }

    if (!changedAny && !quiet)
      console.log("No matching serde attributes found that needed changes.");
    if (changedAny && !apply) console.log("\nRun with --apply to make the changes.");

    if (apply && editedFiles.length > 0) {
      if (!quiet) {
        console.log("\nEdited files:");
        for (const ef of editedFiles) console.log(`  - ${ef}`);
      }
    } else if (apply && !quiet) {
      console.log("\nNo files were changed.");
    }
  } catch (err) {
    console.error(err);
    process.exit(2);
  }
}

if (require.main === module) main();
```

`update-from-configuration.js`:

```js
#!/usr/bin/env node
/*
Update `from_configuration` implementations in rule files that include `deny_unknown_fields`.

Usage:
  node scripts/update_from_configuration.js [--apply] [--base PATH]

- Scans for Rust files under `crates/oxc_linter/src/rules`.
- For files that contain the token `deny_unknown_fields` and the exact
  `from_configuration` method form that uses `unwrap_or_default().into_inner()`,
  it replaces that method body with a shorter `map(DefaultRuleConfig::into_inner)`
  call as described in the repo's style.

Examples:
  # Preview changes (dry-run):
  node scripts/update_from_configuration.js

  # Apply changes in-place:
  node scripts/update_from_configuration.js --apply
*/

const fs = require("fs").promises;
const path = require("path");

async function findRuleFiles(base) {
  const results = [];
  const rulesDir = path.join(base, "crates", "oxc_linter", "src", "rules");
  try {
    const stat = await fs.stat(rulesDir);
    if (!stat.isDirectory()) return results;
  } catch (e) {
    return results;
  }

  async function walk(dir) {
    const entries = await fs.readdir(dir, { withFileTypes: true });
    for (const ent of entries) {
      const res = path.join(dir, ent.name);
      if (ent.isDirectory()) {
        if (ent.name === "node_modules" || ent.name === "target") continue;
        await walk(res);
      } else if (ent.isFile() && res.endsWith(".rs")) {
        results.push(res);
      }
    }
  }
  await walk(rulesDir);
  return results;
}

async function processFile(filePath, apply) {
  const text = await fs.readFile(filePath, "utf8");

  // Only handle files that mention deny_unknown_fields
  if (!text.includes("deny_unknown_fields")) return false;

  const FROM_CONF_SNIPPET = `fn from_configuration(value: serde_json::Value) -> Result<Self, serde_json::error::Error> {
        Ok(serde_json::from_value::<DefaultRuleConfig<Self>>(value)
            .unwrap_or_default()
            .into_inner())
    }`;

  const FROM_CONF_SNIPPET_ALT = `fn from_configuration(value: Value) -> Result<Self, serde_json::error::Error> {
        Ok(serde_json::from_value::<DefaultRuleConfig<Self>>(value)
            .unwrap_or_default()
            .into_inner())
    }`;

  const REPLACEMENT = `fn from_configuration(value: serde_json::Value) -> Result<Self, serde_json::error::Error> {
        serde_json::from_value::<DefaultRuleConfig<Self>>(value).map(DefaultRuleConfig::into_inner)
    }`;

  const REPLACEMENT_ALT = `fn from_configuration(value: Value) -> Result<Self, serde_json::error::Error> {
        serde_json::from_value::<DefaultRuleConfig<Self>>(value).map(DefaultRuleConfig::into_inner)
    }`;

  if (!text.includes(FROM_CONF_SNIPPET) && !text.includes(FROM_CONF_SNIPPET_ALT)) return false;

  let out = text;
  let changed = false;

  if (out.includes(FROM_CONF_SNIPPET)) {
    out = out.replace(FROM_CONF_SNIPPET, REPLACEMENT);
    changed = true;
    console.log(`Will update: ${filePath} - replace serde_json::Value form`);
  }

  if (out.includes(FROM_CONF_SNIPPET_ALT)) {
    out = out.replace(FROM_CONF_SNIPPET_ALT, REPLACEMENT_ALT);
    changed = true;
    console.log(`Will update: ${filePath} - replace Value form`);
  }

  if (changed && apply) {
    await fs.writeFile(filePath, out, "utf8");
    console.log(`Applied changes to ${filePath}\n`);
  }

  return changed;
}

async function main() {
  const args = process.argv.slice(2);
  const apply = args.includes("--apply");
  const quiet = args.includes("--quiet");
  const baseIndex = args.findIndex((a) => a === "--base");
  let base = process.cwd();
  if (baseIndex !== -1 && args.length > baseIndex + 1) base = path.resolve(args[baseIndex + 1]);

  try {
    const files = await findRuleFiles(base);
    if (!files.length) {
      if (!quiet) console.log("No files found in crates/oxc_linter/src/rules/.");
      return;
    }

    let changedAny = false;
    const editedFiles = [];
    for (const f of files) {
      try {
        const changed = await processFile(f, apply);
        if (changed) {
          changedAny = true;
          if (apply) editedFiles.push(f);
        }
      } catch (err) {
        console.error(`Error processing ${f}: ${err}`);
      }
    }

    if (!changedAny && !quiet)
      console.log("No matching from_configuration methods found that needed changes.");
    if (changedAny && !apply) console.log("\nRun with --apply to make the changes.");

    if (apply && editedFiles.length > 0) {
      if (!quiet) {
        console.log("\nEdited files:");
        for (const ef of editedFiles) console.log(`  - ${ef}`);
      }
    } else if (apply && !quiet) {
      console.log("\nNo files were changed.");
    }
  } catch (err) {
    console.error(err);
    process.exit(2);
  }
}

if (require.main === module) main();
```

</details>
@graphite-app graphite-app bot merged commit fc3c86b into main Jan 21, 2026
21 checks passed
@graphite-app graphite-app bot removed the 0-merge Merge with Graphite Merge Queue label Jan 21, 2026
@graphite-app graphite-app bot deleted the error-time branch January 21, 2026 07:01
overlookmotel pushed a commit that referenced this pull request Jan 26, 2026
# Oxlint
### 💥 BREAKING CHANGES

- 777fc40 ast: [**BREAKING**] Add `Ident` type (#18354) (Boshen)

### 🚀 Features

- 34c3ec3 linter/prefer-logical-operator-over-ternary: Implement fixer
(#18545) (camc314)
- 019e0aa linter/valid-typeof: Add suggestions if type is misspelled
(#18543) (camchenry)
- 704c8eb linter/use-isnan: Add more specific error message for
equality/inequality (#18542) (camchenry)
- 1e99ace linter/use-isnan: Support more `indexOf` cases and improve
diagnostic messages (#18537) (camchenry)
- bffd134 linter/text-encoding-identifier-case: Add `withDash` option
(#18533) (camc314)
- 993fd2b parser: Parse unambiguous await with better error messages
(#18480) (Boshen)
- b4b6247 linter/plugins: `RuleTester` support settings (#18445)
(overlookmotel)
- 15d69dc linter: Implement react/display-name rule (#18426) (camchenry)
- 2fbceae linter: Implement rule docs and config support for rules with
tuple config options. (#18372) (connorshea)
- 8db0e78 linter/plugins: Handle BOMs (#18376) (overlookmotel)
- 6ac09e2 linter/plugins: Support source text not being at start of
buffer (#18375) (overlookmotel)
- fc3c86b linter: Update 125 rules to raise errors when provided with
invalid config options. (#18104) (connorshea)
- 2cc6ad2 linter/plugins: Add `ecmaFeatures` to `parserOptions` (#18313)
(overlookmotel)

### 🐛 Bug Fixes

- 2acf568 linter/plugins: Keep `Infinity` in rule default options
(#18550) (overlookmotel)
- 332d2ef linter/plugins: Add `jsx` property to
`parserOptions.ecmaFeatures` (#18549) (overlookmotel)
- 7d9bb1b linter: Update `eslint/func-names` to error on invalid rule
config options, improve docs. (#18510) (connorshea)
- 9c67974 linter: Improve the jsx-a11y/no-noninteractive-tabindex rule
to match original rule logic better (#17848) (connorshea)
- 75e7163 vscode: Support json5 for oxfmt (#18502) (Sysix)
- c205b0d ast: Remove `ThisExpression` from `TSModuleReference` (#18489)
(Boshen)
- c51339a oxlint/lsp: Respect code action `source.fixAll` as an alias
for `source.fixAll.oxc` (#18366) (Sysix)
- 3c0e9b9 oxlint/lsp: Skip dangerous fixes/suggestions for "fix all"
code action and command (#18364) (Sysix)
- c44c093 linter: Fix behavior of unicorn/catch-error-name to match
original rule (#18209) (connorshea)
- 9c65aff linter/jsx-a11y: Change `no-autofocus` autofix to suggestion
(#18155) (Ben Lowery)
- 235c820 linter/unicorn: Fix `prefer-array-some` autofix for
`.filter().length` pattern (#18153) (Ben Lowery)
- a9925dc linter: Mark fixes in `unicorn/no-null` rule as dangerous.
(#18436) (connorshea)
- cee29b4 linter: Remove confusing scope from
`react/only-export-components` rule diagnostics. (#18434) (connorshea)
- aed3669 parser: Parse HTML-like comments in unambiguous mode (#18442)
(Boshen)
- b8a371d linter: Fix the path used in the gitlab format output (#18165)
(connorshea)
- e046ea6 linter: `vue/no-lifecycle-after-await` skip looking into arrow
functions (#18302) (Sysix)
- a9bfbcf linter: Compatibility issue with `DiagnosticData` type in
ESLint (#18396) (루밀LuMir)
- 10ab424 linter: `react/no_array_index_key` continue search for other
attributes (#18409) (Lonami)
- 9d776d4 linter: Update `import/no-cycle` rule to error on invalid
config options. (#18330) (connorshea)
- c163231 linter: Update eslint/sort-imports to validate options.
(#18378) (connorshea)
- 79bbcff linter: Update `eslint/func-style` to error on invalid
configuration options. (#18390) (connorshea)
- b871235 linter/plugins: Fix identifying "use strict" directives in
scope analysis (#18402) (overlookmotel)
- 5985141 linter: Update `jest/prefer-lowercase-title` rule to error on
invalid config options. (#18332) (connorshea)
- faca4b5 linter/plugins: Tokenize `let`, `static` and `yield` as
`Keyword`s (#18368) (overlookmotel)
- a3914fd linter/plugins: Allow line number passed to `report` to be 1
over line count (#18341) (overlookmotel)
- 88e0896 linter: Update `typescript/no-restricted-types` rule to error
on invalid config options. (#18329) (connorshea)
- 9eec600 linter: Update `react/jsx-fragments` rule to raise an error on
invalid configuration options (#18111) (connorshea)
- 0fa969d linter: Update `react/no-will-update-set-state` to error on
invalid config options (#18112) (connorshea)
- 70e7be4 linter: Update `import/no-unassigned-import` to raise an error
when passed invalid config options. (#18108) (connorshea)
- 496cac7 linter: Update `unicorn/explicit-length-check` to raise an
error when passed invalid config options. (#18107) (connorshea)
- 080b1ec linter: Update 5 more rules to error on invalid config
options. (#18113) (connorshea)
- c5d05dd linter: Update 11 rules to raise an error on invalid config
options. (#18109) (connorshea)
- 9e359d4 linter/plugins: Set all properties on global vars objects
(#18317) (overlookmotel)
- 39c7f32 linter/plugins: Set `writeable` flag on variables where
defined as globals (#18316) (overlookmotel)
- a570693 linter/plugins: Fix `CatchClause` scopes (#18312)
(overlookmotel)
- 8c98e69 linter: `vitest/prefer-describe-function-title`: Check earlier
to avoid false positive (#18177) (Jovi De Croock)
- 44be0eb linter/plugins: Set scope analyse settings based on source
type (#18306) (overlookmotel)
- b9a14fd vscode: Update package.json to restrict a few more config
options. (#18270) (Connor Shea)
- c1260cb vscode: Update version info formatting. (#18274) (connorshea)
- 2f68dc6 vscode: Update notification for client restart to specify
tool. (#18273) (connorshea)

### ⚡ Performance

- dc931ba linter/no-inner-declarations: Skip scope flags lookup in
modules (#18249) (overlookmotel)
- 07618a7 linter: Turn off `scope_build_child_ids` for SemanticBuilder
(#18360) (Dunqing)
- 1aac079 linter/exhaustive-deps: Simplify the logic of checking if the
identifier it is a dependency of hook (#18350) (Dunqing)
- 591d522 linter/block-scoped-var: Avoid `iter_all_scope_child_ids` by
walking references/redeclarations scope ancestors (#18335) (Dunqing)
- 2eefd6d linter/plugins: Remove branch from token parsing (#18369)
(overlookmotel)

### 📚 Documentation

- 698c21d linter: Modernize docs for various React rules (#18559)
(connorshea)
- 314a47c linter: Clarify the `no-find-dom-node` rule with a note that
the method was removed in React 19. (#18556) (connorshea)
- 5eff704 linter: Update `no-inner-declarations` to fix config option
docs (#18511) (connorshea)
- dd5d2f6 linter: Improve diagnostic message in `valid_typeof` rule.
(#18507) (connorshea)
- 8ccd853 npm: Update package homepage URLs and add keywords (#18509)
(Boshen)
- 4958233 linter: Add missing "What it does" section in
prefer-reflect-apply rule. (#18475) (connorshea)
- 2fa83a4 linter: Improve the docs for import/unambiguous. (#18474)
(connorshea)
- 7b1505c linter: Improve docs for `oxc/only-used-in-recursion` rule.
(#18473) (connorshea)
- ab506d6 linter/plugins: Correct comment (#18456) (overlookmotel)
- 4565c73 linter: `react/display-name`: add docs for config options
(#18430) (camchenry)
- b95a89f linter: Fix docs for the curly rule. (#18374) (connorshea)
- f675eb4 linter: Fix the `react/only-export-components` rule docs.
(#18319) (connorshea)
- 704db95 linter: "no-unused-vars" extend ignored files section for
svelte and astro files (#18304) (Sysix)
- 3af4a88 linter: Add "Examples" headers to rules missing them (#18266)
(connorshea)
# Oxfmt
### 💥 BREAKING CHANGES

- 777fc40 ast: [**BREAKING**] Add `Ident` type (#18354) (Boshen)

### 🚀 Features

- d71c15d oxfmt: Enable tailwind sort inside xxx-in-js (#18417)
(leaysgur)
- 52b5003 formatter,oxfmt: Support Angular `@Component({ template,
styles })` (#18324) (leaysgur)

### 🐛 Bug Fixes

- 224140c oxfmt: Canonicalize `..` component in config path (#18570)
(leaysgur)
- 30b467e formatter: Preserve trailing comments before the semicolon in
class methods without a body (#18446) (Dunqing)
- c205b0d ast: Remove `ThisExpression` from `TSModuleReference` (#18489)
(Boshen)
- 164bbd7 formatter: Preserve trailing comments inside ternary alternate
branch (#18433) (Dunqing)
- 1c50800 formatter: Use HTML entity escaping for JSX attribute strings
(#18385) (Boshen)
- 4e156d2 formatter: Preserve parentheses for `in` expressions in arrow
function block bodies (#18352) (Boshen)
- 7e6c15b oxfmt: Increase Tailwind CSS test timeout for Windows CI
(#18339) (Boshen)
- 29966eb formatter/dead-code-removal: Handle tailwind sorting (#18321)
(leaysgur)
- 29f41be formatter: Only expand mapped types when newline immediately
follows opening brace (#18087) (Boshen)
- 2194552 formatter: Relocate leading comments for single-element
union/intersection types (#18083) (Boshen)

### ⚡ Performance

- 85ab400 formatter: Store `AstNodes` itself instead of `&'a AstNodes`
as the `parent` field of `AstNode` (#18428) (Dunqing)
- 194d384 formatter: Reduce AstNode size by 8 bytes using
following_span_start (#18347) (Dunqing)
- b2df8fb oxfmt: Enable tailwind plugin only for relevant parser
(#18418) (leaysgur)

### 📚 Documentation

- 8ccd853 npm: Update package homepage URLs and add keywords (#18509)
(Boshen)

Co-authored-by: Boshen <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-cli Area - CLI A-linter Area - Linter C-enhancement Category - New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Comments