@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.
Files changed (99) hide show
  1. package/README.md +60 -16
  2. package/dist/herb-lint.js +1684 -295
  3. package/dist/herb-lint.js.map +1 -1
  4. package/dist/index.cjs +1226 -158
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.js +1188 -160
  7. package/dist/index.js.map +1 -1
  8. package/dist/package.json +11 -4
  9. package/dist/src/cli/argument-parser.js +11 -6
  10. package/dist/src/cli/argument-parser.js.map +1 -1
  11. package/dist/src/cli/file-processor.js +5 -6
  12. package/dist/src/cli/file-processor.js.map +1 -1
  13. package/dist/src/cli/formatters/detailed-formatter.js +3 -5
  14. package/dist/src/cli/formatters/detailed-formatter.js.map +1 -1
  15. package/dist/src/cli/formatters/github-actions-formatter.js +55 -11
  16. package/dist/src/cli/formatters/github-actions-formatter.js.map +1 -1
  17. package/dist/src/cli/index.js +1 -0
  18. package/dist/src/cli/index.js.map +1 -1
  19. package/dist/src/cli/output-manager.js +23 -5
  20. package/dist/src/cli/output-manager.js.map +1 -1
  21. package/dist/src/cli/summary-reporter.js +2 -11
  22. package/dist/src/cli/summary-reporter.js.map +1 -1
  23. package/dist/src/cli.js +88 -4
  24. package/dist/src/cli.js.map +1 -1
  25. package/dist/src/default-rules.js +8 -4
  26. package/dist/src/default-rules.js.map +1 -1
  27. package/dist/src/linter.js.map +1 -1
  28. package/dist/src/rules/erb-prefer-image-tag-helper.js +50 -60
  29. package/dist/src/rules/erb-prefer-image-tag-helper.js.map +1 -1
  30. package/dist/src/rules/html-boolean-attributes-no-value.js +8 -8
  31. package/dist/src/rules/html-boolean-attributes-no-value.js.map +1 -1
  32. package/dist/src/rules/html-no-duplicate-ids.js +134 -9
  33. package/dist/src/rules/html-no-duplicate-ids.js.map +1 -1
  34. package/dist/src/rules/html-no-empty-attributes.js +56 -0
  35. package/dist/src/rules/html-no-empty-attributes.js.map +1 -0
  36. package/dist/src/rules/html-no-positive-tab-index.js +1 -1
  37. package/dist/src/rules/html-no-positive-tab-index.js.map +1 -1
  38. package/dist/src/rules/html-no-self-closing.js +12 -5
  39. package/dist/src/rules/html-no-self-closing.js.map +1 -1
  40. package/dist/src/rules/html-no-underscores-in-attribute-names.js +36 -0
  41. package/dist/src/rules/html-no-underscores-in-attribute-names.js.map +1 -0
  42. package/dist/src/rules/index.js +3 -0
  43. package/dist/src/rules/index.js.map +1 -1
  44. package/dist/src/rules/rule-utils.js +80 -7
  45. package/dist/src/rules/rule-utils.js.map +1 -1
  46. package/dist/src/rules/svg-tag-name-capitalization.js +2 -2
  47. package/dist/src/rules/svg-tag-name-capitalization.js.map +1 -1
  48. package/dist/tsconfig.tsbuildinfo +1 -1
  49. package/dist/types/cli/argument-parser.d.ts +2 -1
  50. package/dist/types/cli/file-processor.d.ts +6 -1
  51. package/dist/types/cli/formatters/github-actions-formatter.d.ts +6 -1
  52. package/dist/types/cli/index.d.ts +1 -0
  53. package/dist/types/cli/output-manager.d.ts +1 -0
  54. package/dist/types/cli.d.ts +20 -5
  55. package/dist/types/linter.d.ts +7 -7
  56. package/dist/types/rules/html-no-empty-attributes.d.ts +7 -0
  57. package/dist/types/rules/html-no-underscores-in-attribute-names.d.ts +7 -0
  58. package/dist/types/rules/index.d.ts +3 -0
  59. package/dist/types/rules/rule-utils.d.ts +46 -5
  60. package/dist/types/src/cli/argument-parser.d.ts +2 -1
  61. package/dist/types/src/cli/file-processor.d.ts +6 -1
  62. package/dist/types/src/cli/formatters/github-actions-formatter.d.ts +6 -1
  63. package/dist/types/src/cli/index.d.ts +1 -0
  64. package/dist/types/src/cli/output-manager.d.ts +1 -0
  65. package/dist/types/src/cli.d.ts +20 -5
  66. package/dist/types/src/linter.d.ts +7 -7
  67. package/dist/types/src/rules/html-no-empty-attributes.d.ts +7 -0
  68. package/dist/types/src/rules/html-no-underscores-in-attribute-names.d.ts +7 -0
  69. package/dist/types/src/rules/index.d.ts +3 -0
  70. package/dist/types/src/rules/rule-utils.d.ts +46 -5
  71. package/docs/rules/README.md +2 -0
  72. package/docs/rules/html-img-require-alt.md +0 -2
  73. package/docs/rules/html-no-empty-attributes.md +77 -0
  74. package/docs/rules/html-no-underscores-in-attribute-names.md +45 -0
  75. package/package.json +11 -4
  76. package/src/cli/argument-parser.ts +15 -7
  77. package/src/cli/file-processor.ts +11 -7
  78. package/src/cli/formatters/detailed-formatter.ts +5 -7
  79. package/src/cli/formatters/github-actions-formatter.ts +64 -11
  80. package/src/cli/index.ts +2 -0
  81. package/src/cli/output-manager.ts +27 -5
  82. package/src/cli/summary-reporter.ts +3 -11
  83. package/src/cli.ts +125 -20
  84. package/src/default-rules.ts +8 -4
  85. package/src/linter.ts +6 -6
  86. package/src/rules/erb-no-silent-tag-in-attribute-name.ts +1 -1
  87. package/src/rules/erb-prefer-image-tag-helper.ts +53 -71
  88. package/src/rules/erb-require-whitespace-inside-tags.ts +2 -2
  89. package/src/rules/html-attribute-double-quotes.ts +1 -1
  90. package/src/rules/html-boolean-attributes-no-value.ts +9 -11
  91. package/src/rules/html-no-duplicate-ids.ts +188 -14
  92. package/src/rules/html-no-empty-attributes.ts +75 -0
  93. package/src/rules/html-no-positive-tab-index.ts +1 -1
  94. package/src/rules/html-no-self-closing.ts +13 -8
  95. package/src/rules/html-no-underscores-in-attribute-names.ts +58 -0
  96. package/src/rules/html-tag-name-lowercase.ts +1 -1
  97. package/src/rules/index.ts +3 -0
  98. package/src/rules/rule-utils.ts +110 -9
  99. 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
