Skip to content

Comments

fix(linter): resolve import/extensions false positives and align with ESLint behavior#14602

Merged
camc314 merged 40 commits intooxc-project:mainfrom
taearls:linter/fix/import-extensions-false-positive
Dec 19, 2025
Merged

fix(linter): resolve import/extensions false positives and align with ESLint behavior#14602
camc314 merged 40 commits intooxc-project:mainfrom
taearls:linter/fix/import-extensions-false-positive

Conversation

@taearls
Copy link
Contributor

@taearls taearls commented Oct 14, 2025

Closes #12220 and #16701

This PR fixes multiple false positive scenarios in the import/extensions rule and adds significant improvements to make it more robust, performant, and ESLint-compatible.

More thorough PR description generated by Claude Code, reviewed by me:

Summary

This PR comprehensively fixes multiple false positive scenarios in the import/extensions linter rule and significantly improves ESLint compatibility, performance, and extensibility.


Bug Fixes

1. Unconfigured Extensions Now Ignored

Before:

// Configuration: { "js": "always" }
import styles from './styles.css';  // ERROR: Unexpected use of extension ".css"
import Component from './App.vue';  // ERROR: Unexpected use of extension ".vue"

After:

// Configuration: { "js": "always" }
import styles from './styles.css';  // OK - .css not configured, ignored
import Component from './App.vue';  // OK - .vue not configured, ignored

Impact: Eliminates false positives for framework-specific extensions (Vue, Svelte) and custom extensions (CSS, SCSS, etc.)


2. Root Package Name Detection Fixed

Before:

// Configuration: ["never"]
import foo from 'pkg.js';           // ERROR: Extension ".js" should not be used
import babel from '@babel/core.js'; // ERROR: Extension ".js" should not be used

After:

// Configuration: ["never"]
import foo from 'pkg.js';           // OK - 'pkg.js' is a package name, not a file
import babel from '@babel/core.js'; // OK - '@babel/core.js' is a package name

// But subpaths ARE validated:
import parser from '@babel/core/lib/parser.js'; // ERROR - this IS a file path

Impact: Correctly distinguishes between package names containing dots (part of the name) vs. file paths with extensions


3. Path Alias Detection Fixed

Before:

// Configuration: ["always"]
import Button from '@/components/Button';  // OK (incorrectly treated as @-scoped package)
import utils from '~/utils/helpers';       // OK (incorrectly ignored)

After:

// Configuration: ["always"]
import Button from '@/components/Button';     // ERROR: Missing file extension
import Button from '@/components/Button.jsx'; // OK

// Real scoped packages still work correctly:
import babel from '@babel/core';              // OK (actual package)
import types from '@types/node';              // OK (actual package)

Impact: Critical fix for Vue.js, React, Next.js, and Nuxt projects using path aliases like @/, ~/, or #/


4. ignorePackages Behavior Now Matches ESLint

Before:

  • Default: true
  • Behavior: Unclear, affected both "always" and "never" rules inconsistently

After:

  • Default: false (matches ESLint)
  • Behavior: Only affects "always" rule, has no effect on "never" rule
// Configuration: ["always", { "ignorePackages": true }]
import foo from 'lodash';        // OK - package exempted from "always"
import bar from './bar.js';      // OK - relative import requires extension

// Configuration: ["never", { "ignorePackages": true }]
import foo from 'lodash/fp.js';  // ERROR - ignorePackages doesn't affect "never"

New Features

Case-Insensitive Extension Matching

Before:

// Configuration: { "js": "always" }
import foo from './foo.JS';  // ERROR: Missing file extension (uppercase not recognized)

After:

// Configuration: { "js": "always" }
import foo from './foo.JS';  // OK - extensions normalized to lowercase
import bar from './bar.Ts';  // OK - .Ts matches "ts" config

Module Resolution Integration

Before: Only checked the string in the import statement

After: Uses ModuleRecord::resolved_absolute_path to validate against actual filesystem files

// File: ./utils.ts exists
import utils from './utils.js';  // Now detects mismatch between written ".js" and actual ".ts"

Benefits:

  • More accurate validation against actual files
  • Handles path alias resolution correctly
  • Better diagnostics for extension mismatches

Arbitrary Extension Support

Before: Only supported hardcoded extensions: js, jsx, ts, tsx, json

After: Supports ANY extension

