@herb-tools/linter 0.4.1 → 0.4.3

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 (147) hide show
  1. package/README.md +16 -4
  2. package/dist/herb-lint.js +557 -122
  3. package/dist/herb-lint.js.map +1 -1
  4. package/dist/index.cjs +454 -67
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.js +448 -69
  7. package/dist/index.js.map +1 -1
  8. package/dist/package.json +4 -4
  9. package/dist/src/cli/file-processor.js +2 -4
  10. package/dist/src/cli/file-processor.js.map +1 -1
  11. package/dist/src/default-rules.js +8 -0
  12. package/dist/src/default-rules.js.map +1 -1
  13. package/dist/src/linter.js +37 -6
  14. package/dist/src/linter.js.map +1 -1
  15. package/dist/src/rules/erb-no-empty-tags.js +4 -3
  16. package/dist/src/rules/erb-no-empty-tags.js.map +1 -1
  17. package/dist/src/rules/erb-no-output-control-flow.js +4 -3
  18. package/dist/src/rules/erb-no-output-control-flow.js.map +1 -1
  19. package/dist/src/rules/erb-prefer-image-tag-helper.js +93 -0
  20. package/dist/src/rules/erb-prefer-image-tag-helper.js.map +1 -0
  21. package/dist/src/rules/erb-require-whitespace-inside-tags.js +22 -5
  22. package/dist/src/rules/erb-require-whitespace-inside-tags.js.map +1 -1
  23. package/dist/src/rules/erb-requires-trailing-newline.js +22 -0
  24. package/dist/src/rules/erb-requires-trailing-newline.js.map +1 -0
  25. package/dist/src/rules/html-anchor-require-href.js +4 -3
  26. package/dist/src/rules/html-anchor-require-href.js.map +1 -1
  27. package/dist/src/rules/html-aria-attribute-must-be-valid.js +4 -3
  28. package/dist/src/rules/html-aria-attribute-must-be-valid.js.map +1 -1
  29. package/dist/src/rules/html-aria-level-must-be-valid.js +27 -0
  30. package/dist/src/rules/html-aria-level-must-be-valid.js.map +1 -0
  31. package/dist/src/rules/html-aria-role-heading-requires-level.js +4 -3
  32. package/dist/src/rules/html-aria-role-heading-requires-level.js.map +1 -1
  33. package/dist/src/rules/html-aria-role-must-be-valid.js +4 -3
  34. package/dist/src/rules/html-aria-role-must-be-valid.js.map +1 -1
  35. package/dist/src/rules/html-attribute-double-quotes.js +4 -3
  36. package/dist/src/rules/html-attribute-double-quotes.js.map +1 -1
  37. package/dist/src/rules/html-attribute-values-require-quotes.js +4 -3
  38. package/dist/src/rules/html-attribute-values-require-quotes.js.map +1 -1
  39. package/dist/src/rules/html-boolean-attributes-no-value.js +4 -3
  40. package/dist/src/rules/html-boolean-attributes-no-value.js.map +1 -1
  41. package/dist/src/rules/html-img-require-alt.js +4 -3
  42. package/dist/src/rules/html-img-require-alt.js.map +1 -1
  43. package/dist/src/rules/html-no-block-inside-inline.js +4 -3
  44. package/dist/src/rules/html-no-block-inside-inline.js.map +1 -1
  45. package/dist/src/rules/html-no-duplicate-attributes.js +4 -3
  46. package/dist/src/rules/html-no-duplicate-attributes.js.map +1 -1
  47. package/dist/src/rules/html-no-duplicate-ids.js +4 -3
  48. package/dist/src/rules/html-no-duplicate-ids.js.map +1 -1
  49. package/dist/src/rules/html-no-empty-headings.js +4 -3
  50. package/dist/src/rules/html-no-empty-headings.js.map +1 -1
  51. package/dist/src/rules/html-no-nested-links.js +4 -3
  52. package/dist/src/rules/html-no-nested-links.js.map +1 -1
  53. package/dist/src/rules/html-tag-name-lowercase.js +21 -11
  54. package/dist/src/rules/html-tag-name-lowercase.js.map +1 -1
  55. package/dist/src/rules/index.js +4 -0
  56. package/dist/src/rules/index.js.map +1 -1
  57. package/dist/src/rules/rule-utils.js +168 -2
  58. package/dist/src/rules/rule-utils.js.map +1 -1
  59. package/dist/src/rules/svg-tag-name-capitalization.js +58 -0
  60. package/dist/src/rules/svg-tag-name-capitalization.js.map +1 -0
  61. package/dist/src/types.js +15 -1
  62. package/dist/src/types.js.map +1 -1
  63. package/dist/tsconfig.tsbuildinfo +1 -1
  64. package/dist/types/linter.d.ts +20 -5
  65. package/dist/types/rules/erb-no-empty-tags.d.ts +4 -3
  66. package/dist/types/rules/erb-no-output-control-flow.d.ts +4 -3
  67. package/dist/types/rules/erb-prefer-image-tag-helper.d.ts +7 -0
  68. package/dist/types/rules/erb-require-whitespace-inside-tags.d.ts +4 -3
  69. package/dist/types/rules/erb-requires-trailing-newline.d.ts +6 -0
  70. package/dist/types/rules/html-anchor-require-href.d.ts +4 -3
  71. package/dist/types/rules/html-aria-attribute-must-be-valid.d.ts +4 -3
  72. package/dist/types/rules/html-aria-level-must-be-valid.d.ts +7 -0
  73. package/dist/types/rules/html-aria-role-heading-requires-level.d.ts +4 -3
  74. package/dist/types/rules/html-aria-role-must-be-valid.d.ts +4 -3
  75. package/dist/types/rules/html-attribute-double-quotes.d.ts +4 -3
  76. package/dist/types/rules/html-attribute-values-require-quotes.d.ts +4 -3
  77. package/dist/types/rules/html-boolean-attributes-no-value.d.ts +4 -3
  78. package/dist/types/rules/html-img-require-alt.d.ts +4 -3
  79. package/dist/types/rules/html-no-block-inside-inline.d.ts +4 -3
  80. package/dist/types/rules/html-no-duplicate-attributes.d.ts +4 -3
  81. package/dist/types/rules/html-no-duplicate-ids.d.ts +4 -3
  82. package/dist/types/rules/html-no-empty-headings.d.ts +4 -3
  83. package/dist/types/rules/html-no-nested-links.d.ts +4 -3
  84. package/dist/types/rules/html-tag-name-lowercase.d.ts +4 -3
  85. package/dist/types/rules/index.d.ts +4 -0
  86. package/dist/types/rules/rule-utils.d.ts +82 -4
  87. package/dist/types/rules/svg-tag-name-capitalization.d.ts +7 -0
  88. package/dist/types/src/linter.d.ts +20 -5
  89. package/dist/types/src/rules/erb-no-empty-tags.d.ts +4 -3
  90. package/dist/types/src/rules/erb-no-output-control-flow.d.ts +4 -3
  91. package/dist/types/src/rules/erb-prefer-image-tag-helper.d.ts +7 -0
  92. package/dist/types/src/rules/erb-require-whitespace-inside-tags.d.ts +4 -3
  93. package/dist/types/src/rules/erb-requires-trailing-newline.d.ts +6 -0
  94. package/dist/types/src/rules/html-anchor-require-href.d.ts +4 -3
  95. package/dist/types/src/rules/html-aria-attribute-must-be-valid.d.ts +4 -3
  96. package/dist/types/src/rules/html-aria-level-must-be-valid.d.ts +7 -0
  97. package/dist/types/src/rules/html-aria-role-heading-requires-level.d.ts +4 -3
  98. package/dist/types/src/rules/html-aria-role-must-be-valid.d.ts +4 -3
  99. package/dist/types/src/rules/html-attribute-double-quotes.d.ts +4 -3
  100. package/dist/types/src/rules/html-attribute-values-require-quotes.d.ts +4 -3
  101. package/dist/types/src/rules/html-boolean-attributes-no-value.d.ts +4 -3
  102. package/dist/types/src/rules/html-img-require-alt.d.ts +4 -3
  103. package/dist/types/src/rules/html-no-block-inside-inline.d.ts +4 -3
  104. package/dist/types/src/rules/html-no-duplicate-attributes.d.ts +4 -3
  105. package/dist/types/src/rules/html-no-duplicate-ids.d.ts +4 -3
  106. package/dist/types/src/rules/html-no-empty-headings.d.ts +4 -3
  107. package/dist/types/src/rules/html-no-nested-links.d.ts +4 -3
  108. package/dist/types/src/rules/html-tag-name-lowercase.d.ts +4 -3
  109. package/dist/types/src/rules/index.d.ts +4 -0
  110. package/dist/types/src/rules/rule-utils.d.ts +82 -4
  111. package/dist/types/src/rules/svg-tag-name-capitalization.d.ts +7 -0
  112. package/dist/types/src/types.d.ts +49 -6
  113. package/dist/types/types.d.ts +49 -6
  114. package/docs/rules/README.md +5 -1
  115. package/docs/rules/erb-prefer-image-tag-helper.md +65 -0
  116. package/docs/rules/erb-requires-trailing-newline.md +37 -0
  117. package/docs/rules/html-anchor-require-href.md +1 -1
  118. package/docs/rules/html-aria-level-must-be-valid.md +37 -0
  119. package/docs/rules/svg-tag-name-capitalization.md +57 -0
  120. package/package.json +4 -4
  121. package/src/cli/file-processor.ts +2 -4
  122. package/src/default-rules.ts +8 -0
  123. package/src/linter.ts +42 -8
  124. package/src/rules/erb-no-empty-tags.ts +5 -4
  125. package/src/rules/erb-no-output-control-flow.ts +6 -4
  126. package/src/rules/erb-prefer-image-tag-helper.ts +124 -0
  127. package/src/rules/erb-require-whitespace-inside-tags.ts +38 -6
  128. package/src/rules/erb-requires-trailing-newline.ts +27 -0
  129. package/src/rules/html-anchor-require-href.ts +5 -4
  130. package/src/rules/html-aria-attribute-must-be-valid.ts +5 -5
  131. package/src/rules/html-aria-level-must-be-valid.ts +42 -0
  132. package/src/rules/html-aria-role-heading-requires-level.ts +5 -4
  133. package/src/rules/html-aria-role-must-be-valid.ts +5 -4
  134. package/src/rules/html-attribute-double-quotes.ts +5 -4
  135. package/src/rules/html-attribute-values-require-quotes.ts +5 -4
  136. package/src/rules/html-boolean-attributes-no-value.ts +5 -4
  137. package/src/rules/html-img-require-alt.ts +5 -4
  138. package/src/rules/html-no-block-inside-inline.ts +5 -4
  139. package/src/rules/html-no-duplicate-attributes.ts +5 -4
  140. package/src/rules/html-no-duplicate-ids.ts +5 -5
  141. package/src/rules/html-no-empty-headings.ts +5 -4
  142. package/src/rules/html-no-nested-links.ts +5 -4
  143. package/src/rules/html-tag-name-lowercase.ts +29 -13
  144. package/src/rules/index.ts +4 -0
  145. package/src/rules/rule-utils.ts +203 -4
  146. package/src/rules/svg-tag-name-capitalization.ts +74 -0
  147. package/src/types.ts +60 -6