- for (const { filename, offense } of allDiagnostics) {
24
- this.formatDiagnostic(filename, offense)
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
- if (allDiagnostics.length > 0) {
28
- console.log()
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
- const message = this.escapeMessage(diagnostic.message)
86
+ let message = diagnostic.message
87
+
88
+ if (diagnostic.code) {
89
+ message += ` [${diagnostic.code}]`
90
+ }
47
91
 
48
- let fullMessage = message
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
- fullMessage += ` [${diagnostic.code}]`
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} file=${escapedFilename},line=${line},col=${column}::${fullMessage}`)
107
+ console.log(`\n::${level} ${annotations}::${escapedMessage}`)
55
108
  }
56
109
 
57
110
  private escapeMessage(string: string): string {
package/src/cli/index.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export { ArgumentParser } from "./argument-parser.js"
2
2
  export { FileProcessor } from "./file-processor.js"
3
3
  export { SummaryReporter } from "./summary-reporter.js"
4
+ export { OutputManager } from "./output-manager.js"
5
+
4
6
  export * from "./formatters/index.js"
@@ -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.formatOption === "github") {
32
- const formatter = new GitHubActionsFormatter()
33
- await formatter.format(allOffenses)
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.formatOption === "github") {
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.formatOption === "github") {
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
- // Calculate padding for alignment
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
- // Add total count and file count
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] // HH:MM:SS format
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
- private argumentParser = new ArgumentParser()
9
- private fileProcessor = new FileProcessor()
10
- private outputManager = new OutputManager()
11
-
12
- private exitWithError(message: string, formatOption: FormatOption, exitCode: number = 1) {
13
- this.outputManager.outputError(message, {
14
- formatOption,
15
- theme: 'auto',
16
- wrapLines: false,
17
- truncateLines: false,
18
- showTiming: false,
19
- startTime: 0,
20
- startDate: new Date()
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
- private exitWithInfo(message: string, formatOption: FormatOption, exitCode: number = 0, timingData?: { startTime: number, startDate: Date, showTiming: boolean }) {
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
- const { pattern, formatOption, showTiming, theme, wrapLines, truncateLines } = this.argumentParser.parse(process.argv)
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 Herb.load()
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 results = await this.fileProcessor.processFiles(files, formatOption)
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)
@@ -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
- private rules: RuleClass[]
8
- private herb: HerbBackend
9
- private offenses: LintOffense[]
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
- private getDefaultRules(): RuleClass[] {
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
- private isLexerRule(rule: Rule): rule is LexerRule {
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
- private isSourceRule(rule: Rule): rule is SourceRule {
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