// .eslintrc.json
{
  "rules": {
    "import/extensions": ["error", {
      "vue": "always",
      "svelte": "never",
      "mjs": "always",
      "cjs": "never",
      "css": "always",
      "scss": "always"
    }]
  }
}

Path Group Overrides (Bespoke Import Specifiers)

New option for custom import protocols used by monorepo tools and custom resolvers.

{
  "rules": {
    "import/extensions": ["error", "always", {
      "pathGroupOverrides": [
        { "pattern": "rootverse{*,*/**}", "action": "ignore" }
      ]
    }]
  }
}

Pattern Matching:

  • * - Match any characters except /
  • ** - Match any characters including / (recursive)
  • {a,b} - Match alternatives

Actions:

  • "enforce" - Apply normal extension validation
  • "ignore" - Skip all extension validation for matching imports
// With above config:
import { x } from 'rootverse+debug:src';       // OK - ignored by pathGroupOverrides
import { y } from 'rootverse+bfe:src/symbols'; // OK - ignored by pathGroupOverrides
import foo from './foo';                        // ERROR - still requires extension

Performance Optimizations

Zero-Copy FxHashMap Configuration

Before:

struct ExtensionsConfig {
    js: FileExtensionConfig,
    jsx: FileExtensionConfig,
    ts: FileExtensionConfig,
    tsx: FileExtensionConfig,
    json: FileExtensionConfig,
}

After:

#[repr(u8)]  // 1 byte per variant
pub enum ExtensionRule {
    Always = 0,
    Never = 1,
    IgnorePackages = 2,
}

struct ExtensionsConfig {
    extensions: FxHashMap<String, &'static ExtensionRule>,
}

Performance Characteristics:

  • O(1) lookups (~3-6ns for 500 entries)
  • Static instances in read-only memory (zero allocations)
  • Pointer equality for rule comparison
  • #[inline] attributes on hot paths

Configuration Changes

Default Behavior

Before: Pre-populated default rules for common extensions could cause false positives

After: Unconfigured extensions are ignored - no false positives by default

ignorePackages Default

Before: true

After: false (matches ESLint)

Configuration Formats Supported

// 1. Global rule only
["always"]
["never"]
["ignorePackages"]

// 2. Per-extension only (no global rule)
[{ "js": "always", "json": "never" }]

// 3. Combined
["always", { "js": "never", "vue": "always" }]

// 4. With options
["always", {
  "ignorePackages": true,
  "checkTypeImports": true,
  "pathGroupOverrides": [...]
}]

Testing

  • Added: 100+ new test cases covering edge cases
  • Removed: 21 redundant test cases
  • Result: All tests passing with zero regressions