@@ -1,5 +1,7 @@
1
1
  import {
2
- Visitor
2
+ Visitor,
3
+ Position,
4
+ Location
3
5
  } from "@herb-tools/core"
4
6
 
5
7
  import type {
@@ -10,9 +12,11 @@ import type {
10
12
  HTMLOpenTagNode,
11
13
  HTMLSelfCloseTagNode,
12
14
  LiteralNode,
13
- Location
15
+ LexResult,
16
+ Token
14
17
  } from "@herb-tools/core"
15
- import type { LintOffense, LintSeverity, } from "../types.js"
18
+ import type { LintOffense, LintSeverity, LintContext } from "../types.js"
19
+ import { DEFAULT_LINT_CONTEXT } from "../types.js"
16
20
 
17
21
  /**
18
22
  * Base visitor class that provides common functionality for rule visitors
@@ -20,11 +24,13 @@ import type { LintOffense, LintSeverity, } from "../types.js"
20
24
  export abstract class BaseRuleVisitor extends Visitor {
21
25
  public readonly offenses: LintOffense[] = []
22
26
  protected ruleName: string
27
+ protected context: LintContext
23
28
 
24
- constructor(ruleName: string) {
29
+ constructor(ruleName: string, context?: Partial<LintContext>) {
25
30
  super()
26
31
 
27
32
  this.ruleName = ruleName
33
+ this.context = { ...DEFAULT_LINT_CONTEXT, ...context }
28
34
  }
29
35
 
30
36
  /**
@@ -188,6 +194,53 @@ export const HTML_BOOLEAN_ATTRIBUTES = new Set([
188
194
 
189
195
  export const HEADING_TAGS = new Set(["h1", "h2", "h3", "h4", "h5", "h6"])
190
196
 
197
+ /**
198
+ * SVG elements that use camelCase naming
199
+ */
200
+ export const SVG_CAMEL_CASE_ELEMENTS = new Set([
201
+ "animateMotion",
202
+ "animateTransform",
203
+ "clipPath",
204
+ "feBlend",
205
+ "feColorMatrix",
206
+ "feComponentTransfer",
207
+ "feComposite",
208
+ "feConvolveMatrix",
209
+ "feDiffuseLighting",
210
+ "feDisplacementMap",
211
+ "feDistantLight",
212
+ "feDropShadow",
213
+ "feFlood",
214
+ "feFuncA",
215
+ "feFuncB",
216
+ "feFuncG",
217
+ "feFuncR",
218
+ "feGaussianBlur",
219
+ "feImage",
220
+ "feMerge",
221
+ "feMergeNode",
222
+ "feMorphology",
223
+ "feOffset",
224
+ "fePointLight",
225
+ "feSpecularLighting",
226
+ "feSpotLight",
227
+ "feTile",
228
+ "feTurbulence",
229
+ "foreignObject",
230
+ "glyphRef",
231
+ "linearGradient",
232
+ "radialGradient",
233
+ "textPath"
234
+ ])
235
+
236
+ /**
237
+ * Mapping from lowercase SVG element names to their correct camelCase versions
238
+ * Generated dynamically from SVG_CAMEL_CASE_ELEMENTS
239
+ */
240
+ export const SVG_LOWERCASE_TO_CAMELCASE = new Map(
241
+ Array.from(SVG_CAMEL_CASE_ELEMENTS).map(element => [element.toLowerCase(), element])
242
+ )
243
+
191
244
  export const VALID_ARIA_ROLES = new Set([
192
245
  "banner", "complementary", "contentinfo", "form", "main", "navigation", "region", "search",
193
246
  "article", "cell", "columnheader", "definition", "directory", "document", "feed", "figure",
@@ -252,6 +305,22 @@ export const ARIA_ATTRIBUTES = new Set([
252
305
  'aria-valuetext',
253
306
  ])
254
307
 
308
+ /**
309
+ * Helper function to create a location at the end of the source with a 1-character range
310
+ */
311
+ export function createEndOfFileLocation(source: string): Location {
312
+ const lines = source.split('\n')
313
+ const lastLineNumber = lines.length
314
+ const lastLine = lines[lines.length - 1]
315
+ const lastColumnNumber = lastLine.length
316
+
317
+ const startColumn = lastColumnNumber > 0 ? lastColumnNumber - 1 : 0
318
+ const start = new Position(lastLineNumber, startColumn)
319
+ const end = new Position(lastLineNumber, lastColumnNumber)
320
+
321
+ return new Location(start, end)
322
+ }
323
+
255
324
  /**
256
325
  * Checks if an element is inline
257
326
  */
@@ -279,6 +348,10 @@ export function isBooleanAttribute(attributeName: string): boolean {
279
348
  * and attribute iteration logic. Provides simplified interface with extracted attribute info.
280
349
  */
281
350
  export abstract class AttributeVisitorMixin extends BaseRuleVisitor {
351
+ constructor(ruleName: string, context?: Partial<LintContext>) {
352
+ super(ruleName, context)
353
+ }
354
+
282
355
  visitHTMLOpenTagNode(node: HTMLOpenTagNode): void {
283
356
  this.checkAttributesOnNode(node)
284
357
  super.visitHTMLOpenTagNode(node)
@@ -336,3 +409,129 @@ export function forEachAttribute(
336
409
  }
337
410
  }
338
411
  }
412
+
413
+ /**
414
+ * Base lexer visitor class that provides common functionality for lexer-based rule visitors
415
+ */
416
+ export abstract class BaseLexerRuleVisitor {
417
+ public readonly offenses: LintOffense[] = []
418
+ protected ruleName: string
419
+ protected context: LintContext
420
+
421
+ constructor(ruleName: string, context?: Partial<LintContext>) {
422
+ this.ruleName = ruleName
423
+ this.context = { ...DEFAULT_LINT_CONTEXT, ...context }
424
+ }
425
+
426
+ /**
427
+ * Helper method to create a lint offense for lexer rules
428
+ */
429
+ protected createOffense(message: string, location: Location, severity: LintSeverity = "error"): LintOffense {
430
+ return {
431
+ rule: this.ruleName,
432
+ code: this.ruleName,
433
+ source: "Herb Linter",
434
+ message,
435
+ location,
436
+ severity,
437
+ }
438
+ }
439
+
440
+ /**
441
+ * Helper method to add an offense to the offenses array
442
+ */
443
+ protected addOffense(message: string, location: Location, severity: LintSeverity = "error"): void {
444
+ this.offenses.push(this.createOffense(message, location, severity))
445
+ }
446
+
447
+ /**
448
+ * Main entry point for lexer rule visitors
449
+ * @param lexResult - The lexer result containing tokens and source
450
+ */
451
+ visit(lexResult: LexResult): void {
452
+ this.visitTokens(lexResult.value.tokens)
453
+ }
454
+
455
+ /**
456
+ * Visit all tokens
457
+ * Override this method to implement token-level checks
458
+ */
459
+ protected visitTokens(tokens: Token[]): void {
460
+ for (const token of tokens) {
461
+ this.visitToken(token)
462
+ }
463
+ }
464
+
465
+ /**
466
+ * Visit individual tokens
467
+ * Override this method to implement per-token checks
468
+ */
469
+ protected visitToken(_token: Token): void {
470
+ // Default implementation does nothing
471
+ }
472
+
473
+ }
474
+
475
+ /**
476
+ * Base source visitor class that provides common functionality for source-based rule visitors
477
+ */
478
+ export abstract class BaseSourceRuleVisitor {
479
+ public readonly offenses: LintOffense[] = []
480
+ protected ruleName: string
481
+ protected context: LintContext
482
+
483
+ constructor(ruleName: string, context?: Partial<LintContext>) {
484
+ this.ruleName = ruleName
485
+ this.context = { ...DEFAULT_LINT_CONTEXT, ...context }
486
+ }
487
+
488
+ /**
489
+ * Helper method to create a lint offense for source rules
490
+ */
491
+ protected createOffense(message: string, location: Location, severity: LintSeverity = "error"): LintOffense {
492
+ return {
493
+ rule: this.ruleName as any, // Type assertion for compatibility
494
+ code: this.ruleName,
495
+ source: "Herb Linter",
496
+ message,
497
+ location,
498
+ severity,
499
+ }
500
+ }
501
+
502
+ /**
503
+ * Helper method to add an offense to the offenses array
504
+ */
505
+ protected addOffense(message: string, location: Location, severity: LintSeverity = "error"): void {
506
+ this.offenses.push(this.createOffense(message, location, severity))
507
+ }
508
+
509
+ /**
510
+ * Main entry point for source rule visitors
511
+ * @param source - The raw source code
512
+ */
513
+ visit(source: string): void {
514
+ this.visitSource(source)
515
+ }
516
+
517
+ /**
518
+ * Visit the source code directly
519
+ * Override this method to implement source-level checks
520
+ */
521
+ protected abstract visitSource(source: string): void
522
+
523
+ /**
524
+ * Helper method to create a location for a specific position in the source
525
+ */
526
+ protected createLocationAt(source: string, position: number): Location {
527
+ const beforePosition = source.substring(0, position)
528
+ const lines = beforePosition.split('\n')
529
+ const line = lines.length
530
+ const column = lines[lines.length - 1].length + 1
531
+
532
+ const start = new Position(line, column)
533
+ const end = new Position(line, column)
534
+
535
+ return new Location(start, end)
536
+ }
537
+ }
@@ -0,0 +1,74 @@
1
+ import { BaseRuleVisitor, SVG_CAMEL_CASE_ELEMENTS, SVG_LOWERCASE_TO_CAMELCASE } from "./rule-utils.js"
2
+
3
+ import { ParserRule } from "../types.js"
4
+ import type { LintOffense, LintContext } from "../types.js"
5
+ import type { HTMLElementNode, HTMLOpenTagNode, HTMLCloseTagNode, HTMLSelfCloseTagNode, Node } from "@herb-tools/core"
6
+
7
+ class SVGTagNameCapitalizationVisitor extends BaseRuleVisitor {
8
+ private insideSVG = false
9
+
10
+ visitHTMLElementNode(node: HTMLElementNode): void {
11
+ const tagName = node.tag_name?.value
12
+
13
+ if (tagName && ["svg"].includes(tagName.toLowerCase())) {
14
+ const wasInsideSVG = this.insideSVG
15
+ this.insideSVG = true
16
+ this.visitChildNodes(node)
17
+ this.insideSVG = wasInsideSVG
18
+ return
19
+ }
20
+
21
+ if (this.insideSVG) {
22
+ if (node.open_tag) {
23
+ this.checkTagName(node.open_tag as HTMLOpenTagNode)
24
+ }
25
+ if (node.close_tag) {
26
+ this.checkTagName(node.close_tag as HTMLCloseTagNode)
27
+ }
28
+ }
29
+
30
+ this.visitChildNodes(node)
31
+ }
32
+
33
+ visitHTMLSelfCloseTagNode(node: HTMLSelfCloseTagNode): void {
34
+ if (this.insideSVG) {
35
+ this.checkTagName(node)
36
+ }
37
+ this.visitChildNodes(node)
38
+ }
39
+
40
+ private checkTagName(node: HTMLOpenTagNode | HTMLCloseTagNode | HTMLSelfCloseTagNode): void {
41
+ const tagName = node.tag_name?.value
42
+
43
+ if (!tagName) return
44
+
45
+ if (SVG_CAMEL_CASE_ELEMENTS.has(tagName)) return
46
+
47
+ const lowercaseTagName = tagName.toLowerCase()
48
+ const correctCamelCase = SVG_LOWERCASE_TO_CAMELCASE.get(lowercaseTagName)
49
+
50
+ if (correctCamelCase && tagName !== correctCamelCase) {
51
+ let type: string = node.type
52
+
53
+ if (node.type == "AST_HTML_OPEN_TAG_NODE") type = "Opening"
54
+ if (node.type == "AST_HTML_CLOSE_TAG_NODE") type = "Closing"
55
+ if (node.type == "AST_HTML_SELF_CLOSE_TAG_NODE") type = "Self-closing"
56
+
57
+ this.addOffense(
58
+ `${type} SVG tag name \`${tagName}\` should use proper capitalization. Use \`${correctCamelCase}\` instead.`,
59
+ node.tag_name!.location,
60
+ "error"
61
+ )
62
+ }
63
+ }
64
+ }
65
+
66
+ export class SVGTagNameCapitalizationRule extends ParserRule {
67
+ name = "svg-tag-name-capitalization"
68
+
69
+ check(node: Node, context?: Partial<LintContext>): LintOffense[] {
70
+ const visitor = new SVGTagNameCapitalizationVisitor(this.name, context)
71
+ visitor.visit(node)
72
+ return visitor.offenses
73
+ }
74
+ }
package/src/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Node, Diagnostic } from "@herb-tools/core"
1
+ import { Node, Diagnostic, LexResult } from "@herb-tools/core"
2
2
  import type { defaultRules } from "./default-rules.js"
3
3
 
4
4
  export type LintSeverity = "error" | "warning"
@@ -20,13 +20,67 @@ export interface LintResult {
20
20
  warnings: number
21
21
  }
22
22
 
23
- export interface Rule {
24
- name: string
25
- check(node: Node): LintOffense[]
23
+ export abstract class ParserRule {
24
+ static type = "parser" as const
25
+ abstract name: string
26
+ abstract check(node: Node, context?: Partial<LintContext>): LintOffense[]
27
+ }
28
+
29
+ export abstract class LexerRule {
30
+ static type = "lexer" as const
31
+ abstract name: string
32
+ abstract check(lexResult: LexResult, context?: Partial<LintContext>): LintOffense[]
33
+ }
34
+
35
+ export interface LexerRuleConstructor {
36
+ type: "lexer"
37
+ new (): LexerRule
38
+ }
39
+
40
+ /**
41
+ * Complete lint context with all properties defined.
42
+ * Use Partial<LintContext> when passing context to rules.
43
+ */
44
+ export interface LintContext {
45
+ fileName: string | undefined
26
46
  }
27
47
 
28
48
  /**
29
- * Type representing a rule class constructor.
49
+ * Default context object with all keys defined but set to undefined
50
+ */
51
+ export const DEFAULT_LINT_CONTEXT: LintContext = {
52
+ fileName: undefined
53
+ } as const
54
+
55
+ export abstract class SourceRule {
56
+ static type = "source" as const
57
+ abstract name: string
58
+ abstract check(source: string, context?: Partial<LintContext>): LintOffense[]
59
+ }
60
+
61
+ export interface SourceRuleConstructor {
62
+ type: "source"
63
+ new (): SourceRule
64
+ }
65
+
66
+ /**
67
+ * Type representing a parser/AST rule class constructor.
30
68
  * The Linter accepts rule classes rather than instances for better performance and memory usage.
69
+ * Parser rules are the default and don't require static properties.
70
+ */
71
+ export type ParserRuleClass = (new () => ParserRule) & {
72
+ type?: "parser"
73
+ }
74
+
75
+ export type LexerRuleClass = LexerRuleConstructor
76
+ export type SourceRuleClass = SourceRuleConstructor
77
+
78
+ /**
79
+ * Union type for any rule instance (Parser/AST, Lexer, or Source)
80
+ */
81
+ export type Rule = ParserRule | LexerRule | SourceRule
82
+
83
+ /**
84
+ * Union type for any rule class (Parser/AST, Lexer, or Source)
31
85
  */
32
- export type RuleClass = new () => Rule
86
+ export type RuleClass = ParserRuleClass | LexerRuleClass | SourceRuleClass