Skip to content

Commit b8bf404

Browse files
authored
feat(tasks/lint-rules): track rules with pending fixes in automated issues (#15989)
Add functionality to detect and display linter rules that have pending auto-fixes in the automated GitHub tracking issues. - Add readAllPendingFixRuleNames() to scan rule files for `pending` keyword - Update RuleEntry type to include isPendingFix field - Add updatePendingFixStatus() to mark implemented rules with pending fixes - Update markdown renderer to show ⏳ emoji for pending fixes - Add pending fix counts to summary and table headers - Preserve isPendingFix status across plugin sync operations Fixes #15977
1 parent d185615 commit b8bf404

File tree

3 files changed

+109
-9
lines changed

3 files changed

+109
-9
lines changed

tasks/lint_rules/src/main.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
syncVitestPluginStatusWithJestPluginStatus,
99
updateImplementedStatus,
1010
updateNotSupportedStatus,
11+
updatePendingFixStatus,
1112
} from './oxlint-rules.mjs';
1213
import { updateGitHubIssue } from './result-reporter.mjs';
1314

@@ -58,6 +59,7 @@ void (async () => {
5859
const ruleEntries = createRuleEntries(linter.getRules());
5960
await updateImplementedStatus(ruleEntries);
6061
updateNotSupportedStatus(ruleEntries);
62+
await updatePendingFixStatus(ruleEntries);
6163
await syncTypeScriptPluginStatusWithEslintPluginStatus(ruleEntries);
6264
await syncVitestPluginStatusWithJestPluginStatus(ruleEntries);
6365
syncUnicornPluginStatusWithEslintPluginStatus(ruleEntries);

tasks/lint_rules/src/markdown-renderer.mjs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/**
22
* @typedef {({ name: string } & import("./oxlint-rules.mjs").RuleEntry)} RuleEntryView
3-
* @typedef {{ isImplemented: number; isNotSupported: number; total: number }} CounterView
3+
* @typedef {{ isImplemented: number; isNotSupported: number; isPendingFix: number; total: number }} CounterView
44
*/
55

66
/** @param {{ npm: string[]; }} props */
@@ -27,8 +27,10 @@ const renderCounters = ({ counters: { recommended, notRecommended, deprecated }
2727

2828
const countersList = [
2929
`- ${recommendedTodos}/${recommended.total} recommended rules are remaining as TODO`,
30+
recommended.isPendingFix > 0 && ` - ${recommended.isPendingFix} of which have pending fixes`,
3031
recommendedTodos === 0 && ` - All done! 🎉`,
3132
`- ${notRecommendedTodos}/${notRecommended.total} not recommended rules are remaining as TODO`,
33+
notRecommended.isPendingFix > 0 && ` - ${notRecommended.isPendingFix} of which have pending fixes`,
3234
notRecommendedTodos === 0 && ` - All done! 🎉`,
3335
]
3436
.filter(Boolean)
@@ -64,16 +66,22 @@ const renderRulesList = ({ title, counters, views, defaultOpen = true }) => `
6466
6567
<details ${defaultOpen ? 'open' : ''}>
6668
<summary>
67-
✨: ${counters.isImplemented}, 🚫: ${counters.isNotSupported} / total: ${counters.total}
69+
✨: ${counters.isImplemented}, 🚫: ${counters.isNotSupported}, ⏳: ${counters.isPendingFix} / total: ${counters.total}
6870
</summary>
6971
7072
| Status | Name | Docs |
7173
| :----: | :--- | :--- |
7274
${views
73-
.map((v) => `| ${v.isImplemented ? '✨' : ''}${v.isNotSupported ? '🚫' : ''} | ${v.name} | ${v.docsUrl} |`)
75+
.map((v) => {
76+
let status = '';
77+
if (v.isImplemented) status += '✨';
78+
if (v.isNotSupported) status += '🚫';
79+
if (v.isPendingFix) status += '⏳';
80+
return `| ${status} | ${v.name} | ${v.docsUrl} |`;
81+
})
7482
.join('\n')}
7583
76-
✨ = Implemented, 🚫 = No need to implement
84+
✨ = Implemented, 🚫 = No need to implement, ⏳ = Fix pending
7785
7886
</details>
7987
`;
@@ -91,9 +99,9 @@ export const renderMarkdown = (pluginName, pluginMeta, ruleEntries) => {
9199
notRecommended: [],
92100
};
93101
const counters = {
94-
deprecated: { isImplemented: 0, isNotSupported: 0, total: 0 },
95-
recommended: { isImplemented: 0, isNotSupported: 0, total: 0 },
96-
notRecommended: { isImplemented: 0, isNotSupported: 0, total: 0 },
102+
deprecated: { isImplemented: 0, isNotSupported: 0, isPendingFix: 0, total: 0 },
103+
recommended: { isImplemented: 0, isNotSupported: 0, isPendingFix: 0, total: 0 },
104+
notRecommended: { isImplemented: 0, isNotSupported: 0, isPendingFix: 0, total: 0 },
97105
};
98106

99107
for (const [name, entry] of ruleEntries) {
@@ -122,6 +130,7 @@ export const renderMarkdown = (pluginName, pluginMeta, ruleEntries) => {
122130

123131
if (entry.isImplemented) counterRef.isImplemented++;
124132
else if (entry.isNotSupported) counterRef.isNotSupported++;
133+
if (entry.isPendingFix && entry.isImplemented) counterRef.isPendingFix++;
125134
counterRef.total++;
126135
}
127136

tasks/lint_rules/src/oxlint-rules.mjs

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
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

44
const 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+
/declare_oxc_lint!\s*\(\s*(?:\/\/\/[^\n]*\n\s*)*(\w+)\s*(?:\(tsgolint\))?\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 (/\bpending\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(/^node/, 'n');
103+
}
104+
105+
pendingFixRules.add(prefixedName);
106+
}
107+
}
108+
}
109+
110+
return pendingFixRules;
111+
};
112+
41113
const 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

Comments
 (0)