@herb-tools/linter 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -16
- package/dist/herb-lint.js +1684 -295
- package/dist/herb-lint.js.map +1 -1
- package/dist/index.cjs +1226 -158
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1188 -160
- package/dist/index.js.map +1 -1
- package/dist/package.json +11 -4
- package/dist/src/cli/argument-parser.js +11 -6
- package/dist/src/cli/argument-parser.js.map +1 -1
- package/dist/src/cli/file-processor.js +5 -6
- package/dist/src/cli/file-processor.js.map +1 -1
- package/dist/src/cli/formatters/detailed-formatter.js +3 -5
- package/dist/src/cli/formatters/detailed-formatter.js.map +1 -1
- package/dist/src/cli/formatters/github-actions-formatter.js +55 -11
- package/dist/src/cli/formatters/github-actions-formatter.js.map +1 -1
- package/dist/src/cli/index.js +1 -0
- package/dist/src/cli/index.js.map +1 -1
- package/dist/src/cli/output-manager.js +23 -5
- package/dist/src/cli/output-manager.js.map +1 -1
- package/dist/src/cli/summary-reporter.js +2 -11
- package/dist/src/cli/summary-reporter.js.map +1 -1
- package/dist/src/cli.js +88 -4
- package/dist/src/cli.js.map +1 -1
- package/dist/src/default-rules.js +8 -4
- package/dist/src/default-rules.js.map +1 -1
- package/dist/src/linter.js.map +1 -1
- package/dist/src/rules/erb-prefer-image-tag-helper.js +50 -60
- package/dist/src/rules/erb-prefer-image-tag-helper.js.map +1 -1
- package/dist/src/rules/html-boolean-attributes-no-value.js +8 -8
- package/dist/src/rules/html-boolean-attributes-no-value.js.map +1 -1
- package/dist/src/rules/html-no-duplicate-ids.js +134 -9
- package/dist/src/rules/html-no-duplicate-ids.js.map +1 -1
- package/dist/src/rules/html-no-empty-attributes.js +56 -0
- package/dist/src/rules/html-no-empty-attributes.js.map +1 -0
- package/dist/src/rules/html-no-positive-tab-index.js +1 -1
- package/dist/src/rules/html-no-positive-tab-index.js.map +1 -1
- package/dist/src/rules/html-no-self-closing.js +12 -5
- package/dist/src/rules/html-no-self-closing.js.map +1 -1
- package/dist/src/rules/html-no-underscores-in-attribute-names.js +36 -0
- package/dist/src/rules/html-no-underscores-in-attribute-names.js.map +1 -0
- package/dist/src/rules/index.js +3 -0
- package/dist/src/rules/index.js.map +1 -1
- package/dist/src/rules/rule-utils.js +80 -7
- package/dist/src/rules/rule-utils.js.map +1 -1
- package/dist/src/rules/svg-tag-name-capitalization.js +2 -2
- package/dist/src/rules/svg-tag-name-capitalization.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/cli/argument-parser.d.ts +2 -1
- package/dist/types/cli/file-processor.d.ts +6 -1
- package/dist/types/cli/formatters/github-actions-formatter.d.ts +6 -1
- package/dist/types/cli/index.d.ts +1 -0
- package/dist/types/cli/output-manager.d.ts +1 -0
- package/dist/types/cli.d.ts +20 -5
- package/dist/types/linter.d.ts +7 -7
- package/dist/types/rules/html-no-empty-attributes.d.ts +7 -0
- package/dist/types/rules/html-no-underscores-in-attribute-names.d.ts +7 -0
- package/dist/types/rules/index.d.ts +3 -0
- package/dist/types/rules/rule-utils.d.ts +46 -5
- package/dist/types/src/cli/argument-parser.d.ts +2 -1
- package/dist/types/src/cli/file-processor.d.ts +6 -1
- package/dist/types/src/cli/formatters/github-actions-formatter.d.ts +6 -1
- package/dist/types/src/cli/index.d.ts +1 -0
- package/dist/types/src/cli/output-manager.d.ts +1 -0
- package/dist/types/src/cli.d.ts +20 -5
- package/dist/types/src/linter.d.ts +7 -7
- package/dist/types/src/rules/html-no-empty-attributes.d.ts +7 -0
- package/dist/types/src/rules/html-no-underscores-in-attribute-names.d.ts +7 -0
- package/dist/types/src/rules/index.d.ts +3 -0
- package/dist/types/src/rules/rule-utils.d.ts +46 -5
- package/docs/rules/README.md +2 -0
- package/docs/rules/html-img-require-alt.md +0 -2
- package/docs/rules/html-no-empty-attributes.md +77 -0
- package/docs/rules/html-no-underscores-in-attribute-names.md +45 -0
- package/package.json +11 -4
- package/src/cli/argument-parser.ts +15 -7
- package/src/cli/file-processor.ts +11 -7
- package/src/cli/formatters/detailed-formatter.ts +5 -7
- package/src/cli/formatters/github-actions-formatter.ts +64 -11
- package/src/cli/index.ts +2 -0
- package/src/cli/output-manager.ts +27 -5
- package/src/cli/summary-reporter.ts +3 -11
- package/src/cli.ts +125 -20
- package/src/default-rules.ts +8 -4
- package/src/linter.ts +6 -6
- package/src/rules/erb-no-silent-tag-in-attribute-name.ts +1 -1
- package/src/rules/erb-prefer-image-tag-helper.ts +53 -71
- package/src/rules/erb-require-whitespace-inside-tags.ts +2 -2
- package/src/rules/html-attribute-double-quotes.ts +1 -1
- package/src/rules/html-boolean-attributes-no-value.ts +9 -11
- package/src/rules/html-no-duplicate-ids.ts +188 -14
- package/src/rules/html-no-empty-attributes.ts +75 -0
- package/src/rules/html-no-positive-tab-index.ts +1 -1
- package/src/rules/html-no-self-closing.ts +13 -8
- package/src/rules/html-no-underscores-in-attribute-names.ts +58 -0
- package/src/rules/html-tag-name-lowercase.ts +1 -1
- package/src/rules/index.ts +3 -0
- package/src/rules/rule-utils.ts +110 -9
- package/src/rules/svg-tag-name-capitalization.ts +2 -2
|
@@ -1,10 +1,24 @@
|
|
|
1
|
+
import { Highlighter } from "@herb-tools/highlighter"
|
|
2
|
+
|
|
1
3
|
import { BaseFormatter } from "./base-formatter.js"
|
|
4
|
+
import { name, version } from "../../../package.json"
|
|
2
5
|
|
|
3
6
|
import type { Diagnostic } from "@herb-tools/core"
|
|
4
7
|
import type { ProcessedFile } from "../file-processor.js"
|
|
5
8
|
|
|
6
9
|
// https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-commands
|
|
7
10
|
export class GitHubActionsFormatter extends BaseFormatter {
|
|
11
|
+
private highlighter: Highlighter
|
|
12
|
+
private wrapLines: boolean
|
|
13
|
+
private truncateLines: boolean
|
|
14
|
+
|
|
15
|
+
constructor(wrapLines: boolean = true, truncateLines: boolean = false) {
|
|
16
|
+
super()
|
|
17
|
+
this.wrapLines = wrapLines
|
|
18
|
+
this.truncateLines = truncateLines
|
|
19
|
+
this.highlighter = new Highlighter()
|
|
20
|
+
}
|
|
21
|
+
|
|
8
22
|
private static readonly MESSAGE_ESCAPE_MAP: Record<string, string> = {
|
|
9
23
|
'%': '%25',
|
|
10
24
|
'\n': '%0A',
|
|
@@ -19,13 +33,39 @@ export class GitHubActionsFormatter extends BaseFormatter {
|
|
|
19
33
|
',': '%2C'
|
|
20
34
|
}
|
|
21
35
|
|
|
22
|
-
async format(allDiagnostics: ProcessedFile[]): Promise<void> {
|
|
23
|
-
|
|
24
|
-
|
|
36
|
+
async format(allDiagnostics: ProcessedFile[], _isSingleFile: boolean = false): Promise<void> {
|
|
37
|
+
await this.formatAnnotations(allDiagnostics)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async formatAnnotations(allDiagnostics: ProcessedFile[]): Promise<void> {
|
|
41
|
+
if (allDiagnostics.length === 0) return
|
|
42
|
+
|
|
43
|
+
if (!this.highlighter.initialized) {
|
|
44
|
+
await this.highlighter.initialize()
|
|
25
45
|
}
|
|
26
46
|
|
|
27
|
-
|
|
28
|
-
|
|
47
|
+
for (const { filename, offense, content } of allDiagnostics) {
|
|
48
|
+
const originalNoColor = process.env.NO_COLOR
|
|
49
|
+
process.env.NO_COLOR = "1"
|
|
50
|
+
|
|
51
|
+
let plainCodePreview = ""
|
|
52
|
+
try {
|
|
53
|
+
const formatted = this.highlighter.highlightDiagnostic(filename, offense, content, {
|
|
54
|
+
contextLines: 2,
|
|
55
|
+
wrapLines: this.wrapLines,
|
|
56
|
+
truncateLines: this.truncateLines
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
plainCodePreview = formatted.split('\n').slice(1).join('\n')
|
|
60
|
+
} finally {
|
|
61
|
+
if (originalNoColor === undefined) {
|
|
62
|
+
delete process.env.NO_COLOR
|
|
63
|
+
} else {
|
|
64
|
+
process.env.NO_COLOR = originalNoColor
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this.formatDiagnostic(filename, offense, plainCodePreview)
|
|
29
69
|
}
|
|
30
70
|
}
|
|
31
71
|
|
|
@@ -36,22 +76,35 @@ export class GitHubActionsFormatter extends BaseFormatter {
|
|
|
36
76
|
}
|
|
37
77
|
|
|
38
78
|
// GitHub Actions annotation format:
|
|
39
|
-
// ::{level} file={file},line={line},col={col}::{message}
|
|
79
|
+
// ::{level} file={file},line={line},col={col},title={title}::{message}
|
|
40
80
|
//
|
|
41
|
-
private formatDiagnostic(filename: string, diagnostic: Diagnostic): void {
|
|
81
|
+
private formatDiagnostic(filename: string, diagnostic: Diagnostic, codePreview: string = ""): void {
|
|
42
82
|
const level = diagnostic.severity === "error" ? "error" : "warning"
|
|
43
83
|
const { line, column } = diagnostic.location.start
|
|
44
84
|
|
|
45
85
|
const escapedFilename = this.escapeParam(filename)
|
|
46
|
-
|
|
86
|
+
let message = diagnostic.message
|
|
87
|
+
|
|
88
|
+
if (diagnostic.code) {
|
|
89
|
+
message += ` [${diagnostic.code}]`
|
|
90
|
+
}
|
|
47
91
|
|
|
48
|
-
|
|
92
|
+
if (codePreview) {
|
|
93
|
+
message += "\n\n" + codePreview
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const escapedMessage = this.escapeMessage(message)
|
|
97
|
+
|
|
98
|
+
let annotations = `file=${escapedFilename},line=${line},col=${column}`
|
|
49
99
|
|
|
50
100
|
if (diagnostic.code) {
|
|
51
|
-
|
|
101
|
+
const title = `${diagnostic.code} • ${name}@${version}`
|
|
102
|
+
const escapedTitle = this.escapeParam(title)
|
|
103
|
+
|
|
104
|
+
annotations += `,title=${escapedTitle}`
|
|
52
105
|
}
|
|
53
106
|
|
|
54
|
-
console.log(`\n::${level}
|
|
107
|
+
console.log(`\n::${level} ${annotations}::${escapedMessage}`)
|
|
55
108
|
}
|
|
56
109
|
|
|
57
110
|
private escapeMessage(string: string): string {
|
package/src/cli/index.ts
CHANGED
|
@@ -11,6 +11,7 @@ interface OutputOptions {
|
|
|
11
11
|
wrapLines: boolean
|
|
12
12
|
truncateLines: boolean
|
|
13
13
|
showTiming: boolean
|
|
14
|
+
useGitHubActions: boolean
|
|
14
15
|
startTime: number
|
|
15
16
|
startDate: Date
|
|
16
17
|
}
|
|
@@ -28,9 +29,30 @@ export class OutputManager {
|
|
|
28
29
|
async outputResults(results: LintResults, options: OutputOptions): Promise<void> {
|
|
29
30
|
const { allOffenses, files, totalErrors, totalWarnings, filesWithOffenses, ruleCount, ruleOffenses } = results
|
|
30
31
|
|
|
31
|
-
if (options.
|
|
32
|
-
const
|
|
33
|
-
await
|
|
32
|
+
if (options.useGitHubActions) {
|
|
33
|
+
const githubFormatter = new GitHubActionsFormatter(options.wrapLines, options.truncateLines)
|
|
34
|
+
await githubFormatter.formatAnnotations(allOffenses)
|
|
35
|
+
|
|
36
|
+
if (options.formatOption !== "json") {
|
|
37
|
+
const regularFormatter = options.formatOption === "simple"
|
|
38
|
+
? new SimpleFormatter()
|
|
39
|
+
: new DetailedFormatter(options.theme, options.wrapLines, options.truncateLines)
|
|
40
|
+
|
|
41
|
+
await regularFormatter.format(allOffenses, files.length === 1)
|
|
42
|
+
|
|
43
|
+
this.summaryReporter.displayMostViolatedRules(ruleOffenses)
|
|
44
|
+
this.summaryReporter.displaySummary({
|
|
45
|
+
files,
|
|
46
|
+
totalErrors,
|
|
47
|
+
totalWarnings,
|
|
48
|
+
filesWithOffenses,
|
|
49
|
+
ruleCount,
|
|
50
|
+
startTime: options.startTime,
|
|
51
|
+
startDate: options.startDate,
|
|
52
|
+
showTiming: options.showTiming,
|
|
53
|
+
ruleOffenses
|
|
54
|
+
})
|
|
55
|
+
}
|
|
34
56
|
} else if (options.formatOption === "json") {
|
|
35
57
|
const output: JSONOutput = {
|
|
36
58
|
offenses: allOffenses.map(({ filename, offense }) => ({
|
|
@@ -88,7 +110,7 @@ export class OutputManager {
|
|
|
88
110
|
* Output informational message (like "no files found")
|
|
89
111
|
*/
|
|
90
112
|
outputInfo(message: string, options: OutputOptions): void {
|
|
91
|
-
if (options.
|
|
113
|
+
if (options.useGitHubActions) {
|
|
92
114
|
// GitHub Actions format doesn't output anything for info messages
|
|
93
115
|
} else if (options.formatOption === "json") {
|
|
94
116
|
const output: JSONOutput = {
|
|
@@ -123,7 +145,7 @@ export class OutputManager {
|
|
|
123
145
|
* Output error message
|
|
124
146
|
*/
|
|
125
147
|
outputError(message: string, options: OutputOptions): void {
|
|
126
|
-
if (options.
|
|
148
|
+
if (options.useGitHubActions) {
|
|
127
149
|
console.log(`::error::${message}`)
|
|
128
150
|
} else if (options.formatOption === "json") {
|
|
129
151
|
const output: JSONOutput = {
|
|
@@ -23,14 +23,11 @@ export class SummaryReporter {
|
|
|
23
23
|
console.log("\n")
|
|
24
24
|
console.log(` ${colorize("Summary:", "bold")}`)
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
const labelWidth = 12 // Width for the longest label "Offenses"
|
|
26
|
+
const labelWidth = 12
|
|
28
27
|
const pad = (label: string) => label.padEnd(labelWidth)
|
|
29
28
|
|
|
30
|
-
// Checked summary
|
|
31
29
|
console.log(` ${colorize(pad("Checked"), "gray")} ${colorize(`${files.length} ${this.pluralize(files.length, "file")}`, "cyan")}`)
|
|
32
30
|
|
|
33
|
-
// Files summary (for multiple files)
|
|
34
31
|
if (files.length > 1) {
|
|
35
32
|
const filesChecked = files.length
|
|
36
33
|
const filesClean = filesChecked - filesWithOffenses
|
|
@@ -52,11 +49,9 @@ export class SummaryReporter {
|
|
|
52
49
|
}
|
|
53
50
|
}
|
|
54
51
|
|
|
55
|
-
// Offenses summary with file count
|
|
56
52
|
let offensesSummary = ""
|
|
57
53
|
const parts = []
|
|
58
54
|
|
|
59
|
-
// Build the main part with errors and warnings
|
|
60
55
|
if (totalErrors > 0) {
|
|
61
56
|
parts.push(colorize(colorize(`${totalErrors} ${this.pluralize(totalErrors, "error")}`, "brightRed"), "bold"))
|
|
62
57
|
}
|
|
@@ -64,7 +59,6 @@ export class SummaryReporter {
|
|
|
64
59
|
if (totalWarnings > 0) {
|
|
65
60
|
parts.push(colorize(colorize(`${totalWarnings} ${this.pluralize(totalWarnings, "warning")}`, "brightYellow"), "bold"))
|
|
66
61
|
} else if (totalErrors > 0) {
|
|
67
|
-
// Show 0 warnings when there are errors but no warnings
|
|
68
62
|
parts.push(colorize(colorize(`${totalWarnings} ${this.pluralize(totalWarnings, "warning")}`, "green"), "bold"))
|
|
69
63
|
}
|
|
70
64
|
|
|
@@ -72,7 +66,7 @@ export class SummaryReporter {
|
|
|
72
66
|
offensesSummary = colorize(colorize("0 offenses", "green"), "bold")
|
|
73
67
|
} else {
|
|
74
68
|
offensesSummary = parts.join(" | ")
|
|
75
|
-
|
|
69
|
+
|
|
76
70
|
let detailText = ""
|
|
77
71
|
|
|
78
72
|
const totalOffenses = totalErrors + totalWarnings
|
|
@@ -86,16 +80,14 @@ export class SummaryReporter {
|
|
|
86
80
|
|
|
87
81
|
console.log(` ${colorize(pad("Offenses"), "gray")} ${offensesSummary}`)
|
|
88
82
|
|
|
89
|
-
// Timing information (if enabled)
|
|
90
83
|
if (showTiming) {
|
|
91
84
|
const duration = Date.now() - startTime
|
|
92
|
-
const timeString = startDate.toTimeString().split(' ')[0]
|
|
85
|
+
const timeString = startDate.toTimeString().split(' ')[0]
|
|
93
86
|
|
|
94
87
|
console.log(` ${colorize(pad("Start at"), "gray")} ${colorize(timeString, "cyan")}`)
|
|
95
88
|
console.log(` ${colorize(pad("Duration"), "gray")} ${colorize(`${duration}ms`, "cyan")} ${colorize(colorize(`(${ruleCount} ${this.pluralize(ruleCount, "rule")})`, "gray"), "dim")}`)
|
|
96
89
|
}
|
|
97
90
|
|
|
98
|
-
// Success message for all files clean
|
|
99
91
|
if (filesWithOffenses === 0 && files.length > 1) {
|
|
100
92
|
console.log("")
|
|
101
93
|
console.log(` ${colorize("✓", "brightGreen")} ${colorize("All files are clean!", "green")}`)
|
package/src/cli.ts
CHANGED
|
@@ -1,47 +1,145 @@
|
|
|
1
1
|
import { glob } from "glob"
|
|
2
2
|
import { Herb } from "@herb-tools/node-wasm"
|
|
3
|
+
import { existsSync, statSync } from "fs"
|
|
4
|
+
import { dirname, resolve, relative } from "path"
|
|
5
|
+
|
|
3
6
|
import { ArgumentParser, type FormatOption } from "./cli/argument-parser.js"
|
|
4
7
|
import { FileProcessor } from "./cli/file-processor.js"
|
|
5
8
|
import { OutputManager } from "./cli/output-manager.js"
|
|
6
9
|
|
|
10
|
+
export * from "./cli/index.js"
|
|
11
|
+
|
|
7
12
|
export class CLI {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
13
|
+
protected argumentParser = new ArgumentParser()
|
|
14
|
+
protected fileProcessor = new FileProcessor()
|
|
15
|
+
protected outputManager = new OutputManager()
|
|
16
|
+
protected projectPath: string = process.cwd()
|
|
17
|
+
|
|
18
|
+
getProjectPath(): string {
|
|
19
|
+
return this.projectPath
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
protected findProjectRoot(startPath: string): string {
|
|
23
|
+
let currentPath = resolve(startPath)
|
|
24
|
+
|
|
25
|
+
if (existsSync(currentPath) && statSync(currentPath).isFile()) {
|
|
26
|
+
currentPath = dirname(currentPath)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const projectIndicators = [
|
|
30
|
+
'package.json',
|
|
31
|
+
'Gemfile',
|
|
32
|
+
'.git',
|
|
33
|
+
'tsconfig.json',
|
|
34
|
+
'composer.json',
|
|
35
|
+
'pyproject.toml',
|
|
36
|
+
'requirements.txt',
|
|
37
|
+
'.herb.yml'
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
while (currentPath !== '/') {
|
|
41
|
+
for (const indicator of projectIndicators) {
|
|
42
|
+
if (existsSync(resolve(currentPath, indicator))) {
|
|
43
|
+
return currentPath
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const parentPath = dirname(currentPath)
|
|
48
|
+
if (parentPath === currentPath) {
|
|
49
|
+
break
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
currentPath = parentPath
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return existsSync(startPath) && statSync(startPath).isDirectory()
|
|
56
|
+
? startPath
|
|
57
|
+
: dirname(startPath)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
protected exitWithError(message: string, formatOption: FormatOption, exitCode: number = 1) {
|
|
61
|
+
this.outputManager.outputError(message, {
|
|
62
|
+
formatOption,
|
|
63
|
+
theme: 'auto',
|
|
64
|
+
wrapLines: false,
|
|
65
|
+
truncateLines: false,
|
|
66
|
+
showTiming: false,
|
|
67
|
+
useGitHubActions: false,
|
|
68
|
+
startTime: 0,
|
|
69
|
+
startDate: new Date()
|
|
21
70
|
})
|
|
22
71
|
process.exit(exitCode)
|
|
23
72
|
}
|
|
24
73
|
|
|
25
|
-
|
|
74
|
+
protected exitWithInfo(message: string, formatOption: FormatOption, exitCode: number = 0, timingData?: { startTime: number, startDate: Date, showTiming: boolean }) {
|
|
26
75
|
const outputOptions = {
|
|
27
76
|
formatOption,
|
|
28
77
|
theme: 'auto' as const,
|
|
29
78
|
wrapLines: false,
|
|
30
79
|
truncateLines: false,
|
|
31
80
|
showTiming: timingData?.showTiming ?? false,
|
|
81
|
+
useGitHubActions: false,
|
|
32
82
|
startTime: timingData?.startTime ?? Date.now(),
|
|
33
83
|
startDate: timingData?.startDate ?? new Date()
|
|
34
84
|
}
|
|
35
|
-
|
|
85
|
+
|
|
36
86
|
this.outputManager.outputInfo(message, outputOptions)
|
|
37
87
|
process.exit(exitCode)
|
|
38
88
|
}
|
|
39
89
|
|
|
90
|
+
protected determineProjectPath(pattern: string | undefined): void {
|
|
91
|
+
if (pattern) {
|
|
92
|
+
const resolvedPattern = resolve(pattern)
|
|
93
|
+
|
|
94
|
+
if (existsSync(resolvedPattern)) {
|
|
95
|
+
const stats = statSync(resolvedPattern)
|
|
96
|
+
|
|
97
|
+
if (stats.isDirectory()) {
|
|
98
|
+
this.projectPath = resolvedPattern
|
|
99
|
+
} else {
|
|
100
|
+
this.projectPath = this.findProjectRoot(resolvedPattern)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
protected adjustPattern(pattern: string | undefined): string {
|
|
107
|
+
if (!pattern) {
|
|
108
|
+
return '**/*.html.erb'
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const resolvedPattern = resolve(pattern)
|
|
112
|
+
|
|
113
|
+
if (existsSync(resolvedPattern)) {
|
|
114
|
+
const stats = statSync(resolvedPattern)
|
|
115
|
+
|
|
116
|
+
if (stats.isDirectory()) {
|
|
117
|
+
return '**/*.html.erb'
|
|
118
|
+
} else if (stats.isFile()) {
|
|
119
|
+
return relative(this.projectPath, resolvedPattern)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return pattern
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
protected async beforeProcess(): Promise<void> {
|
|
127
|
+
await Herb.load()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
protected async afterProcess(_results: any, _outputOptions: any): Promise<void> {
|
|
131
|
+
// Hook for subclasses to add custom output after processing
|
|
132
|
+
}
|
|
133
|
+
|
|
40
134
|
async run() {
|
|
41
135
|
const startTime = Date.now()
|
|
42
136
|
const startDate = new Date()
|
|
43
137
|
|
|
44
|
-
|
|
138
|
+
let { pattern, formatOption, showTiming, theme, wrapLines, truncateLines, useGitHubActions } = this.argumentParser.parse(process.argv)
|
|
139
|
+
|
|
140
|
+
this.determineProjectPath(pattern)
|
|
141
|
+
|
|
142
|
+
pattern = this.adjustPattern(pattern)
|
|
45
143
|
|
|
46
144
|
const outputOptions = {
|
|
47
145
|
formatOption,
|
|
@@ -49,22 +147,29 @@ export class CLI {
|
|
|
49
147
|
wrapLines,
|
|
50
148
|
truncateLines,
|
|
51
149
|
showTiming,
|
|
150
|
+
useGitHubActions,
|
|
52
151
|
startTime,
|
|
53
152
|
startDate
|
|
54
153
|
}
|
|
55
154
|
|
|
56
155
|
try {
|
|
57
|
-
await
|
|
156
|
+
await this.beforeProcess()
|
|
58
157
|
|
|
59
|
-
const files = await glob(pattern)
|
|
158
|
+
const files = await glob(pattern, { cwd: this.projectPath })
|
|
60
159
|
|
|
61
160
|
if (files.length === 0) {
|
|
62
161
|
this.exitWithInfo(`No files found matching pattern: ${pattern}`, formatOption, 0, { startTime, startDate, showTiming })
|
|
63
162
|
}
|
|
64
163
|
|
|
65
|
-
const
|
|
66
|
-
|
|
164
|
+
const context = {
|
|
165
|
+
projectPath: this.projectPath,
|
|
166
|
+
pattern: pattern
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const results = await this.fileProcessor.processFiles(files, formatOption, context)
|
|
170
|
+
|
|
67
171
|
await this.outputManager.outputResults({ ...results, files }, outputOptions)
|
|
172
|
+
await this.afterProcess(results, outputOptions)
|
|
68
173
|
|
|
69
174
|
if (results.totalErrors > 0) {
|
|
70
175
|
process.exit(1)
|
package/src/default-rules.ts
CHANGED
|
@@ -19,19 +19,21 @@ import { HTMLAvoidBothDisabledAndAriaDisabledRule } from "./rules/html-avoid-bot
|
|
|
19
19
|
import { HTMLBooleanAttributesNoValueRule } from "./rules/html-boolean-attributes-no-value.js"
|
|
20
20
|
import { HTMLIframeHasTitleRule } from "./rules/html-iframe-has-title.js"
|
|
21
21
|
import { HTMLImgRequireAltRule } from "./rules/html-img-require-alt.js"
|
|
22
|
-
import { HTMLNavigationHasLabelRule } from "./rules/html-navigation-has-label.js"
|
|
22
|
+
// import { HTMLNavigationHasLabelRule } from "./rules/html-navigation-has-label.js"
|
|
23
23
|
import { HTMLNoAriaHiddenOnFocusableRule } from "./rules/html-no-aria-hidden-on-focusable.js"
|
|
24
24
|
// import { HTMLNoBlockInsideInlineRule } from "./rules/html-no-block-inside-inline.js"
|
|
25
25
|
import { HTMLNoDuplicateAttributesRule } from "./rules/html-no-duplicate-attributes.js"
|
|
26
26
|
import { HTMLNoDuplicateIdsRule } from "./rules/html-no-duplicate-ids.js"
|
|
27
|
+
import { HTMLNoEmptyAttributesRule } from "./rules/html-no-empty-attributes.js"
|
|
27
28
|
import { HTMLNoEmptyHeadingsRule } from "./rules/html-no-empty-headings.js"
|
|
28
29
|
import { HTMLNoNestedLinksRule } from "./rules/html-no-nested-links.js"
|
|
29
30
|
import { HTMLNoPositiveTabIndexRule } from "./rules/html-no-positive-tab-index.js"
|
|
30
31
|
import { HTMLNoSelfClosingRule } from "./rules/html-no-self-closing.js"
|
|
31
|
-
import { HTMLNoTitleAttributeRule } from "./rules/html-no-title-attribute.js"
|
|
32
|
+
// import { HTMLNoTitleAttributeRule } from "./rules/html-no-title-attribute.js"
|
|
32
33
|
import { HTMLTagNameLowercaseRule } from "./rules/html-tag-name-lowercase.js"
|
|
33
34
|
import { ParserNoErrorsRule } from "./rules/parser-no-errors.js"
|
|
34
35
|
import { SVGTagNameCapitalizationRule } from "./rules/svg-tag-name-capitalization.js"
|
|
36
|
+
import { HTMLNoUnderscoresInAttributeNamesRule } from "./rules/html-no-underscores-in-attribute-names.js"
|
|
35
37
|
|
|
36
38
|
export const defaultRules: RuleClass[] = [
|
|
37
39
|
ERBNoEmptyTagsRule,
|
|
@@ -53,17 +55,19 @@ export const defaultRules: RuleClass[] = [
|
|
|
53
55
|
HTMLBooleanAttributesNoValueRule,
|
|
54
56
|
HTMLIframeHasTitleRule,
|
|
55
57
|
HTMLImgRequireAltRule,
|
|
56
|
-
HTMLNavigationHasLabelRule,
|
|
58
|
+
// HTMLNavigationHasLabelRule,
|
|
57
59
|
HTMLNoAriaHiddenOnFocusableRule,
|
|
58
60
|
// HTMLNoBlockInsideInlineRule,
|
|
59
61
|
HTMLNoDuplicateAttributesRule,
|
|
60
62
|
HTMLNoDuplicateIdsRule,
|
|
63
|
+
HTMLNoEmptyAttributesRule,
|
|
61
64
|
HTMLNoEmptyHeadingsRule,
|
|
62
65
|
HTMLNoNestedLinksRule,
|
|
63
66
|
HTMLNoPositiveTabIndexRule,
|
|
64
67
|
HTMLNoSelfClosingRule,
|
|
65
|
-
HTMLNoTitleAttributeRule,
|
|
68
|
+
// HTMLNoTitleAttributeRule,
|
|
66
69
|
HTMLTagNameLowercaseRule,
|
|
67
70
|
ParserNoErrorsRule,
|
|
68
71
|
SVGTagNameCapitalizationRule,
|
|
72
|
+
HTMLNoUnderscoresInAttributeNamesRule,
|
|
69
73
|
]
|
package/src/linter.ts
CHANGED
|
@@ -4,9 +4,9 @@ import type { RuleClass, Rule, ParserRule, LexerRule, SourceRule, LintResult, Li
|
|
|
4
4
|
import type { HerbBackend } from "@herb-tools/core"
|
|
5
5
|
|
|
6
6
|
export class Linter {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
protected rules: RuleClass[]
|
|
8
|
+
protected herb: HerbBackend
|
|
9
|
+
protected offenses: LintOffense[]
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Creates a new Linter instance.
|
|
@@ -23,7 +23,7 @@ export class Linter {
|
|
|
23
23
|
* Returns the default set of rule classes used by the linter.
|
|
24
24
|
* @returns Array of rule classes
|
|
25
25
|
*/
|
|
26
|
-
|
|
26
|
+
protected getDefaultRules(): RuleClass[] {
|
|
27
27
|
return defaultRules
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -34,14 +34,14 @@ export class Linter {
|
|
|
34
34
|
/**
|
|
35
35
|
* Type guard to check if a rule is a LexerRule
|
|
36
36
|
*/
|
|
37
|
-
|
|
37
|
+
protected isLexerRule(rule: Rule): rule is LexerRule {
|
|
38
38
|
return (rule.constructor as any).type === "lexer"
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* Type guard to check if a rule is a SourceRule
|
|
43
43
|
*/
|
|
44
|
-
|
|
44
|
+
protected isSourceRule(rule: Rule): rule is SourceRule {
|
|
45
45
|
return (rule.constructor as any).type === "source"
|
|
46
46
|
}
|
|
47
47
|
|
|
@@ -23,7 +23,7 @@ class ERBNoSilentTagInAttributeNameVisitor extends BaseRuleVisitor {
|
|
|
23
23
|
private isSilentERBTag(node: ERBContentNode): boolean {
|
|
24
24
|
const silentTags = ["<%", "<%-", "<%#"]
|
|
25
25
|
|
|
26
|
-
return silentTags.includes(node.tag_opening?.value ||
|
|
26
|
+
return silentTags.includes(node.tag_opening?.value || "")
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|