@herb-tools/linter 0.8.6 → 0.8.8

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 (82) hide show
  1. package/README.md +54 -2
  2. package/dist/herb-lint.js +17157 -31275
  3. package/dist/herb-lint.js.map +1 -1
  4. package/dist/index.cjs +473 -2113
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.js +468 -2115
  7. package/dist/index.js.map +1 -1
  8. package/dist/loader.cjs +6868 -11350
  9. package/dist/loader.cjs.map +1 -1
  10. package/dist/loader.js +6862 -11351
  11. package/dist/loader.js.map +1 -1
  12. package/dist/package.json +9 -8
  13. package/dist/src/cli/argument-parser.js +18 -2
  14. package/dist/src/cli/argument-parser.js.map +1 -1
  15. package/dist/src/cli/file-processor.js +1 -1
  16. package/dist/src/cli/file-processor.js.map +1 -1
  17. package/dist/src/cli.js +25 -10
  18. package/dist/src/cli.js.map +1 -1
  19. package/dist/src/custom-rule-loader.js +2 -2
  20. package/dist/src/custom-rule-loader.js.map +1 -1
  21. package/dist/src/linter.js +16 -3
  22. package/dist/src/linter.js.map +1 -1
  23. package/dist/src/rules/erb-strict-locals-comment-syntax.js +206 -0
  24. package/dist/src/rules/erb-strict-locals-comment-syntax.js.map +1 -0
  25. package/dist/src/rules/erb-strict-locals-required.js +38 -0
  26. package/dist/src/rules/erb-strict-locals-required.js.map +1 -0
  27. package/dist/src/rules/file-utils.js +21 -0
  28. package/dist/src/rules/file-utils.js.map +1 -0
  29. package/dist/src/rules/html-head-only-elements.js +2 -0
  30. package/dist/src/rules/html-head-only-elements.js.map +1 -1
  31. package/dist/src/rules/html-no-duplicate-attributes.js +91 -21
  32. package/dist/src/rules/html-no-duplicate-attributes.js.map +1 -1
  33. package/dist/src/rules/html-no-empty-headings.js +22 -36
  34. package/dist/src/rules/html-no-empty-headings.js.map +1 -1
  35. package/dist/src/rules/index.js +4 -0
  36. package/dist/src/rules/index.js.map +1 -1
  37. package/dist/src/rules/string-utils.js +72 -0
  38. package/dist/src/rules/string-utils.js.map +1 -0
  39. package/dist/src/rules.js +4 -0
  40. package/dist/src/rules.js.map +1 -1
  41. package/dist/src/types.js +6 -0
  42. package/dist/src/types.js.map +1 -1
  43. package/dist/tsconfig.tsbuildinfo +1 -1
  44. package/dist/types/cli/argument-parser.d.ts +3 -0
  45. package/dist/types/cli/file-processor.d.ts +1 -0
  46. package/dist/types/cli.d.ts +1 -1
  47. package/dist/types/linter.d.ts +5 -1
  48. package/dist/types/rules/erb-strict-locals-comment-syntax.d.ts +9 -0
  49. package/dist/types/rules/erb-strict-locals-required.d.ts +9 -0
  50. package/dist/types/rules/file-utils.d.ts +13 -0
  51. package/dist/types/rules/index.d.ts +4 -0
  52. package/dist/types/rules/string-utils.d.ts +15 -0
  53. package/dist/types/src/cli/argument-parser.d.ts +3 -0
  54. package/dist/types/src/cli/file-processor.d.ts +1 -0
  55. package/dist/types/src/cli.d.ts +1 -1
  56. package/dist/types/src/linter.d.ts +5 -1
  57. package/dist/types/src/rules/erb-strict-locals-comment-syntax.d.ts +9 -0
  58. package/dist/types/src/rules/erb-strict-locals-required.d.ts +9 -0
  59. package/dist/types/src/rules/file-utils.d.ts +13 -0
  60. package/dist/types/src/rules/index.d.ts +4 -0
  61. package/dist/types/src/rules/string-utils.d.ts +15 -0
  62. package/dist/types/src/types.d.ts +6 -0
  63. package/dist/types/types.d.ts +6 -0
  64. package/docs/rules/README.md +1 -0
  65. package/docs/rules/erb-strict-locals-comment-syntax.md +153 -0
  66. package/docs/rules/erb-strict-locals-required.md +107 -0
  67. package/package.json +9 -8
  68. package/src/cli/argument-parser.ts +21 -2
  69. package/src/cli/file-processor.ts +2 -1
  70. package/src/cli.ts +34 -11
  71. package/src/custom-rule-loader.ts +2 -2
  72. package/src/linter.ts +19 -3
  73. package/src/rules/erb-strict-locals-comment-syntax.ts +274 -0
  74. package/src/rules/erb-strict-locals-required.ts +52 -0
  75. package/src/rules/file-utils.ts +23 -0
  76. package/src/rules/html-head-only-elements.ts +1 -0
  77. package/src/rules/html-no-duplicate-attributes.ts +141 -26
  78. package/src/rules/html-no-empty-headings.ts +21 -44
  79. package/src/rules/index.ts +4 -0
  80. package/src/rules/string-utils.ts +72 -0
  81. package/src/rules.ts +4 -0
  82. package/src/types.ts +6 -0