Coverage Areas

  • Path aliases (@/, ~/, #/)
  • Scoped packages (@babel/core, @types/node)
  • Root package names with dots (pkg.js, @name/pkg.js)
  • Case-insensitive extensions (./foo.JS, ./bar.Ts)
  • Custom extensions (.vue, .mjs, .css)
  • Query strings and fragments
  • Unicode filenames
  • Monorepo protocols (bespoke import specifiers)

Real-World Validation

  • Tested against Mastodon repository
  • Reduced false positive errors from 169 to 120

Documentation Updates

  • Expanded rule documentation with configuration examples
  • Added "Known Limitations" section for package.json detection
  • Clarified ignorePackages semantics
  • Added pathGroupOverrides documentation
  • Added per-extension configuration examples

Migration Notes

Breaking Changes

  1. ignorePackages default changed from true to false

    If you relied on the old default, explicitly set it:

    ["always", { "ignorePackages": true }]
  2. ignorePackages no longer affects "never" rule

    This was undefined/inconsistent behavior before. Now it only exempts packages from the "always" rule.

Non-Breaking Improvements

  • Existing configurations continue to work
  • False positives reduced significantly
  • Better ESLint compatibility

@taearls taearls requested a review from camc314 as a code owner October 14, 2025 22:12
@github-actions github-actions bot added A-linter Area - Linter C-bug Category - Bug labels Oct 14, 2025
@taearls taearls force-pushed the linter/fix/import-extensions-false-positive branch from 4115709 to 6bf3602 Compare October 14, 2025 22:15
@codspeed-hq
Copy link

codspeed-hq bot commented Oct 14, 2025

CodSpeed Performance Report

Merging #14602 will not alter performance

Comparing taearls:linter/fix/import-extensions-false-positive (b3fbe82) with main (c897794)1

Summary

✅ 4 untouched
⏩ 41 skipped2

Footnotes

  1. No successful run was found on main (d77e22d) during the generation of this report, so c897794 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

  2. 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.

@camc314 camc314 self-assigned this Oct 15, 2025
@taearls taearls force-pushed the linter/fix/import-extensions-false-positive branch from 0f575f0 to ec5b0e7 Compare October 20, 2025 15:15
Copy link
Contributor

@camc314 camc314 left a comment

Choose a reason for hiding this comment

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

Would it be better to use use the module record for this.

code like:

    // Check for path aliases with single-char prefix followed by /
    // Examples: @/, ~/, #/
    if module_name.len() >= 2 {
        let chars: Vec<char> = module_name.chars().collect();
        if chars.len() >= 2 && chars[1] == '/' {
            // Single character before slash - this is a path alias
            return false;
        }
    }

feels flaky

@taearls
Copy link
Contributor Author

taearls commented Oct 23, 2025

Would it be better to use use the module record for this.

code like:

    // Check for path aliases with single-char prefix followed by /
    // Examples: @/, ~/, #/
    if module_name.len() >= 2 {
        let chars: Vec<char> = module_name.chars().collect();
        if chars.len() >= 2 && chars[1] == '/' {
            // Single character before slash - this is a path alias
            return false;
        }
    }

feels flaky

yeah that makes sense. I'll work on refactoring this to use the module record.

Update:

after looking at it a bit more, I couldn't find any additional contextual information in the module record (aside from the module's import name that is already being used) that I could leverage to my benefit. I did update the helper function this snippet is in to be a little cleaner and more clear with its intentions.

as far as I can tell, the linter doesn't support custom resolver configurations in tsconfig.json yet, but I think this fixes the specific bugs that were reported in the issue. there are still some gaps though I believe. any custom aliases or typescript paths that are configured in a user's tsconfig.json will not be respected yet.

