1- import { readFile } from 'node:fs/promises' ;
2- import { resolve } from 'node:path' ;
1+ import { readFile , readdir } from 'node:fs/promises' ;
2+ import { resolve , join } from 'node:path' ;
33
44const readAllImplementedRuleNames = async ( ) => {
55 const rulesFile = await readFile ( resolve ( 'crates/oxc_linter/src/rules.rs' ) , 'utf8' ) ;
@@ -38,6 +38,78 @@ const readAllImplementedRuleNames = async () => {
3838 throw new Error ( 'Failed to find the end of the rules list' ) ;
3939} ;
4040
41+ /**
42+ * Read all rule files and find rules with pending fixes.
43+ * A rule has a pending fix if it's declared with the `pending` keyword in its
44+ * declare_oxc_lint! macro, like: declare_oxc_lint!(RuleName, plugin, category, pending)
45+ */
46+ const readAllPendingFixRuleNames = async ( ) => {
47+ /** @type {Set<string> } */
48+ const pendingFixRules = new Set ( ) ;
49+
50+ const rulesDir = resolve ( 'crates/oxc_linter/src/rules' ) ;
51+
52+ /**
53+ * Recursively read all .rs files in a directory
54+ * @param {string } dir
55+ * @returns {Promise<string[]> }
56+ */
57+ const readRustFiles = async ( dir ) => {
58+ const entries = await readdir ( dir , { withFileTypes : true } ) ;
59+ const files = await Promise . all (
60+ entries . map ( ( entry ) => {
61+ const fullPath = join ( dir , entry . name ) ;
62+ if ( entry . isDirectory ( ) ) {
63+ return readRustFiles ( fullPath ) ;
64+ } else if ( entry . name . endsWith ( '.rs' ) && entry . name !== 'mod.rs' ) {
65+ return [ fullPath ] ;
66+ }
67+ return [ ] ;
68+ } ) ,
69+ ) ;
70+ return files . flat ( ) ;
71+ } ;
72+
73+ const ruleFiles = await readRustFiles ( rulesDir ) ;
74+
75+ for ( const filePath of ruleFiles ) {
76+ // oxlint-disable-next-line no-await-in-loop
77+ const content = await readFile ( filePath , 'utf8' ) ;
78+
79+ // Look for declare_oxc_lint! macro with pending fix
80+ // Pattern matches: declare_oxc_lint!( ... , pending, ... )
81+ // or: declare_oxc_lint!( ... , pending ) at the end
82+ const declareMacroMatch = content . match (
83+ / d e c l a r e _ o x c _ l i n t ! \s * \( \s * (?: \/ \/ \/ [ ^ \n ] * \n \s * ) * ( \w + ) \s * (?: \( t s g o l i n t \) ) ? \s * , \s * ( \w + ) \s * , \s * ( \w + ) \s * , \s * ( [ ^ ) ] + ) \) / s,
84+ ) ;
85+
86+ if ( declareMacroMatch ) {
87+ const [ , ruleName , plugin , , restParams ] = declareMacroMatch ;
88+
89+ // Check if 'pending' appears in the remaining parameters
90+ // It could be standalone or part of fix capabilities like "pending" or "fix = pending"
91+ if ( / \b p e n d i n g \b / . test ( restParams ) ) {
92+ // Convert Rust struct name to kebab-case rule name
93+ const kebabRuleName = ruleName
94+ . replace ( / ( [ a - z ] ) ( [ A - Z ] ) / g, '$1-$2' )
95+ . replace ( / ( [ A - Z ] + ) ( [ A - Z ] [ a - z ] ) / g, '$1-$2' )
96+ . toLowerCase ( ) ;
97+
98+ let prefixedName = `${ plugin } /${ kebabRuleName } ` ;
99+
100+ // Handle node -> n rename
101+ if ( prefixedName . startsWith ( 'node/' ) ) {
102+ prefixedName = prefixedName . replace ( / ^ n o d e / , 'n' ) ;
103+ }
104+
105+ pendingFixRules . add ( prefixedName ) ;
106+ }
107+ }
108+ }
109+
110+ return pendingFixRules ;
111+ } ;
112+
41113const NOT_SUPPORTED_RULE_NAMES = new Set ( [
42114 'eslint/no-dupe-args' , // superseded by strict mode
43115 'eslint/no-octal' , // superseded by strict mode
@@ -235,6 +307,7 @@ const NOT_SUPPORTED_RULE_NAMES = new Set([
235307 * isRecommended: boolean,
236308 * isImplemented: boolean,
237309 * isNotSupported: boolean,
310+ * isPendingFix: boolean,
238311 * }} RuleEntry
239312 * @typedef {Map<string, RuleEntry> } RuleEntries
240313 */
@@ -259,6 +332,7 @@ export const createRuleEntries = (loadedAllRules) => {
259332 // Will be updated later
260333 isImplemented : false ,
261334 isNotSupported : false ,
335+ isPendingFix : false ,
262336 } ) ;
263337 }
264338
@@ -284,6 +358,18 @@ export const updateNotSupportedStatus = (ruleEntries) => {
284358 }
285359} ;
286360
361+ /** @param {RuleEntries } ruleEntries */
362+ export const updatePendingFixStatus = async ( ruleEntries ) => {
363+ const pendingFixRuleNames = await readAllPendingFixRuleNames ( ) ;
364+
365+ for ( const name of pendingFixRuleNames ) {
366+ const rule = ruleEntries . get ( name ) ;
367+ if ( rule && rule . isImplemented ) {
368+ rule . isPendingFix = true ;
369+ }
370+ }
371+ } ;
372+
287373/**
288374 * @param {string } constName
289375 * @param {string } fileContent
@@ -337,6 +423,7 @@ export const overrideTypeScriptPluginStatusWithEslintPluginStatus = async (ruleE
337423 ruleEntries . set ( `typescript/${ rule } ` , {
338424 ...typescriptRuleEntry ,
339425 isImplemented : eslintRuleEntry . isImplemented ,
426+ isPendingFix : eslintRuleEntry . isPendingFix ,
340427 } ) ;
341428 }
342429 }
@@ -358,6 +445,7 @@ export const syncVitestPluginStatusWithJestPluginStatus = async (ruleEntries) =>
358445 ruleEntries . set ( `vitest/${ rule } ` , {
359446 ...vitestRuleEntry ,
360447 isImplemented : jestRuleEntry . isImplemented ,
448+ isPendingFix : jestRuleEntry . isPendingFix ,
361449 } ) ;
362450 }
363451 }
@@ -378,6 +466,7 @@ export const syncUnicornPluginStatusWithEslintPluginStatus = (ruleEntries) => {
378466 ruleEntries . set ( `unicorn/${ rule } ` , {
379467 ...unicornRuleEntry ,
380468 isImplemented : eslintRuleEntry . isImplemented ,
469+ isPendingFix : eslintRuleEntry . isPendingFix ,
381470 } ) ;
382471 }
383472 }
0 commit comments