@@ -1,48 +1,163 @@
1
- import { ParserRule } from "../types.js"
2
- import { AttributeVisitorMixin, StaticAttributeStaticValueParams, StaticAttributeDynamicValueParams } from "./rule-utils.js"
1
+ import { ParserRule, BaseAutofixContext } from "../types.js"
2
+ import { ControlFlowTrackingVisitor, ControlFlowType, getAttributeName } from "./rule-utils.js"
3
3
 
4
4
  import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
5
- import type { HTMLOpenTagNode, HTMLAttributeNode, ParseResult } from "@herb-tools/core"
5
+ import type { HTMLOpenTagNode, HTMLAttributeNode, ParseResult, Location } from "@herb-tools/core"
6
6
 
7
- class NoDuplicateAttributesVisitor extends AttributeVisitorMixin {
8
- private attributeNames = new Map<string, HTMLAttributeNode[]>()
7
+ interface ControlFlowState {
8
+ previousBranchAttributes: Set<string>
9
+ previousControlFlowAttributes: Set<string>
10
+ }
11
+
12
+ interface BranchState {
13
+ previousBranchAttributes: Set<string>
14
+ }
15
+
16
+ class NoDuplicateAttributesVisitor extends ControlFlowTrackingVisitor<
17
+ BaseAutofixContext,
18
+ ControlFlowState,
19
+ BranchState
20
+ > {
21
+ private tagAttributes = new Set<string>()
22
+ private currentBranchAttributes = new Set<string>()
23
+ private controlFlowAttributes = new Set<string>()
9
24
 
10
25
  visitHTMLOpenTagNode(node: HTMLOpenTagNode): void {
11
- this.attributeNames.clear()
26
+ this.tagAttributes = new Set()
27
+ this.currentBranchAttributes = new Set()
28
+ this.controlFlowAttributes = new Set()
12
29
  super.visitHTMLOpenTagNode(node)
13
- this.reportDuplicates()
14
30
  }
15
31
 
32
+ visitHTMLAttributeNode(node: HTMLAttributeNode): void {
33
+ this.checkAttribute(node)
34
+ }
35
+
36
+ protected onEnterControlFlow(_controlFlowType: ControlFlowType, wasAlreadyInControlFlow: boolean): ControlFlowState {
37
+ const stateToRestore: ControlFlowState = {
38
+ previousBranchAttributes: this.currentBranchAttributes,
39
+ previousControlFlowAttributes: this.controlFlowAttributes,
40
+ }
41
+
42
+ this.currentBranchAttributes = new Set()
43
+
44
+ if (!wasAlreadyInControlFlow) {
45
+ this.controlFlowAttributes = new Set()
46
+ }
47
+
48
+ return stateToRestore
49
+ }
50
+
51
+ protected onExitControlFlow(
52
+ controlFlowType: ControlFlowType,
53
+ wasAlreadyInControlFlow: boolean,
54
+ stateToRestore: ControlFlowState,
55
+ ): void {
56
+ if (controlFlowType === ControlFlowType.CONDITIONAL && !wasAlreadyInControlFlow) {
57
+ this.controlFlowAttributes.forEach((attr) => this.tagAttributes.add(attr))
58
+ }
59
+
60
+ this.currentBranchAttributes = stateToRestore.previousBranchAttributes
61
+ this.controlFlowAttributes = stateToRestore.previousControlFlowAttributes
62
+ }
63
+
64
+ protected onEnterBranch(): BranchState {
65
+ const stateToRestore: BranchState = {
66
+ previousBranchAttributes: this.currentBranchAttributes,
67
+ }
68
+
69
+ if (this.isInControlFlow) {
70
+ this.currentBranchAttributes = new Set()
71
+ }
72
+
73
+ return stateToRestore
74
+ }
75
+
76
+ protected onExitBranch(_stateToRestore: BranchState): void {}
77
+
78
+ private checkAttribute(attributeNode: HTMLAttributeNode): void {
79
+ const identifier = getAttributeName(attributeNode)
80
+ if (!identifier) return
16
81
 
17
- protected checkStaticAttributeStaticValue({ attributeName, attributeNode }: StaticAttributeStaticValueParams): void {
18
- this.trackAttributeName(attributeName, attributeNode)
82
+ this.processAttributeDuplicate(identifier, attributeNode)
19
83
  }
20
84
 
21
- protected checkStaticAttributeDynamicValue({ attributeName, attributeNode }: StaticAttributeDynamicValueParams): void {
22
- this.trackAttributeName(attributeName, attributeNode)
85
+ private processAttributeDuplicate(identifier: string, attributeNode: HTMLAttributeNode): void {
86
+ if (!this.isInControlFlow) {
87
+ this.handleHTMLAttribute(identifier, attributeNode)
88
+ return
89
+ }
90
+
91
+ if (this.currentControlFlowType === ControlFlowType.LOOP) {
92
+ this.handleLoopAttribute(identifier, attributeNode)
93
+ } else {
94
+ this.handleConditionalAttribute(identifier, attributeNode)
95
+ }
96
+
97
+ this.currentBranchAttributes.add(identifier)
23
98
  }
24
99
 
25
- private trackAttributeName(attributeName: string, attributeNode: HTMLAttributeNode): void {
26
- if (!this.attributeNames.has(attributeName)) {
27
- this.attributeNames.set(attributeName, [])
100
+ private handleHTMLAttribute(identifier: string, attributeNode: HTMLAttributeNode): void {
101
+ if (this.tagAttributes.has(identifier)) {
102
+ this.addDuplicateAttributeOffense(identifier, attributeNode.name!.location)
103
+ }
104
+
105
+ this.tagAttributes.add(identifier)
106
+ }
107
+
108
+ private handleLoopAttribute(identifier: string, attributeNode: HTMLAttributeNode): void {
109
+ if (this.currentBranchAttributes.has(identifier)) {
110
+ this.addSameLoopIterationOffense(identifier, attributeNode.name!.location)
111
+ return
112
+ }
113
+
114
+ if (this.tagAttributes.has(identifier)) {
115
+ this.addDuplicateAttributeOffense(identifier, attributeNode.name!.location)
116
+ return
28
117
  }
29
118
 
30
- this.attributeNames.get(attributeName)!.push(attributeNode)
119
+ this.addLoopWillDuplicateOffense(identifier, attributeNode.name!.location)
31
120
  }
32
121
 
33
- private reportDuplicates(): void {
34
- for (const [attributeName, attributeNodes] of this.attributeNames) {
35
- if (attributeNodes.length > 1) {
36
- for (let i = 1; i < attributeNodes.length; i++) {
37
- const attributeNode = attributeNodes[i]
122
+ private handleConditionalAttribute(identifier: string, attributeNode: HTMLAttributeNode): void {
123
+ if (this.currentBranchAttributes.has(identifier)) {
124
+ this.addSameBranchOffense(identifier, attributeNode.name!.location)
125
+ return
126
+ }
38
127
 
39
- this.addOffense(
40
- `Duplicate attribute \`${attributeName}\` found on tag. Remove the duplicate occurrence.`,
41
- attributeNode.name!.location,
42
- )
43
- }
44
- }
128
+ if (this.tagAttributes.has(identifier)) {
129
+ this.addDuplicateAttributeOffense(identifier, attributeNode.name!.location)
45
130
  }
131
+
132
+ this.controlFlowAttributes.add(identifier)
133
+ }
134
+
135
+ private addDuplicateAttributeOffense(identifier: string, location: Location): void {
136
+ this.addOffense(
137
+ `Duplicate attribute \`${identifier}\`. Browsers only use the first occurrence and ignore duplicate attributes. Remove the duplicate or merge the values.`,
138
+ location,
139
+ )
140
+ }
141
+
142
+ private addSameLoopIterationOffense(identifier: string, location: Location): void {
143
+ this.addOffense(
144
+ `Duplicate attribute \`${identifier}\` in same loop iteration. Each iteration will produce an element with duplicate attributes. Remove one or merge the values.`,
145
+ location,
146
+ )
147
+ }
148
+
149
+ private addLoopWillDuplicateOffense(identifier: string, location: Location): void {
150
+ this.addOffense(
151
+ `Attribute \`${identifier}\` inside loop will appear multiple times on this element. Use a dynamic attribute name like \`${identifier}-<%= index %>\` or move the attribute outside the loop.`,
152
+ location,
153
+ )
154
+ }
155
+
156
+ private addSameBranchOffense(identifier: string, location: Location): void {
157
+ this.addOffense(
158
+ `Duplicate attribute \`${identifier}\` in same branch. This branch will produce an element with duplicate attributes. Remove one or merge the values.`,
159
+ location,
160
+ )
46
161
  }
47
162
  }
48
163
 
@@ -1,31 +1,29 @@
1
1
  import { BaseRuleVisitor, getTagName, getAttributes, findAttributeByName, getAttributeValue, HEADING_TAGS } from "./rule-utils.js"
2
+ import { isHTMLOpenTagNode, isLiteralNode, isHTMLTextNode, isHTMLElementNode } from "@herb-tools/core"
2
3
 
3
4
  import { ParserRule } from "../types.js"
4
5
 
5
6
  import type { UnboundLintOffense, LintContext, FullRuleConfig } from "../types.js"
6
- import type { HTMLElementNode, HTMLOpenTagNode, ParseResult, LiteralNode, HTMLTextNode } from "@herb-tools/core"
7
+ import type { HTMLElementNode, HTMLOpenTagNode, ParseResult } from "@herb-tools/core"
7
8
 
8
9
  class NoEmptyHeadingsVisitor extends BaseRuleVisitor {
9
10
  visitHTMLElementNode(node: HTMLElementNode): void {
11
+ const tagName = getTagName(node.open_tag)?.toLowerCase()
12
+ if (tagName === "template") return
13
+
10
14
  this.checkHeadingElement(node)
11
15
  super.visitHTMLElementNode(node)
12
16
  }
13
17
 
14
-
15
18
  private checkHeadingElement(node: HTMLElementNode): void {
16
- if (!node.open_tag || node.open_tag.type !== "AST_HTML_OPEN_TAG_NODE") {
17
- return
18
- }
19
+ if (!node.open_tag) return
20
+ if (!isHTMLOpenTagNode(node.open_tag)) return
19
21
 
20
- const openTag = node.open_tag as HTMLOpenTagNode
21
- const tagName = getTagName(openTag)
22
-
23
- if (!tagName) {
24
- return
25
- }
22
+ const tagName = getTagName(node.open_tag)
23
+ if (!tagName) return
26
24
 
27
25
  const isStandardHeading = HEADING_TAGS.has(tagName)
28
- const isAriaHeading = this.hasHeadingRole(openTag)
26
+ const isAriaHeading = this.hasHeadingRole(node.open_tag)
29
27
 
30
28
  if (!isStandardHeading && !isAriaHeading) {
31
29
  return
@@ -43,7 +41,6 @@ class NoEmptyHeadingsVisitor extends BaseRuleVisitor {
43
41
  }
44
42
  }
45
43
 
46
-
47
44
  private isEmptyHeading(node: HTMLElementNode): boolean {
48
45
  if (!node.body || node.body.length === 0) {
49
46
  return true
@@ -52,24 +49,13 @@ class NoEmptyHeadingsVisitor extends BaseRuleVisitor {
52
49
  let hasAccessibleContent = false
53
50
 
54
51
  for (const child of node.body) {
55
- if (child.type === "AST_LITERAL_NODE") {
56
- const literalNode = child as LiteralNode
57
-
58
- if (literalNode.content.trim().length > 0) {
52
+ if (isLiteralNode(child) || isHTMLTextNode(child)) {
53
+ if (child.content.trim().length > 0) {
59
54
  hasAccessibleContent = true
60
55
  break
61
56
  }
62
- } else if (child.type === "AST_HTML_TEXT_NODE") {
63
- const textNode = child as HTMLTextNode
64
-
65
- if (textNode.content.trim().length > 0) {
66
- hasAccessibleContent = true
67
- break
68
- }
69
- } else if (child.type === "AST_HTML_ELEMENT_NODE") {
70
- const elementNode = child as HTMLElementNode
71
-
72
- if (this.isElementAccessible(elementNode)) {
57
+ } else if (isHTMLElementNode(child)) {
58
+ if (this.isElementAccessible(child)) {
73
59
  hasAccessibleContent = true
74
60
  break
75
61
  }
@@ -95,12 +81,10 @@ class NoEmptyHeadingsVisitor extends BaseRuleVisitor {
95
81
  }
96
82
 
97
83
  private isElementAccessible(node: HTMLElementNode): boolean {
98
- if (!node.open_tag || node.open_tag.type !== "AST_HTML_OPEN_TAG_NODE") {
99
- return true
100
- }
84
+ if (!node.open_tag) return true
85
+ if (!isHTMLOpenTagNode(node.open_tag)) return true
101
86
 
102
- const openTag = node.open_tag as HTMLOpenTagNode
103
- const attributes = getAttributes(openTag)
87
+ const attributes = getAttributes(node.open_tag)
104
88
  const ariaHiddenAttribute = findAttributeByName(attributes, "aria-hidden")
105
89
 
106
90
  if (ariaHiddenAttribute) {
@@ -116,19 +100,12 @@ class NoEmptyHeadingsVisitor extends BaseRuleVisitor {
116
100
  }
117
101
 
118
102
  for (const child of node.body) {
119
- if (child.type === "AST_LITERAL_NODE") {
120
- const literalNode = child as LiteralNode
121
- if (literalNode.content.trim().length > 0) {
122
- return true
123
- }
124
- } else if (child.type === "AST_HTML_TEXT_NODE") {
125
- const textNode = child as HTMLTextNode
126
- if (textNode.content.trim().length > 0) {
103
+ if (isLiteralNode(child) || isHTMLTextNode(child)) {
104
+ if (child.content.trim().length > 0) {
127
105
  return true
128
106
  }
129
- } else if (child.type === "AST_HTML_ELEMENT_NODE") {
130
- const elementNode = child as HTMLElementNode
131
- if (this.isElementAccessible(elementNode)) {
107
+ } else if (isHTMLElementNode(child)) {
108
+ if (this.isElementAccessible(child)) {
132
109
  return true
133
110
  }
134
111
  } else {
@@ -1,4 +1,6 @@
1
1
  export * from "./rule-utils.js"
2
+ export * from "./file-utils.js"
3
+ export * from "./string-utils.js"
2
4
  export * from "./herb-disable-comment-base.js"
3
5
 
4
6
  export * from "./erb-comment-syntax.js"
@@ -12,6 +14,8 @@ export * from "./erb-prefer-image-tag-helper.js"
12
14
  export * from "./erb-require-trailing-newline.js"
13
15
  export * from "./erb-require-whitespace-inside-tags.js"
14
16
  export * from "./erb-right-trim.js"
17
+ export * from "./erb-strict-locals-comment-syntax.js"
18
+ export * from "./erb-strict-locals-required.js"
15
19
 
16
20
  export * from "./herb-disable-comment-valid-rule-name.js"
17
21
  export * from "./herb-disable-comment-no-redundant-all.js"
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Checks if parentheses in a string are balanced
3
+ * Returns false if there are more closing parens than opening at any point
4
+ */
5
+ export function hasBalancedParentheses(content: string): boolean {
6
+ let depth = 0
7
+
8
+ for (const char of content) {
9
+ if (char === "(") depth++
10
+ if (char === ")") depth--
11
+ if (depth < 0) return false
12
+ }
13
+
14
+ return depth === 0
15
+ }
16
+
17
+ /**
18
+ * Splits a string by commas at the top level only
19
+ * Respects nested parentheses, brackets, braces, and strings
20
+ *
21
+ * @example
22
+ * splitByTopLevelComma("a, b, c") // ["a", " b", " c"]
23
+ * splitByTopLevelComma("a, (b, c), d") // ["a", " (b, c)", " d"]
24
+ * splitByTopLevelComma('a, "b, c", d') // ["a", ' "b, c"', " d"]
25
+ */
26
+ export function splitByTopLevelComma(str: string): string[] {
27
+ const result: string[] = []
28
+
29
+ let current = ""
30
+ let parenDepth = 0
31
+ let bracketDepth = 0
32
+ let braceDepth = 0
33
+ let inString = false
34
+ let stringChar = ""
35
+
36
+ for (let i = 0; i < str.length; i++) {
37
+ const char = str[i]
38
+ const previousChar = i > 0 ? str[i - 1] : ""
39
+
40
+ if ((char === '"' || char === "'") && previousChar !== "\\") {
41
+ if (!inString) {
42
+ inString = true
43
+ stringChar = char
44
+ } else if (char === stringChar) {
45
+ inString = false
46
+ }
47
+ }
48
+
49
+ if (!inString) {
50
+ if (char === "(") parenDepth++
51
+ if (char === ")") parenDepth--
52
+ if (char === "[") bracketDepth++
53
+ if (char === "]") bracketDepth--
54
+ if (char === "{") braceDepth++
55
+ if (char === "}") braceDepth--
56
+
57
+ if (char === "," && parenDepth === 0 && bracketDepth === 0 && braceDepth === 0) {
58
+ result.push(current)
59
+ current = ""
60
+ continue
61
+ }
62
+ }
63
+
64
+ current += char
65
+ }
66
+
67
+ if (current) {
68
+ result.push(current)
69
+ }
70
+
71
+ return result
72
+ }
package/src/rules.ts CHANGED
@@ -11,6 +11,8 @@ import { ERBPreferImageTagHelperRule } from "./rules/erb-prefer-image-tag-helper
11
11
  import { ERBRequireTrailingNewlineRule } from "./rules/erb-require-trailing-newline.js"
12
12
  import { ERBRequireWhitespaceRule } from "./rules/erb-require-whitespace-inside-tags.js"
13
13
  import { ERBRightTrimRule } from "./rules/erb-right-trim.js"
14
+ import { ERBStrictLocalsCommentSyntaxRule } from "./rules/erb-strict-locals-comment-syntax.js"
15
+ import { ERBStrictLocalsRequiredRule } from "./rules/erb-strict-locals-required.js"
14
16
 
15
17
  import { HerbDisableCommentValidRuleNameRule } from "./rules/herb-disable-comment-valid-rule-name.js"
16
18
  import { HerbDisableCommentNoRedundantAllRule } from "./rules/herb-disable-comment-no-redundant-all.js"
@@ -67,6 +69,8 @@ export const rules: RuleClass[] = [
67
69
  ERBRequireTrailingNewlineRule,
68
70
  ERBRequireWhitespaceRule,
69
71
  ERBRightTrimRule,
72
+ ERBStrictLocalsCommentSyntaxRule,
73
+ ERBStrictLocalsRequiredRule,
70
74
 
71
75
  HerbDisableCommentValidRuleNameRule,
72
76
  HerbDisableCommentNoRedundantAllRule,
package/src/types.ts CHANGED
@@ -85,6 +85,8 @@ export abstract class ParserRule<TAutofixContext extends BaseAutofixContext = Ba
85
85
  static type = "parser" as const
86
86
  /** Indicates whether this rule supports autofix. Defaults to false. */
87
87
  static autocorrectable = false
88
+ /** Indicates whether this rule supports unsafe autofix (requires --fix-unsafely). Defaults to false. */
89
+ static unsafeAutocorrectable = false
88
90
  abstract name: string
89
91
 
90
92
  get defaultConfig(): FullRuleConfig {
@@ -119,6 +121,8 @@ export abstract class LexerRule<TAutofixContext extends BaseAutofixContext = Bas
119
121
  static type = "lexer" as const
120
122
  /** Indicates whether this rule supports autofix. Defaults to false. */
121
123
  static autocorrectable = false
124
+ /** Indicates whether this rule supports unsafe autofix (requires --fix-unsafely). Defaults to false. */
125
+ static unsafeAutocorrectable = false
122
126
  abstract name: string
123
127
 
124
128
  get defaultConfig(): FullRuleConfig {
@@ -176,6 +180,8 @@ export abstract class SourceRule<TAutofixContext extends BaseAutofixContext = Ba
176
180
  static type = "source" as const
177
181
  /** Indicates whether this rule supports autofix. Defaults to false. */
178
182
  static autocorrectable = false
183
+ /** Indicates whether this rule supports unsafe autofix (requires --fix-unsafely). Defaults to false. */
184
+ static unsafeAutocorrectable = false
179
185
  abstract name: string
180
186
 
181
187
  get defaultConfig(): FullRuleConfig {