however, I think adding full resolver config support deserves its own dedicated issue / PR because the filesystem lookups to resolve the tsconfig.json (and to safely handle if it doesn't exist) would have performance implications.

maybe I'm missing something though. let me know if you have any suggestions in terms of how I can use the module record better here

@connorshea
Copy link
Member

connorshea commented Nov 9, 2025

This MR does fix various issues in the reproduction repo here.

The only things it doesn't fix are main4.js and main5.js but we can try to handle those in a different PR.

main4.js should fail because I have the config set as "import/extensions": ["error", "always", { "ignorePackages": true }], and main4 has import foo from "./foo"; in it. Weirdly, if it doesn't have the console.log(foo) line after the import, it does trigger the lint error. So that's weird.

linting output on the repro repo for eslint vs main vs this branch

oxlint 1.26.0 (3 errors, and 1 error missing that is expected, based on what ESLint does):

connorshea@Connors-MacBook-Pro oxlint-extensions-reproduction % pnpm oxlint

  × eslint-plugin-import(extensions): File extension "JS" should not be included in the import declaration.
   ╭─[main5.js:3:1]
 2 │ // ESLint passes on this, oxlint does not currently.
 3 │ import foo from './foo.JS';
   · ───────────────────────────
 4 │ 
   ╰────
  help: Remove the file extension from this import.

  × eslint-plugin-import(extensions): Missing file extension in import declaration
   ╭─[main1.js:4:1]
 3 │ // We have `ignorePackages` set to true, so this should not error.
 4 │ import { defineConfig } from "vitest/config";
   · ─────────────────────────────────────────────
 5 │ 
   ╰────
  help: Add a file extension to this import.

  × eslint-plugin-import(extensions): Missing file extension in import declaration
   ╭─[main2.js:3:1]
 2 │ // This behavior is not correct because I have `ignorePackages` set to true, so it should be... ignored.
 3 │ import { defineConfig } from "vitest/config";
   · ─────────────────────────────────────────────
 4 │ 
   ╰────
  help: Add a file extension to this import.

Found 0 warnings and 3 errors.
Finished in 49ms on 7 files using 8 threads.

This PR (after being rebased on top of main, 1 error but it's different from ESLint's error):

connorshea@Connors-MacBook-Pro oxlint % node dist/cli.js -c ../../../oxlint-extensions-reproduction/.oxlintrc.json ~/code/oxlint-extensions-reproduction/

  × eslint-plugin-import(extensions): File extension "JS" should not be included in the import declaration.
   ╭─[/Users/connorshea/code/oxlint-extensions-reproduction/main5.js:3:1]
 2 │ // ESLint passes on this, oxlint does not currently.
 3 │ import foo from './foo.JS';
   · ───────────────────────────
 4 │ 
   ╰────
  help: Remove the file extension from this import.

Found 0 warnings and 1 error.
Finished in 324ms on 7 files with 1 rules using 8 threads.

ESLint (1 error):

connorshea@Connors-MacBook-Pro oxlint-extensions-reproduction % pnpm eslint

/Users/connorshea/code/oxlint-extensions-reproduction/main4.js
  5:17  error  Missing file extension "js" for "./foo"  import/extensions

✖ 1 problem (1 error, 0 warnings)

Aside on main5.js:

I'm not sure if we should necessarily downcase the extension filename to fix main5,js there? And technically the difference may be entirely based on eslint allowing any extensions not-defined-by-the-config by default, while oxlint doesn't? So if it's because 'JS' is technically a distinct extension from 'js', then maybe we would fix this behavior in a different way. 🤷

Regardless, I don't think that edge case needs to be fixed in this MR. I do think we should consider adding a bunch of fixtures for this rule, to confirm various complex behaviors, but that can also be done in a separate PR imo.

@connorshea
Copy link
Member

This definitely seems to improve the number of discrepancies between oxlint and ESLint, based on testing it on the Mastodon repo.

1.26.0 on mastodon: 169 errors
this PR (after rebase) on mastodon: 120 errors

However, I think more work will need to be done to fix further issues with this rule, as things like this are showing up as violations still after this PR, despite not being violations (config is always at the top-level, then never for importing tsx files):

x eslint-plugin-import(extensions): Missing file extension in import declaration
   ,-[app/javascript/mastodon/features/emoji/emoji_picker.tsx:7:1]
 6 | 
 7 | import { EMOJI_MODE_NATIVE } from './constants';
   : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 8 | import EmojiData from './emoji_data.json';
   `----
  help: Add a file extension to this import.

See emoji_picker.tsx. The constants file being imported is at app/javascript/mastodon/features/emoji/constants.ts, and so should be enforced to never have an extension. Yet the rule still enforces it.

I'm wondering if this rule actually correctly has the ability to check the source file's type when it's being imported?

The .oxlintrc.json in mastodon I'm testing with is this, if anyone wants to reproduce this behavior:

oxlintrc.json
{
  "$schema": "./node_modules/oxlint/configuration_schema.json",
  "plugins": [
    "import"
  ],
  "categories": {
    "correctness": "off"
  },
  "env": {
    "builtin": true,
    "es2021": true,
    "browser": true,
    "shared-node-browser": true
  },
  "rules": {
    "import/extensions": [
      "error",
      "always",
      {
        "js": "never",
        "jsx": "never",
        "ts": "never",
        "tsx": "never"
      }
    ]
  },
  "ignorePatterns": [
    "build/**/*",
    "coverage/**/*",
    "db/**/*",
    "lib/**/*",
    "log/**/*",
    "node_modules/**/*",
    "public/**/*",
    "!public/embed.js",
    "spec/**/*",
    "tmp/**/*",
    "vendor/**/*",
    "streaming/**/*",
    ".bundle/**/*",
    "storybook-static/**/*"
  ]
}

@taearls
Copy link
Contributor Author

taearls commented Nov 10, 2025

This definitely seems to improve the number of discrepancies between oxlint and ESLint, based on testing it on the Mastodon repo.

1.26.0 on mastodon: 169 errors
this PR (after rebase) on mastodon: 120 errors

However, I think more work will need to be done to fix further issues with this rule, as things like this are showing up as violations still after this PR, despite not being violations (config is always at the top-level, then never for importing tsx files):

x eslint-plugin-import(extensions): Missing file extension in import declaration
   ,-[app/javascript/mastodon/features/emoji/emoji_picker.tsx:7:1]
 6 | 
 7 | import { EMOJI_MODE_NATIVE } from './constants';
   : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 8 | import EmojiData from './emoji_data.json';
   `----
  help: Add a file extension to this import.

See emoji_picker.tsx. The constants file being imported is at app/javascript/mastodon/features/emoji/constants.ts, and so should be enforced to never have an extension. Yet the rule still enforces it.

I'm wondering if this rule actually correctly has the ability to check the source file's type when it's being imported?

The .oxlintrc.json in mastodon I'm testing with is this, if anyone wants to reproduce this behavior:

oxlintrc.json
{
  "$schema": "./node_modules/oxlint/configuration_schema.json",
  "plugins": [
    "import"
  ],
  "categories": {
    "correctness": "off"
  },
  "env": {
    "builtin": true,
    "es2021": true,
    "browser": true,
    "shared-node-browser": true
  },
  "rules": {
    "import/extensions": [
      "error",
      "always",
      {
        "js": "never",
        "jsx": "never",
        "ts": "never",
        "tsx": "never"
      }
    ]
  },
  "ignorePatterns": [
    "build/**/*",
    "coverage/**/*",
    "db/**/*",
    "lib/**/*",
    "log/**/*",
    "node_modules/**/*",
    "public/**/*",
    "!public/embed.js",
    "spec/**/*",
    "tmp/**/*",
    "vendor/**/*",
    "streaming/**/*",
    ".bundle/**/*",
    "storybook-static/**/*"
  ]
}

Good to know! Thanks for the detailed report. I can take a look this week into this further, but I'll probably do it in a separate development branch because I suspect it will be a more significant refactor to get all those things working.

I don't think the file extension parsing is working how we would expect. When I initially implemented this rule, I basically just finished once I got all the tests to pass, but there have been a few problems with this rule.

I agree that we should add more testing to verify the behavior. It deserves a closer look for sure.

@connorshea
Copy link
Member

See also #15009, not sure which is the best to go with as the solution here but I definitely think we need to prioritize fixing this rule.

@taearls
Copy link
Contributor Author

taearls commented Nov 14, 2025

I'm not sure either. From the discord conversation I'm also working on a separate branch that I was planning to point to this one when it's ready that will support custom extensions (i.e., not hard coded ones).

taearls added a commit to taearls/oxc that referenced this pull request Nov 15, 2025
File extensions are now normalized to lowercase for case-insensitive matching,
resolving the edge case where `./foo.JS` was incorrectly flagged.

**Changes:**
- Normalize extensions to lowercase in `get_file_extension_from_module_name()`
- Normalize extensions to lowercase in `get_resolved_extension()`
- Add test cases for uppercase and mixed-case extensions

**Fixes:**
- ✅ `./foo.JS` now correctly matches `{ "js": "always" }` config
- ✅ `./foo.Ts` now correctly matches `{ "ts": "never" }` config
- ✅ Case variations (JS, Js, js) all treated identically

Addresses the main5.js edge case from oxc-project#14602 testing.

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

Co-Authored-By: Claude <noreply@anthropic.com>
taearls added a commit to taearls/oxc that referenced this pull request Nov 15, 2025
File extensions are now normalized to lowercase for case-insensitive matching,
resolving the edge case where `./foo.JS` was incorrectly flagged.

**Changes:**
- Normalize extensions to lowercase in `get_file_extension_from_module_name()`
- Normalize extensions to lowercase in `get_resolved_extension()`
- Add test cases for uppercase and mixed-case extensions

**Fixes:**
- ✅ `./foo.JS` now correctly matches `{ "js": "always" }` config
- ✅ `./foo.Ts` now correctly matches `{ "ts": "never" }` config
- ✅ Case variations (JS, Js, js) all treated identically

Addresses the main5.js edge case from oxc-project#14602 testing.

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

Co-Authored-By: Claude <noreply@anthropic.com>
@taearls taearls force-pushed the linter/fix/import-extensions-false-positive branch from 229407b to 6fa1ba6 Compare November 15, 2025 21:06
taearls added a commit to taearls/oxc that referenced this pull request Nov 15, 2025
File extensions are now normalized to lowercase for case-insensitive matching,
resolving the edge case where `./foo.JS` was incorrectly flagged.

**Changes:**
- Normalize extensions to lowercase in `get_file_extension_from_module_name()`
- Normalize extensions to lowercase in `get_resolved_extension()`
- Add test cases for uppercase and mixed-case extensions

**Fixes:**
- ✅ `./foo.JS` now correctly matches `{ "js": "always" }` config
- ✅ `./foo.Ts` now correctly matches `{ "ts": "never" }` config
- ✅ Case variations (JS, Js, js) all treated identically

Addresses the main5.js edge case from oxc-project#14602 testing.

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

Co-Authored-By: Claude <noreply@anthropic.com>
@taearls
Copy link
Contributor Author

taearls commented Nov 15, 2025

I discussed and tested this rule with @connorshea on discord and pushed up some changes that will significantly improve the behavior of this rule significantly.

the key highlights:

  • I updated the parsing logic so that if a file extension is not included in the configuration, and the global setting is NOT never, then this rule will not emit diagnostics about files with that extension. this makes it so users get custom file extension support without needing to manually configure potentially dozens of file extensions in their config. Users can incrementally flag what they want and have fine control over what extensions are allowed.

  • the parsing logic uses the ModuleRecord struct rather than string matching against the source code. this means that we'll be able to check against the actual file extension in in the filesystem rather than simply performing static analysis on source code.

  • I removed the hard coded file extension configurations and am using an FxHashMap to build up a mapping based on the user's configuration.

  • I used bitflags to denote always | never | ignorePackages so that the enum is memory efficient.

  • added support for the pathGroupOverrides configuration option

I'm planning to review it more thoroughly over the next couple of days to make sure everything looks good and that I didn't miss anything, but meanwhile I think it's in a good enough place for review.

@github-actions github-actions bot added the A-codegen Area - Code Generation label Nov 15, 2025
@taearls taearls changed the title fix(linter): fix false positives reported when analyzing package imports fix(linter): resolve import/extensions false positives and align with ESLint behavior Nov 16, 2025
@taearls
Copy link
Contributor Author

taearls commented Nov 18, 2025

I discussed and tested this rule with @connorshea on discord and pushed up some changes that will significantly improve the behavior of this rule significantly.

the key highlights:

  • I updated the parsing logic so that if a file extension is not included in the configuration, and the global setting is NOT never, then this rule will not emit diagnostics about files with that extension. this makes it so users get custom file extension support without needing to manually configure potentially dozens of file extensions in their config. Users can incrementally flag what they want and have fine control over what extensions are allowed.
  • the parsing logic uses the ModuleRecord struct rather than string matching against the source code. this means that we'll be able to check against the actual file extension in in the filesystem rather than simply performing static analysis on source code.
  • I removed the hard coded file extension configurations and am using an FxHashMap to build up a mapping based on the user's configuration.
  • I used bitflags to denote always | never | ignorePackages so that the enum is memory efficient.
  • added support for the pathGroupOverrides configuration option

I'm planning to review it more thoroughly over the next couple of days to make sure everything looks good and that I didn't miss anything, but meanwhile I think it's in a good enough place for review.

I'm feeling good about these changes after giving them a quick review and updating the PR description.

@taearls
Copy link
Contributor Author

taearls commented Dec 13, 2025

I believe I've addressed all the feedback on this PR. I was able to simplify the implementation a decent amount and verify that it still behaves the same against the rule's testing and against mastodon's codebase.

I think this is in a good place for a re-review when you get a chance @camchenry @camc314

@connorshea
Copy link
Member

Rebuilt the latest version of this branch and ran it on the Mastodon codebase, confirmed it still works well :)

