Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion javascript/packages/linter/src/cli/file-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface ProcessingContext {
export interface ProcessingResult {
totalErrors: number
totalWarnings: number
totalIgnored: number
filesWithOffenses: number
filesFixed: number
ruleCount: number
Expand Down Expand Up @@ -52,6 +53,7 @@ export class FileProcessor {
async processFiles(files: string[], formatOption: FormatOption = 'detailed', context?: ProcessingContext): Promise<ProcessingResult> {
let totalErrors = 0
let totalWarnings = 0
let totalIgnored = 0
let filesWithOffenses = 0
let filesFixed = 0
let ruleCount = 0
Expand Down Expand Up @@ -149,8 +151,9 @@ export class FileProcessor {
totalWarnings += lintResult.warnings
filesWithOffenses++
}
totalIgnored += lintResult.ignored
}

return { totalErrors, totalWarnings, filesWithOffenses, filesFixed, ruleCount, allOffenses, ruleOffenses, context }
return { totalErrors, totalWarnings, totalIgnored, filesWithOffenses, filesFixed, ruleCount, allOffenses, ruleOffenses, context }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface JSONSummary {
filesWithOffenses: number
totalErrors: number
totalWarnings: number
totalIgnored: number
totalOffenses: number
ruleCount: number
}
Expand All @@ -34,6 +35,7 @@ interface JSONFormatOptions {
files: string[]
totalErrors: number
totalWarnings: number
totalIgnored: number
filesWithOffenses: number
ruleCount: number
startTime: number
Expand Down Expand Up @@ -79,6 +81,7 @@ export class JSONFormatter extends BaseFormatter {
filesWithOffenses: options.filesWithOffenses,
totalErrors: options.totalErrors,
totalWarnings: options.totalWarnings,
totalIgnored: options.totalIgnored,
totalOffenses: options.totalErrors + options.totalWarnings,
ruleCount: options.ruleCount
}
Expand Down
6 changes: 5 additions & 1 deletion javascript/packages/linter/src/cli/output-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export class OutputManager {
* Output successful lint results
*/
async outputResults(results: LintResults, options: OutputOptions): Promise<void> {
const { allOffenses, files, totalErrors, totalWarnings, filesWithOffenses, ruleCount, ruleOffenses } = results
const { allOffenses, files, totalErrors, totalWarnings, totalIgnored, filesWithOffenses, ruleCount, ruleOffenses } = results

const autofixableCount = allOffenses.filter(offense => offense.autocorrectable).length

Expand All @@ -47,6 +47,7 @@ export class OutputManager {
files,
totalErrors,
totalWarnings,
totalIgnored,
filesWithOffenses,
ruleCount,
startTime: options.startTime,
Expand All @@ -71,6 +72,7 @@ export class OutputManager {
filesWithOffenses,
totalErrors,
totalWarnings,
totalIgnored,
totalOffenses: totalErrors + totalWarnings,
ruleCount
},
Expand Down Expand Up @@ -99,6 +101,7 @@ export class OutputManager {
files,
totalErrors,
totalWarnings,
totalIgnored,
filesWithOffenses,
ruleCount,
startTime: options.startTime,
Expand All @@ -124,6 +127,7 @@ export class OutputManager {
filesWithOffenses: 0,
totalErrors: 0,
totalWarnings: 0,
totalIgnored: 0,
totalOffenses: 0,
ruleCount: 0
},
Expand Down
7 changes: 6 additions & 1 deletion javascript/packages/linter/src/cli/summary-reporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export interface SummaryData {
files: string[]
totalErrors: number
totalWarnings: number
totalIgnored: number
filesWithOffenses: number
ruleCount: number
startTime: number
Expand All @@ -19,7 +20,7 @@ export class SummaryReporter {
}

displaySummary(data: SummaryData): void {
const { files, totalErrors, totalWarnings, filesWithOffenses, ruleCount, startTime, startDate, showTiming, autofixableCount } = data
const { files, totalErrors, totalWarnings, totalIgnored, filesWithOffenses, ruleCount, startTime, startDate, showTiming, autofixableCount } = data

console.log("\n")
console.log(` ${colorize("Summary:", "bold")}`)
Expand Down Expand Up @@ -63,6 +64,10 @@ export class SummaryReporter {
parts.push(colorize(colorize(`${totalWarnings} ${this.pluralize(totalWarnings, "warning")}`, "green"), "bold"))
}

if (totalIgnored > 0) {
parts.push(colorize(colorize(`${totalIgnored} ignored`, "gray"), "bold"))
}

if (parts.length === 0) {
offensesSummary = colorize(colorize("0 offenses", "green"), "bold")
} else {
Expand Down
41 changes: 39 additions & 2 deletions javascript/packages/linter/src/linter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,50 @@ export class Linter {
return (rule.constructor as any).type === "source"
}

private filterOffenses(ruleOffenses: LintOffense[], sourceLines: string[], ruleName: string): { kept: LintOffense[], ignored: LintOffense[] } {
const kept: LintOffense[] = [];
const ignored: LintOffense[] = [];

for (const offense of ruleOffenses) {
const line = offense.location.start.line;
if (line > sourceLines.length) {
kept.push(offense);
continue;
}
const lineContent = sourceLines[line - 1];

const disableCommentRegex = /<%#\s+herb:disable\s+(.*)%>/;
const match = lineContent.match(disableCommentRegex);

if (match) {
const rulesRaw = (match && match[1]) || '';
const rules = rulesRaw.split(",").map((rule) => rule.trim());
if (rules.includes(ruleName) || rules.includes("all")) {
ignored.push(offense);
} else {
kept.push(offense);
}
} else {
kept.push(offense);
}
}

return { kept, ignored };
}

/**
* Lint source code using Parser/AST, Lexer, and Source rules.
* @param source - The source code to lint
* @param context - Optional context for linting (e.g., fileName for distinguishing files vs snippets)
*/
lint(source: string, context?: Partial<LintContext>): LintResult {
this.offenses = []
let ignoredCount = 0;

const parseResult = this.herb.parse(source, { track_whitespace: true })
const lexResult = this.herb.lex(source)
const hasParserErrors = parseResult.recursiveErrors().length > 0
const sourceLines = source.split("\n");

for (const RuleClass of this.rules) {
const rule = new RuleClass()
Expand Down Expand Up @@ -105,7 +138,10 @@ export class Linter {
}
}

this.offenses.push(...ruleOffenses)
const { kept, ignored } = this.filterOffenses(ruleOffenses, sourceLines, rule.name);
ignoredCount += ignored.length;

this.offenses.push(...kept)
}

const errors = this.offenses.filter(offense => offense.severity === "error").length
Expand All @@ -114,7 +150,8 @@ export class Linter {
return {
offenses: this.offenses,
errors,
warnings
warnings,
ignored: ignoredCount
}
}

Expand Down
1 change: 1 addition & 0 deletions javascript/packages/linter/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export interface LintResult<TAutofixContext extends BaseAutofixContext = BaseAut
offenses: LintOffense<TAutofixContext>[]
errors: number
warnings: number
ignored: number
}

/**
Expand Down
39 changes: 39 additions & 0 deletions javascript/packages/linter/test/__snapshots__/cli.test.ts.snap

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions javascript/packages/linter/test/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,13 @@ describe("CLI Output Formatting", () => {
expect(exitCode).toBe(1)
})

test("Ignores disabled rules", () => {
const { output, exitCode } = runLinter("ignored.html.erb")

expect(output).toMatchSnapshot()
expect(exitCode).toBe(1)
})

test("rejects --github with --json format", () => {
const { output, exitCode } = runLinter("test-file-with-errors.html.erb", "--json", "--github")

Expand Down
9 changes: 9 additions & 0 deletions javascript/packages/linter/test/fixtures/ignored.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<div>
<h1 class="<%= classes %>">
<%= title %>
</h1>

<DIV>hello</DIV>
<DIV>hello</DIV> <%# herb:disable html-tag-name-lowercase %>
<% %> <%# herb:disable erb-no-empty-tags %>
</div>
9 changes: 9 additions & 0 deletions javascript/packages/linter/test/linter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,5 +193,14 @@ describe("@herb-tools/linter", () => {
expect(result2.offenses).toHaveLength(1)
expect(result2.offenses[0].rule).toBe("content-based-rule")
})

test("can disable a rule with a comment", () => {
const html = '<DIV>test</DIV> <%# herb:disable html-tag-name-lowercase %>'
const linter = new Linter(Herb, [HTMLTagNameLowercaseRule])
const lintResult = linter.lint(html)

expect(lintResult.offenses).toHaveLength(0)
expect(lintResult.ignored).toBe(2)
})
})
})
Loading