Skip to content

Commit df5f8dd

Browse files
connorsheacamc314
andauthored
feat(linter): Add short descriptions to most lint rules. (#23365)
This adds short description fields using a codemod script and then reviewing the result (by myself and with Claude to find any possibly-weird ones and flag them for me) and then cleaning up some overly-long or weirdly-formatted rule short descriptions (by myself). This _does not_ cover all rules, as the codemod was not built to handle the shared jest/vitest rules or "What it does" sections that were any more complex than a single sentence. But it handles >75% of them. I'll work on the remainder in the next few days after we get this part merged. I've also made modifications/edits to various rule descriptions and short descriptions when I noticed as part of this work that they were overly verbose, inconsistent with the description format of most other rules, or grammatically incorrect. <details> <summary>Codemod script for reference</summary> ```js #!/usr/bin/env node // Insert a `short_description = "..."` argument into the // `declare_oxc_lint!` macro for every rule whose inline-doc "What it does" // section is a single sentence. // // Notes: // - Only handles rules with INLINE `///` doc comments. Shared-docs rules // (vitest/jest rules pulling from rules/shared) are intentionally skipped. // - Markdown links `[text](url)` are flattened to `text`. // - The insertion goes right after `version = "...",` so `short_description` // sits at the end of the macro arguments. Indentation mirrors `version`. // - `declare_oxc_lint!` must accept the `short_description = "..."` argument // (see crates/oxc_macros/src/declare_oxc_lint.rs); otherwise the generated // code will not compile. // // Usage: node insert_short_descriptions.mjs # dry-run, prints stats + sample diffs // node insert_short_descriptions.mjs --write # actually modify files // node insert_short_descriptions.mjs --show <substring> # preview generated insertions import { readFileSync, writeFileSync, readdirSync, statSync } from "node:fs"; import { join, relative } from "node:path"; const RULES_DIR = "~/code/oxc/crates/oxc_linter/src/rules"; const SHARED_DIR = join(RULES_DIR, "shared"); function walk(dir) { const out = []; for (const entry of readdirSync(dir)) { const p = join(dir, entry); const s = statSync(p); if (s.isDirectory()) out.push(...walk(p)); else if (entry.endsWith(".rs")) out.push(p); } return out; } // --- extraction (inline-only) -------------------------------------------- function ruleDocLines(source) { const out = []; for (const line of source.split("\n")) { const m = line.match(/^\s*\/\/\/ ?(.*)$/); if (m) out.push(m[1]); } return out; } function extractWhatItDoesFromMarkdown(lines) { let start = -1; for (let i = 0; i < lines.length; i++) { if (/^\s*###\s+What it does\s*$/i.test(lines[i])) { start = i + 1; break; } } if (start === -1) return null; const body = []; for (let i = start; i < lines.length; i++) { if (/^\s*###\s+/.test(lines[i])) break; body.push(lines[i]); } while (body.length && body[0].trim() === "") body.shift(); while (body.length && body[body.length - 1].trim() === "") body.pop(); return body; } // A "What it does" body that contains a markdown list or a URL can't be // faithfully reduced to a one-line sentence — flattening it produces a run-on // string (the whole list inlined) or a description that leads with a raw URL. // Skip these rather than emit garbage. function hasListOrUrl(bodyLines) { let inFence = false; for (const line of bodyLines) { if (/^\s*```/.test(line)) { inFence = !inFence; continue; } if (inFence) continue; if (/^\s*([-*+]|\d+\.)\s+/.test(line)) return true; // markdown list item if (/https?:\/\//.test(line)) return true; // bare/inline URL } return false; } function countSentences(bodyLines) { const kept = []; let inFence = false; for (const line of bodyLines) { if (/^\s*```/.test(line)) { inFence = !inFence; continue; } if (!inFence) kept.push(line); } let text = kept.join(" ").trim(); if (!text) return 0; text = text.replace(/`[^`]*`/g, "X"); text = text.replace(/\b(e\.g|i\.e|etc|vs|cf|approx|Mr|Mrs|Ms|Dr|Jr|Sr)\./gi, "$1"); text = text.replace(/(\d)\.(\d)/g, "$1$2"); const matches = text.match(/[.!?]+(?=\s|$)/g); return matches ? matches.length : 0; } // --- build short_description --------------------------------------------- function bodyToShortDescription(bodyLines) { const kept = []; let inFence = false; for (const line of bodyLines) { if (/^\s*```/.test(line)) { inFence = !inFence; continue; } if (!inFence) kept.push(line); } let text = kept.join(" ").replace(/\s+/g, " ").trim(); let prev; do { prev = text; text = text.replace(/\[([^\]]*)\]\(([^)]*)\)/g, "$1"); } while (text !== prev); return text; } function escapeForRustString(s) { return s.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); } // --- macro surgery ------------------------------------------------------- function findMacroSpan(source) { const idx = source.indexOf("declare_oxc_lint!"); if (idx === -1) return null; let i = idx + "declare_oxc_lint!".length; while (i < source.length && /\s/.test(source[i])) i++; if (source[i] !== "(") return null; const open = i; let depth = 1; i++; while (i < source.length && depth > 0) { const c = source[i]; if (c === '"') { i++; while (i < source.length && source[i] !== '"') { if (source[i] === "\\") i++; i++; } } else if (c === "/" && source[i + 1] === "/") { while (i < source.length && source[i] !== "\n") i++; } else if (c === "(") { depth++; } else if (c === ")") { depth--; if (depth === 0) return { open, close: i }; } i++; } return null; } function insertShortDescription(source, sentence) { const span = findMacroSpan(source); if (!span) return null; const inner = source.slice(span.open + 1, span.close); // Don't touch rules that already have the field. if (/\bshort_description\s*=/.test(inner)) return source; // Match the last `version = "..."` assignment. Last-match wins so we skip // any stray mentions in doc comments. The trailing comma (if any) is // detected separately below — folding `\s*,?` into this regex would let // `\s*` swallow the following newline when there is no comma, misplacing the // insertion point. const versionRe = /\bversion\s*=\s*"(?:[^"\\]|\\.)*"/g; let lastMatch = null; let vm; while ((vm = versionRe.exec(inner)) !== null) lastMatch = vm; if (!lastMatch) return null; const matchStartAbs = span.open + 1 + lastMatch.index; let matchEndAbs = matchStartAbs + lastMatch[0].length; // Consume an optional trailing comma that sits on the same line (after only // horizontal whitespace), leaving any newline untouched. let j = matchEndAbs; while (source[j] === " " || source[j] === "\t") j++; const hasTrailingComma = source[j] === ","; if (hasTrailingComma) matchEndAbs = j + 1; let lineStart = matchStartAbs; while (lineStart > 0 && source[lineStart - 1] !== "\n") lineStart--; const indent = source.slice(lineStart, matchStartAbs); const isOnOwnLine = /^\s*$/.test(indent); const escaped = escapeForRustString(sentence); let insertion; if (isOnOwnLine) { const prefix = hasTrailingComma ? "" : ","; insertion = `${prefix}\n${indent}short_description = "${escaped}",`; } else { const prefix = hasTrailingComma ? " " : ", "; insertion = `${prefix}short_description = "${escaped}",`; } return source.slice(0, matchEndAbs) + insertion + source.slice(matchEndAbs); } // --- main ---------------------------------------------------------------- const args = process.argv.slice(2); const flag = (n) => args.includes(n); const showIdx = args.indexOf("--show"); const showNeedle = showIdx >= 0 ? args[showIdx + 1] : null; const writeMode = flag("--write"); const files = walk(RULES_DIR).sort(); let processed = 0; let skippedShared = 0; let skippedMulti = 0; let skippedListOrUrl = 0; let skippedMissing = 0; let skippedAlreadyHas = 0; let skippedNoVersion = 0; let changed = 0; const samples = []; for (const file of files) { if (file.startsWith(SHARED_DIR + "/")) continue; const src = readFileSync(file, "utf8"); if (!/declare_oxc_lint!/.test(src)) continue; processed++; const docLines = ruleDocLines(src); const body = extractWhatItDoesFromMarkdown(docLines); if (body === null) { skippedShared++; continue; } if (body.length === 0) { skippedMissing++; continue; } if (hasListOrUrl(body)) { skippedListOrUrl++; continue; } const n = countSentences(body); if (n !== 1) { skippedMulti++; continue; } const sentence = bodyToShortDescription(body); if (!sentence) { skippedMissing++; continue; } if (/\bshort_description\s*=/.test(src)) { skippedAlreadyHas++; continue; } const updated = insertShortDescription(src, sentence); if (updated === null) { skippedNoVersion++; continue; } if (updated === src) { skippedAlreadyHas++; continue; } if (showNeedle && file.includes(showNeedle)) { samples.push({ file, sentence, updated }); } else if (samples.length < 3 && !showNeedle) { samples.push({ file, sentence, updated }); } if (writeMode) writeFileSync(file, updated); changed++; } console.log(`Rule files scanned: ${processed}`); console.log(`Would insert short_description: ${changed}`); console.log(`Skipped (shared-docs / no inline): ${skippedShared}`); console.log(`Skipped (multi-sentence / none): ${skippedMulti}`); console.log(`Skipped (list or URL body): ${skippedListOrUrl}`); console.log(`Skipped (empty/missing body): ${skippedMissing}`); console.log(`Skipped (already has field): ${skippedAlreadyHas}`); console.log(`Skipped (no version= found): ${skippedNoVersion}`); console.log(writeMode ? `\nWROTE ${changed} files.` : `\nDry run. Pass --write to apply.`); if (samples.length) { console.log("\nSamples:\n"); for (const s of samples) { console.log(`--- ${relative(RULES_DIR, s.file)} ---`); console.log(` short_description = "${escapeForRustString(s.sentence)}"`); const span = findMacroSpan(s.updated); if (span) { const chunk = s.updated.slice(span.open - 20, span.close + 2); console.log(""); for (const line of chunk.split("\n")) console.log(" " + line); } console.log(""); } } ``` </details> --------- Co-authored-by: Cameron Clark <[email protected]>
1 parent 40556ad commit df5f8dd

593 files changed

Lines changed: 845 additions & 261 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

crates/oxc_linter/src/rules/eslint/accessor_pairs.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ declare_oxc_lint!(
119119
pedantic,
120120
config = AccessorPairsConfig,
121121
version = "1.33.0",
122+
short_description = "Enforces getter/setter pairs in objects and classes.",
122123
);
123124

124125
impl Rule for AccessorPairs {

crates/oxc_linter/src/rules/eslint/array_callback_return/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ declare_oxc_lint!(
164164
pending,
165165
config = ArrayCallbackReturn,
166166
version = "0.0.3",
167+
short_description = "Enforce return statements in callbacks of array methods.",
167168
);
168169

169170
impl Rule for ArrayCallbackReturn {

crates/oxc_linter/src/rules/eslint/capitalized_comments.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ declare_oxc_lint!(
155155
fix,
156156
config = CapitalizedCommentsOptions,
157157
version = "1.34.0",
158+
short_description = "Enforces or disallows capitalization of the first letter of a comment.",
158159
);
159160

160161
impl Rule for CapitalizedComments {

crates/oxc_linter/src/rules/eslint/class_methods_use_this.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ declare_oxc_lint!(
130130
restriction,
131131
config = ClassMethodsUseThisConfig,
132132
version = "1.16.0",
133+
short_description = "Enforce that class methods utilize `this`.",
133134
);
134135

135136
impl Rule for ClassMethodsUseThis {

crates/oxc_linter/src/rules/eslint/complexity.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ declare_oxc_lint!(
148148
restriction,
149149
config = ComplexityConfig,
150150
version = "1.37.0",
151+
short_description = "Enforces a maximum cyclomatic complexity in a program, which is the number of linearly independent paths in a program.",
151152
);
152153

153154
impl Rule for Complexity {

crates/oxc_linter/src/rules/eslint/default_case.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ declare_oxc_lint!(
123123
restriction,
124124
config = DefaultCaseConfig,
125125
version = "0.4.0",
126+
short_description = "Enforces that all `switch` statements include a `default` case, unless explicitly marked with a configured comment.",
126127
);
127128

128129
impl Rule for DefaultCase {

crates/oxc_linter/src/rules/eslint/default_case_last.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ declare_oxc_lint!(
8282
eslint,
8383
style,
8484
version = "0.0.16",
85+
short_description = "Requires the `default` clause in `switch` statements to be the last one.",
8586
);
8687

8788
impl Rule for DefaultCaseLast {

crates/oxc_linter/src/rules/eslint/default_param_last.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ declare_oxc_lint!(
6969
eslint,
7070
style,
7171
version = "0.2.15",
72+
short_description = "Requires default parameters in functions to be the last ones.",
7273
);
7374

7475
impl Rule for DefaultParamLast {

crates/oxc_linter/src/rules/eslint/eqeqeq.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ declare_oxc_lint!(
176176
fix = conditional_fix_dangerous,
177177
config = Eqeqeq,
178178
version = "0.0.3",
179+
short_description = "Requires the use of the `===` and `!==` operators, disallowing the use of `==` and `!=`.",
179180
);
180181

181182
impl Eqeqeq {

crates/oxc_linter/src/rules/eslint/for_direction.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ declare_oxc_lint!(
8686
correctness,
8787
fix_dangerous,
8888
version = "0.0.3",
89+
short_description = "Disallow `for` loops where the update clause moves the counter in the wrong direction, preventing the loop from reaching its stop condition.",
8990
);
9091

9192
#[derive(Debug, Eq, PartialEq)]

0 commit comments

Comments
 (0)