Before, with oxlint 1.32.0 (--type-aware --report-unused-disable-directives):

Found 33 warnings and 152 errors.

After, with this PR (--type-aware --report-unused-disable-directives):

Found 33 warnings and 12 errors.

(any differences in the starting #s vs the last comment I made are generally due to the --report-unused-disable-directives and fixes I've been working on for the @oxlint/migrate package)

Copy link
Member

@connorshea connorshea left a comment

Choose a reason for hiding this comment

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

From a docs/"does the rule work" perspective, LGTM. Will let cam or cam review and approve the actual rust code.

Copy link
Member

@camchenry camchenry left a comment

Choose a reason for hiding this comment

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

I think this looks pretty great! I'm not too familiar with the original rule, but I've read over the logic here and I think it makes sense.

I've left a number of comments and I would make some of these changes myself but I'm not currently at my laptop to do so. But hopefully this is also instructive for future PRs for the types of things I'm thinking about (and AI tends to miss on). I don't think any of the comments are particularly time consuming though.

I also left a few follow-up comments that are non-blocking and just notes for future PRs on top of this work.

Overall, I'm excited to get this merged in, as this is a big improvement to this rule. Since this is an existing rule with issues, I don't want to let perfection get in the way of good enough, so I'm in favor of these changes after feedback if @camc314 is good with it too.

taearls and others added 12 commits December 15, 2025 11:34
Move ignorePackages and pathGroupOverrides documentation from the
struct-level doc comment to individual field doc comments for better
discoverability. Also removes the redundant default value comment
since the Default derive already expresses this.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add #[serde(skip)] attribute to the require_extension field since
it is not user-configurable from the config object. This prevents
it from appearing in the generated documentation as a supported
config option.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Restore the missing doc comment for the check_type_imports field
that describes its purpose in enforcing extension rules for type
imports.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add json to the has_any_never_rules() method for consistency with
is_standard_extension() which already includes json. This ensures
proper lenient behavior when json files have "never" rules configured.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Remove the redundant `!call_expr.arguments.is_empty()` check before
iterating over arguments. Iterating over an empty iterator is a no-op
and this check adds unnecessary complexity.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…value

Move the standalone build_config function to be a static method of
ExtensionsConfig named from_json_value. This is more idiomatic Rust
and improves code organization by keeping related functionality
together.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Reorder the condition checks to put the cheapest operations first:
1. is_none() - simple option check
2. is_standard_extension() - matches! macro
3. has_rule() - hash table lookup

This optimization reduces unnecessary work when the early conditions
short-circuit the evaluation.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Inline the is_path_group_action_enforce check into the conditional
instead of computing it as a separate variable. This defers the
matches! call until it's actually needed in the condition.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…mentation

Move require() call processing from run_once to a dedicated fn run
implementation. This enables linter framework optimizations to filter
nodes by type and only invoke the rule for CallExpression nodes.

- Add fn run() to handle require() calls
- Keep run_once() for module record iteration
- Regenerate rule_runner_impls.rs with updated NODE_TYPES and RUN_FUNCTIONS

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Simplify string handling by removing unnecessary CompactStr conversions.
The code was converting &str → CompactStr → &str which added overhead
without benefit.

- Change get_file_extension_from_module_name to take &str, return Option<String>
- Change validate_extension to take Option<&str> for written_extension
- Change extension_should_not_be_included_in_diagnostic to take &str
- Remove unused oxc_span::CompactStr import

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add early return when global require_extension is Never - this skips
up to 7 hash lookups since all extensions default to Never anyway.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…ction

Replace multiple bounds checks with a single slice pattern match.
This reduces potential bounds checking overhead when detecting
single-character path aliases like ~/ or #/.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@taearls
Copy link
Contributor Author

taearls commented Dec 15, 2025

I think this looks pretty great! I'm not too familiar with the original rule, but I've read over the logic here and I think it makes sense.

I've left a number of comments and I would make some of these changes myself but I'm not currently at my laptop to do so. But hopefully this is also instructive for future PRs for the types of things I'm thinking about (and AI tends to miss on). I don't think any of the comments are particularly time consuming though.

I also left a few follow-up comments that are non-blocking and just notes for future PRs on top of this work.

Overall, I'm excited to get this merged in, as this is a big improvement to this rule. Since this is an existing rule with issues, I don't want to let perfection get in the way of good enough, so I'm in favor of these changes after feedback if @camc314 is good with it too.

I appreciate your thoughtful feedback! It's helpful to get more insight into what the best practices are for these linting rules and what anti patterns you're checking for. I'll definitely keep that in mind for future contributions.

I pushed up commits to address each comment of feedback. Everything still works as before, but everything should be addressed now. I'm hoping that separate commits for each improvement will make it easier to review each of the changes individually.

@taearls
Copy link
Contributor Author

taearls commented Dec 19, 2025

@camc314 when you get a chance can I get a review on this? I think it's good to go. Merging this will close 3 open issues.

@camc314 camc314 merged commit 2c45017 into oxc-project:main Dec 19, 2025
20 checks passed
@sebastienbarre

This comment was marked as resolved.

@connorshea
Copy link
Member

connorshea commented Dec 20, 2025

Improved the docs a bit more here #17162, we'll need to update them again in the nearish future once we have proper tuple config support.

Thanks for the work on this @taearls!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-codegen Area - Code Generation A-linter Area - Linter C-bug Category - Bug

Projects

None yet

6 participants