Skip to content

Commit b06cf00

Browse files
authored
Linter CLI: Implement --disable-failing (#1485)
This pull request implements a new `--disable-failing` flag for the Linter CLI that lints the codebase and then disables all current rules that cause offenses by adding a disable rule entry in `.herb.yml`.
1 parent a2a31d4 commit b06cf00

File tree

4 files changed

+158
-4
lines changed

4 files changed

+158
-4
lines changed

javascript/packages/linter/src/cli.ts

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ export class CLI {
147147
const startTime = Date.now()
148148
const startDate = new Date()
149149

150-
const { patterns, configFile, formatOption, showTiming, theme, wrapLines, truncateLines, useGitHubActions, fix, fixUnsafe, ignoreDisableComments, force, init, upgrade, loadCustomRules, failLevel, jobs } = this.argumentParser.parse(process.argv)
150+
const { patterns, configFile, formatOption, showTiming, theme, wrapLines, truncateLines, useGitHubActions, fix, fixUnsafe, ignoreDisableComments, force, init, upgrade, disableFailing, loadCustomRules, failLevel, jobs } = this.argumentParser.parse(process.argv)
151151

152152
this.determineProjectPath(patterns)
153153

@@ -225,8 +225,11 @@ export class CLI {
225225
const upgradeProcessor = new FileProcessor()
226226
const results = await upgradeProcessor.processFiles(files, 'json', upgradeContext)
227227

228-
for (const [ruleName, data] of results.ruleOffenses) {
229-
ruleOffenseCounts.set(ruleName, data.count)
228+
for (const { offense } of results.allOffenses) {
229+
if (offense.severity !== "error" && offense.severity !== "warning") continue
230+
231+
const ruleName = offense.code || ""
232+
ruleOffenseCounts.set(ruleName, (ruleOffenseCounts.get(ruleName) || 0) + 1)
230233
}
231234

232235
rulesToDisable = skippedByVersion.filter(rule => ruleOffenseCounts.has(rule.ruleName))
@@ -281,6 +284,68 @@ export class CLI {
281284
process.exit(0)
282285
}
283286

287+
if (disableFailing) {
288+
const configPath = configFile || this.projectPath
289+
290+
if (!Config.exists(configPath)) {
291+
console.error(`\n✗ No .herb.yml found. Run ${colorize("herb-lint --init", "cyan")} first.\n`)
292+
process.exit(1)
293+
}
294+
295+
const config = await Config.load(configPath, { version, exitOnError: true, createIfMissing: false, silent: true })
296+
297+
console.log(`\n${colorize("↻", "cyan")} Linting codebase to find rules with offenses...`)
298+
299+
await Herb.load()
300+
301+
const files = await config.findFilesForTool('linter', this.projectPath)
302+
303+
const disableFailingContext: ProcessingContext = {
304+
projectPath: this.projectPath,
305+
config,
306+
jobs,
307+
}
308+
309+
const processor = new FileProcessor()
310+
const results = await processor.processFiles(files, 'json', disableFailingContext)
311+
const failingRules = new Map<string, number>()
312+
const PROTECTED_RULES = new Set(["parser-no-errors"])
313+
314+
for (const { offense } of results.allOffenses) {
315+
if (PROTECTED_RULES.has(offense.code || "")) continue
316+
if (offense.severity !== "error" && offense.severity !== "warning") continue
317+
318+
failingRules.set(offense.code || "", (failingRules.get(offense.code || "") || 0) + 1)
319+
}
320+
321+
if (failingRules.size === 0) {
322+
console.log(`\n${colorize("✓", "brightGreen")} No offenses found. All rules are passing!\n`)
323+
process.exit(0)
324+
}
325+
326+
const rulesMutation: Record<string, { enabled: boolean }> = {}
327+
328+
for (const ruleName of failingRules.keys()) {
329+
rulesMutation[ruleName] = { enabled: false }
330+
}
331+
332+
await Config.mutateConfigFile(config.path, {
333+
linter: { rules: rulesMutation }
334+
})
335+
336+
const totalOffenses = Array.from(failingRules.values()).reduce((sum, count) => sum + count, 0)
337+
const sortedRules = Array.from(failingRules.entries()).sort((a, b) => b[1] - a[1])
338+
339+
console.log(`\n${colorize("!", "yellow")} Found ${colorize(String(totalOffenses), "bold")} ${totalOffenses === 1 ? "offense" : "offenses"} across ${colorize(String(failingRules.size), "bold")} ${failingRules.size === 1 ? "rule" : "rules"}. Disabled in ${colorize(".herb.yml", "cyan")}:\n`)
340+
341+
for (const [ruleName, count] of sortedRules) {
342+
console.log(` ${colorize("✗", "red")} ${colorize(ruleName, "white")} ${colorize(`(${count} ${count === 1 ? "offense" : "offenses"})`, "gray")}`)
343+
}
344+
345+
console.log(`\n When you're ready, review the disabled rules in your ${colorize(".herb.yml", "cyan")} and re-enable them after fixing the offenses.\n`)
346+
process.exit(0)
347+
}
348+
284349
const silent = formatOption === 'json'
285350
const config = await Config.load(configFile || this.projectPath, { version, exitOnError: true, createIfMissing: false, silent })
286351
const linterConfig = config.options.linter || {}

javascript/packages/linter/src/cli/argument-parser.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export interface ParsedArguments {
2727
force: boolean
2828
init: boolean
2929
upgrade: boolean
30+
disableFailing: boolean
3031
loadCustomRules: boolean
3132
failLevel?: DiagnosticSeverity
3233
jobs: number
@@ -45,6 +46,7 @@ export class ArgumentParser {
4546
-v, --version show version
4647
--init create a .herb.yml configuration file in the current directory
4748
--upgrade update .herb.yml version and disable all newly introduced rules
49+
--disable-failing lint the codebase and disable all rules that have offenses in .herb.yml
4850
-c, --config-file <path> explicitly specify path to .herb.yml config file
4951
--force force linting even if disabled in .herb.yml
5052
--fix automatically fix auto-correctable offenses
@@ -74,6 +76,7 @@ export class ArgumentParser {
7476
version: { type: "boolean", short: "v" },
7577
init: { type: "boolean" },
7678
upgrade: { type: "boolean" },
79+
"disable-failing": { type: "boolean" },
7780
"config-file": { type: "string", short: "c" },
7881
force: { type: "boolean" },
7982
fix: { type: "boolean" },
@@ -159,6 +162,7 @@ export class ArgumentParser {
159162
const configFile = values["config-file"]
160163
const init = values.init || false
161164
const upgrade = values.upgrade || false
165+
const disableFailing = values["disable-failing"] || false
162166
const loadCustomRules = !values["no-custom-rules"]
163167

164168
let failLevel: DiagnosticSeverity | undefined
@@ -185,7 +189,7 @@ export class ArgumentParser {
185189
jobs = parsed
186190
}
187191

188-
return { patterns, configFile, formatOption, showTiming, theme, wrapLines, truncateLines, useGitHubActions, fix, fixUnsafe, ignoreDisableComments, force, init, upgrade, loadCustomRules, failLevel, jobs }
192+
return { patterns, configFile, formatOption, showTiming, theme, wrapLines, truncateLines, useGitHubActions, fix, fixUnsafe, ignoreDisableComments, force, init, upgrade, disableFailing, loadCustomRules, failLevel, jobs }
189193
}
190194

191195
private getFilePatterns(positionals: string[]): string[] {
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { describe, test, expect, beforeAll } from "vitest"
2+
import { Herb } from "@herb-tools/node-wasm"
3+
import { Linter } from "../src/linter.js"
4+
import { HTMLTagNameLowercaseRule } from "../src/rules/html-tag-name-lowercase.js"
5+
import { HTMLImgRequireAltRule } from "../src/rules/html-img-require-alt.js"
6+
import { HTMLNoDuplicateAttributesRule } from "../src/rules/html-no-duplicate-attributes.js"
7+
import { ParserNoErrorsRule } from "../src/rules/parser-no-errors.js"
8+
9+
import type { RuleClass } from "../src/types.js"
10+
11+
const PROTECTED_RULES = new Set(["parser-no-errors"])
12+
13+
function findFailingRules(ruleClasses: RuleClass[], source: string, fileName?: string) {
14+
const linter = new Linter(Herb, ruleClasses)
15+
const result = linter.lint(source, { fileName })
16+
const failingRules = new Map<string, number>()
17+
18+
for (const offense of result.offenses) {
19+
if (PROTECTED_RULES.has(offense.rule)) continue
20+
if (offense.severity !== "error" && offense.severity !== "warning") continue
21+
22+
failingRules.set(offense.rule, (failingRules.get(offense.rule) || 0) + 1)
23+
}
24+
25+
return failingRules
26+
}
27+
28+
describe("disable-failing", () => {
29+
beforeAll(async () => {
30+
await Herb.load()
31+
})
32+
33+
test("identifies rules with offenses", () => {
34+
const failingRules = findFailingRules(
35+
[HTMLTagNameLowercaseRule, HTMLImgRequireAltRule],
36+
'<DIV><img src="logo.png"></DIV>'
37+
)
38+
39+
expect(failingRules.has("html-tag-name-lowercase")).toBe(true)
40+
expect(failingRules.has("html-img-require-alt")).toBe(true)
41+
expect(failingRules.get("html-tag-name-lowercase")).toBe(2)
42+
expect(failingRules.get("html-img-require-alt")).toBe(1)
43+
})
44+
45+
test("returns empty map when no offenses", () => {
46+
const failingRules = findFailingRules(
47+
[HTMLTagNameLowercaseRule, HTMLImgRequireAltRule],
48+
'<div><img src="logo.png" alt="Logo"></div>'
49+
)
50+
51+
expect(failingRules.size).toBe(0)
52+
})
53+
54+
test("only includes rules that have offenses", () => {
55+
const failingRules = findFailingRules(
56+
[HTMLTagNameLowercaseRule, HTMLImgRequireAltRule, HTMLNoDuplicateAttributesRule],
57+
'<div><img src="logo.png"></div>'
58+
)
59+
60+
expect(failingRules.has("html-img-require-alt")).toBe(true)
61+
expect(failingRules.has("html-tag-name-lowercase")).toBe(false)
62+
expect(failingRules.has("html-no-duplicate-attributes")).toBe(false)
63+
})
64+
65+
test("parser-no-errors is excluded when source has parse errors", () => {
66+
const failingRules = findFailingRules(
67+
[ParserNoErrorsRule],
68+
'<div><span'
69+
)
70+
71+
expect(failingRules.has("parser-no-errors")).toBe(false)
72+
expect(failingRules.size).toBe(0)
73+
})
74+
75+
test("parser-no-errors is excluded but other rules still report", () => {
76+
const failingRules = findFailingRules(
77+
[HTMLImgRequireAltRule],
78+
'<img src="logo.png">'
79+
)
80+
81+
expect(failingRules.has("html-img-require-alt")).toBe(true)
82+
expect(failingRules.has("parser-no-errors")).toBe(false)
83+
})
84+
})

javascript/packages/linter/test/upgrade.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,4 +141,5 @@ describe("Smart upgrade", () => {
141141
expect(rulesToDisable).toHaveLength(0)
142142
})
143143
})
144+
144145
})

0 commit comments

Comments
 (0)