@herb-tools/linter 0.8.7 → 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 (79) hide show
  1. package/README.md +28 -2
  2. package/dist/herb-lint.js +5406 -15659
  3. package/dist/herb-lint.js.map +1 -1
  4. package/dist/index.cjs +381 -37
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.js +376 -39
  7. package/dist/index.js.map +1 -1
  8. package/dist/loader.cjs +1231 -7911
  9. package/dist/loader.cjs.map +1 -1
  10. package/dist/loader.js +1225 -7912
  11. package/dist/loader.js.map +1 -1
  12. package/dist/package.json +7 -7
  13. package/dist/src/cli/argument-parser.js +5 -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 +14 -8
  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 +14 -1
  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-empty-headings.js +22 -36
  32. package/dist/src/rules/html-no-empty-headings.js.map +1 -1
  33. package/dist/src/rules/index.js +4 -0
  34. package/dist/src/rules/index.js.map +1 -1
  35. package/dist/src/rules/string-utils.js +72 -0
  36. package/dist/src/rules/string-utils.js.map +1 -0
  37. package/dist/src/rules.js +4 -0
  38. package/dist/src/rules.js.map +1 -1
  39. package/dist/src/types.js +6 -0
  40. package/dist/src/types.js.map +1 -1
  41. package/dist/tsconfig.tsbuildinfo +1 -1
  42. package/dist/types/cli/argument-parser.d.ts +1 -0
  43. package/dist/types/cli/file-processor.d.ts +1 -0
  44. package/dist/types/cli.d.ts +1 -1
  45. package/dist/types/linter.d.ts +5 -1
  46. package/dist/types/rules/erb-strict-locals-comment-syntax.d.ts +9 -0
  47. package/dist/types/rules/erb-strict-locals-required.d.ts +9 -0
  48. package/dist/types/rules/file-utils.d.ts +13 -0
  49. package/dist/types/rules/index.d.ts +4 -0
  50. package/dist/types/rules/string-utils.d.ts +15 -0
  51. package/dist/types/src/cli/argument-parser.d.ts +1 -0
  52. package/dist/types/src/cli/file-processor.d.ts +1 -0
  53. package/dist/types/src/cli.d.ts +1 -1
  54. package/dist/types/src/linter.d.ts +5 -1
  55. package/dist/types/src/rules/erb-strict-locals-comment-syntax.d.ts +9 -0
  56. package/dist/types/src/rules/erb-strict-locals-required.d.ts +9 -0
  57. package/dist/types/src/rules/file-utils.d.ts +13 -0
  58. package/dist/types/src/rules/index.d.ts +4 -0
  59. package/dist/types/src/rules/string-utils.d.ts +15 -0
  60. package/dist/types/src/types.d.ts +6 -0
  61. package/dist/types/types.d.ts +6 -0
  62. package/docs/rules/README.md +1 -0
  63. package/docs/rules/erb-strict-locals-comment-syntax.md +153 -0
  64. package/docs/rules/erb-strict-locals-required.md +107 -0
  65. package/package.json +7 -7
  66. package/src/cli/argument-parser.ts +6 -2
  67. package/src/cli/file-processor.ts +2 -1
  68. package/src/cli.ts +18 -8
  69. package/src/custom-rule-loader.ts +2 -2
  70. package/src/linter.ts +17 -1
  71. package/src/rules/erb-strict-locals-comment-syntax.ts +274 -0
  72. package/src/rules/erb-strict-locals-required.ts +52 -0
  73. package/src/rules/file-utils.ts +23 -0
  74. package/src/rules/html-head-only-elements.ts +1 -0
  75. package/src/rules/html-no-empty-headings.ts +21 -44
  76. package/src/rules/index.ts +4 -0
  77. package/src/rules/string-utils.ts +72 -0
  78. package/src/rules.ts +4 -0
  79. package/src/types.ts +6 -0
@@ -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
  export * from "./erb-comment-syntax.js";
4
6
  export * from "./erb-no-case-node-children.js";
@@ -11,6 +13,8 @@ export * from "./erb-prefer-image-tag-helper.js";
11
13
  export * from "./erb-require-trailing-newline.js";
12
14
  export * from "./erb-require-whitespace-inside-tags.js";
13
15
  export * from "./erb-right-trim.js";
16
+ export * from "./erb-strict-locals-comment-syntax.js";
17
+ export * from "./erb-strict-locals-required.js";
14
18
  export * from "./herb-disable-comment-valid-rule-name.js";
15
19
  export * from "./herb-disable-comment-no-redundant-all.js";
16
20
  export * from "./herb-disable-comment-no-duplicate-rules.js";
@@ -0,0 +1,15 @@
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 declare function hasBalancedParentheses(content: string): boolean;
6
+ /**
7
+ * Splits a string by commas at the top level only
8
+ * Respects nested parentheses, brackets, braces, and strings
9
+ *
10
+ * @example
11
+ * splitByTopLevelComma("a, b, c") // ["a", " b", " c"]
12
+ * splitByTopLevelComma("a, (b, c), d") // ["a", " (b, c)", " d"]
13
+ * splitByTopLevelComma('a, "b, c", d') // ["a", ' "b, c"', " d"]
14
+ */
15
+ export declare function splitByTopLevelComma(str: string): string[];
@@ -68,6 +68,8 @@ export declare abstract class ParserRule<TAutofixContext extends BaseAutofixCont
68
68
  static type: "parser";
69
69
  /** Indicates whether this rule supports autofix. Defaults to false. */
70
70
  static autocorrectable: boolean;
71
+ /** Indicates whether this rule supports unsafe autofix (requires --fix-unsafely). Defaults to false. */
72
+ static unsafeAutocorrectable: boolean;
71
73
  abstract name: string;
72
74
  get defaultConfig(): FullRuleConfig;
73
75
  abstract check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense<TAutofixContext>[];
@@ -96,6 +98,8 @@ export declare abstract class LexerRule<TAutofixContext extends BaseAutofixConte
96
98
  static type: "lexer";
97
99
  /** Indicates whether this rule supports autofix. Defaults to false. */
98
100
  static autocorrectable: boolean;
101
+ /** Indicates whether this rule supports unsafe autofix (requires --fix-unsafely). Defaults to false. */
102
+ static unsafeAutocorrectable: boolean;
99
103
  abstract name: string;
100
104
  get defaultConfig(): FullRuleConfig;
101
105
  abstract check(lexResult: LexResult, context?: Partial<LintContext>): UnboundLintOffense<TAutofixContext>[];
@@ -139,6 +143,8 @@ export declare abstract class SourceRule<TAutofixContext extends BaseAutofixCont
139
143
  static type: "source";
140
144
  /** Indicates whether this rule supports autofix. Defaults to false. */
141
145
  static autocorrectable: boolean;
146
+ /** Indicates whether this rule supports unsafe autofix (requires --fix-unsafely). Defaults to false. */
147
+ static unsafeAutocorrectable: boolean;
142
148
  abstract name: string;
143
149
  get defaultConfig(): FullRuleConfig;
144
150
  abstract check(source: string, context?: Partial<LintContext>): UnboundLintOffense<TAutofixContext>[];
@@ -68,6 +68,8 @@ export declare abstract class ParserRule<TAutofixContext extends BaseAutofixCont
68
68
  static type: "parser";
69
69
  /** Indicates whether this rule supports autofix. Defaults to false. */
70
70
  static autocorrectable: boolean;
71
+ /** Indicates whether this rule supports unsafe autofix (requires --fix-unsafely). Defaults to false. */
72
+ static unsafeAutocorrectable: boolean;
71
73
  abstract name: string;
72
74
  get defaultConfig(): FullRuleConfig;
73
75
  abstract check(result: ParseResult, context?: Partial<LintContext>): UnboundLintOffense<TAutofixContext>[];
@@ -96,6 +98,8 @@ export declare abstract class LexerRule<TAutofixContext extends BaseAutofixConte
96
98
  static type: "lexer";
97
99
  /** Indicates whether this rule supports autofix. Defaults to false. */
98
100
  static autocorrectable: boolean;
101
+ /** Indicates whether this rule supports unsafe autofix (requires --fix-unsafely). Defaults to false. */
102
+ static unsafeAutocorrectable: boolean;
99
103
  abstract name: string;
100
104
  get defaultConfig(): FullRuleConfig;
101
105
  abstract check(lexResult: LexResult, context?: Partial<LintContext>): UnboundLintOffense<TAutofixContext>[];
@@ -139,6 +143,8 @@ export declare abstract class SourceRule<TAutofixContext extends BaseAutofixCont
139
143
  static type: "source";
140
144
  /** Indicates whether this rule supports autofix. Defaults to false. */
141
145
  static autocorrectable: boolean;
146
+ /** Indicates whether this rule supports unsafe autofix (requires --fix-unsafely). Defaults to false. */
147
+ static unsafeAutocorrectable: boolean;
142
148
  abstract name: string;
143
149
  get defaultConfig(): FullRuleConfig;
144
150
  abstract check(source: string, context?: Partial<LintContext>): UnboundLintOffense<TAutofixContext>[];
@@ -15,6 +15,7 @@ This page contains documentation for all Herb Linter rules.
15
15
  - [`erb-require-whitespace-inside-tags`](./erb-require-whitespace-inside-tags.md) - Requires whitespace around ERB tags
16
16
  - [`erb-require-trailing-newline`](./erb-require-trailing-newline.md) - Enforces that all HTML+ERB template files end with exactly one trailing newline character.
17
17
  - [`erb-right-trim`](./erb-right-trim.md) - Enforce consistent right-trimming syntax.
18
+ - [`erb-strict-locals-comment-syntax`](./erb-strict-locals-comment-syntax.md) - Enforce strict locals comment syntax.
18
19
  - [`herb-disable-comment-malformed`](./herb-disable-comment-malformed.md) - Detect malformed `herb:disable` comments.
19
20
  - [`herb-disable-comment-missing-rules`](./herb-disable-comment-missing-rules.md) - Require rule names in `herb:disable` comments.
20
21
  - [`herb-disable-comment-no-duplicate-rules`](./herb-disable-comment-no-duplicate-rules.md) - Disallow duplicate rule names in `herb:disable` comments.
@@ -0,0 +1,153 @@
1
+ # Linter Rule: Enforce strict locals comment syntax
2
+
3
+ **Rule:** `erb-strict-locals-comment-syntax`
4
+
5
+ ## Description
6
+
7
+ Ensures that strict locals comments use the exact `locals: ( ... )` syntax so they are properly recognized by Rails and tooling. Also validates that only keyword arguments are used (no positional, block, or splat arguments).
8
+
9
+ ## Rationale
10
+
11
+ Strict locals comments declare which locals are expected in a template. Misspellings or malformed syntax silently disable the declaration, leading to confusing runtime errors when required locals are missing.
12
+
13
+ Additionally, Rails only supports keyword arguments in strict locals declarations. Positional, block, and splat arguments will raise an `ActionView::Error` at render-time.
14
+
15
+ This rule catches invalid comment forms and argument types early during development.
16
+
17
+ ## Examples
18
+
19
+ ### ✅ Good
20
+
21
+ Required keyword argument:
22
+
23
+ ```erb
24
+ <%# locals: (user:) %>
25
+ ```
26
+
27
+ Keyword argument with default value:
28
+
29
+ ```erb
30
+ <%# locals: (user:, admin: false) %>
31
+ ```
32
+
33
+ Complex default values:
34
+
35
+ ```erb
36
+ <%# locals: (items: [], config: {}) %>
37
+ ```
38
+
39
+ No locals (empty):
40
+
41
+ ```erb
42
+ <%# locals: () %>
43
+ ```
44
+
45
+ Double-splat for optional keyword arguments:
46
+
47
+ ```erb
48
+ <%# locals: (message: "Hello", **attributes) %>
49
+ ```
50
+
51
+ ### 🚫 Bad
52
+
53
+ #### Wrong comment syntax
54
+
55
+ Missing colon after `locals`:
56
+
57
+ ```erb
58
+ <%# locals() %>
59
+ ```
60
+
61
+ Singular `local` instead of `locals`:
62
+
63
+ ```erb
64
+ <%# local: (user:) %>
65
+ ```
66
+
67
+ Missing colon before parentheses:
68
+
69
+ ```erb
70
+ <%# locals (user:) %>
71
+ ```
72
+
73
+ Missing parentheses around parameters:
74
+
75
+ ```erb
76
+ <%# locals: user %>
77
+ ```
78
+
79
+ Empty `locals:` without parentheses:
80
+
81
+ ```erb
82
+ <%# locals: %>
83
+ ```
84
+
85
+ Unbalanced parentheses:
86
+
87
+ ```erb
88
+ <%# locals: (user: %>
89
+ ```
90
+
91
+ #### Wrong tag type (must use ERB comment tag)
92
+
93
+ Ruby comment in execution tag:
94
+
95
+ ```erb
96
+ <% # locals: (user:) %>
97
+ ```
98
+
99
+ #### Unsupported argument types
100
+
101
+ Positional argument (use `user:` instead):
102
+
103
+ ```erb
104
+ <%# locals: (user) %>
105
+ ```
106
+
107
+ Block argument:
108
+
109
+ ```erb
110
+ <%# locals: (&block) %>
111
+ ```
112
+
113
+ Single splat argument:
114
+
115
+ ```erb
116
+ <%# locals: (*args) %>
117
+ ```
118
+
119
+ Note: Double-splat (`**attributes`) IS supported for optional keyword arguments.
120
+
121
+ #### Invalid Ruby syntax
122
+
123
+ Trailing comma:
124
+
125
+ ```erb
126
+ <%# locals: (user:,) %>
127
+ ```
128
+
129
+ Leading comma:
130
+
131
+ ```erb
132
+ <%# locals: (, user:) %>
133
+ ```
134
+
135
+ Double comma:
136
+
137
+ ```erb
138
+ <%# locals: (user:,, admin:) %>
139
+ ```
140
+
141
+ #### Duplicate declarations
142
+
143
+ Only one `locals:` comment is allowed per partial:
144
+
145
+ ```erb
146
+ <%# locals: (user:) %>
147
+ <p>Content</p>
148
+ <%# locals: (admin:) %>
149
+ ```
150
+
151
+ ## References
152
+
153
+ - [Action View - Strict Locals](https://guides.rubyonrails.org/action_view_overview.html#strict-locals)
@@ -0,0 +1,107 @@
1
+ # Linter Rule: Require strict locals in Rails partials
2
+
3
+ **Rule:** `erb-strict-locals-required`
4
+
5
+ **Default:** Disabled (opt-in)
6
+
7
+ ## Description
8
+
9
+ Requires that every Rails partial template includes a strict locals declaration comment using the supported syntax:
10
+
11
+ ```erb
12
+ <%# locals: () %>
13
+ ```
14
+
15
+ A partial is any template whose filename begins with an underscore (e.g. `_card.html.erb`).
16
+
17
+ ## Rationale
18
+
19
+ Partials often rely on implicit locals, which makes them harder to understand, refactor, and lint. Requiring strict locals:
20
+
21
+ - Documents the partial's public API at the top of the file
22
+ - Improves readability and onboarding
23
+ - Enables better static analysis (unknown locals, missing locals, unused locals)
24
+ - Reduces runtime surprises when locals are renamed or removed
25
+
26
+ This rule encourages partials to be explicit about what they expect. Partials that intentionally accept no locals should still declare an explicit empty signature.
27
+
28
+ ## Configuration
29
+
30
+ This rule is disabled by default. To enable it, add to your [`.herb.yml`](/configuration):
31
+
32
+ ```yaml [.herb.yml]
33
+ linter:
34
+ rules:
35
+ erb-strict-locals-required:
36
+ enabled: true
37
+ ```
38
+
39
+ ## Autofix
40
+
41
+ This rule supports **unsafe autofix** via `--fix-unsafely`. When applied, it inserts an empty strict locals declaration at the top of the file:
42
+
43
+ ```erb
44
+ <%# locals: () %>
45
+ ```
46
+
47
+ This is considered "unsafe" because:
48
+ - It changes the partial's behavior (strict mode will now error on undeclared locals)
49
+ - You may need to manually add the actual local variables your partial uses
50
+
51
+ To apply the autofix:
52
+
53
+ ```bash
54
+ herb-lint --fix-unsafely _partial.html.erb
55
+ ```
56
+
57
+ After the autofix runs, review the file and update the locals declaration to include any variables your partial expects.
58
+
59
+ ## Examples
60
+
61
+ ### ✅ Good
62
+
63
+ Partial with required keyword argument:
64
+
65
+ ```erb
66
+ <%# locals: (user:) %>
67
+
68
+ <div class="user-card">
69
+ <%= user.name %>
70
+ </div>
71
+ ```
72
+
73
+ Partial with keyword argument and default:
74
+
75
+ ```erb [app/views/users/_card.html.erb]
76
+ <%# locals: (user:, admin: false) %>
77
+
78
+ <div class="user-card">
79
+ <%= user.name %>
80
+
81
+ <% if admin %>
82
+ <span class="badge">Admin</span>
83
+ <% end %>
84
+ </div>
85
+ ```
86
+
87
+ Partial with no locals (empty declaration):
88
+
89
+ ```erb [app/views/pages/_content.html.erb]
90
+ <%# locals: () %>
91
+
92
+ <p>Static content only</p>
93
+ ```
94
+
95
+ ### 🚫 Bad
96
+
97
+ Partial without strict locals declaration:
98
+
99
+ ```erb [app/views/users/_card.html.erb]
100
+ <div class="user-card">
101
+ <%= user.name %>
102
+ </div>
103
+ ```
104
+
105
+ ## References
106
+
107
+ - [Action View - Strict Locals](https://guides.rubyonrails.org/action_view_overview.html#strict-locals)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@herb-tools/linter",
3
- "version": "0.8.7",
3
+ "version": "0.8.8",
4
4
  "description": "HTML+ERB linter for validating HTML structure and enforcing best practices",
5
5
  "license": "MIT",
6
6
  "homepage": "https://herb-tools.dev",
@@ -45,12 +45,12 @@
45
45
  }
46
46
  },
47
47
  "dependencies": {
48
- "@herb-tools/config": "0.8.7",
49
- "@herb-tools/core": "0.8.7",
50
- "@herb-tools/highlighter": "0.8.7",
51
- "@herb-tools/node-wasm": "0.8.7",
52
- "@herb-tools/printer": "0.8.7",
53
- "@herb-tools/rewriter": "0.8.7",
48
+ "@herb-tools/config": "0.8.8",
49
+ "@herb-tools/core": "0.8.8",
50
+ "@herb-tools/highlighter": "0.8.8",
51
+ "@herb-tools/node-wasm": "0.8.8",
52
+ "@herb-tools/printer": "0.8.8",
53
+ "@herb-tools/rewriter": "0.8.8",
54
54
  "picomatch": "^4.0.2",
55
55
  "tinyglobby": "^0.2.15"
56
56
  },
@@ -21,6 +21,7 @@ export interface ParsedArguments {
21
21
  truncateLines: boolean
22
22
  useGitHubActions: boolean
23
23
  fix: boolean
24
+ fixUnsafe: boolean
24
25
  ignoreDisableComments: boolean
25
26
  force: boolean
26
27
  init: boolean
@@ -43,6 +44,7 @@ export class ArgumentParser {
43
44
  -c, --config-file <path> explicitly specify path to .herb.yml config file
44
45
  --force force linting even if disabled in .herb.yml
45
46
  --fix automatically fix auto-correctable offenses
47
+ --fix-unsafely also apply unsafe auto-fixes (implies --fix)
46
48
  --ignore-disable-comments report offenses even when suppressed with <%# herb:disable %> comments
47
49
  --fail-level <severity> exit with error code when diagnostics of this severity or higher are present (error|warning|info|hint) [default: error]
48
50
  --format output format (simple|detailed|json) [default: detailed]
@@ -68,6 +70,7 @@ export class ArgumentParser {
68
70
  "config-file": { type: "string", short: "c" },
69
71
  force: { type: "boolean" },
70
72
  fix: { type: "boolean" },
73
+ "fix-unsafely": { type: "boolean" },
71
74
  "ignore-disable-comments": { type: "boolean" },
72
75
  "fail-level": { type: "string" },
73
76
  format: { type: "string" },
@@ -141,7 +144,8 @@ export class ArgumentParser {
141
144
 
142
145
  const theme = values.theme || DEFAULT_THEME
143
146
  const patterns = this.getFilePatterns(positionals)
144
- const fix = values.fix || false
147
+ const fixUnsafe = values["fix-unsafely"] || false
148
+ const fix = values.fix || fixUnsafe // --fix-unsafely implies --fix
145
149
  const force = !!values.force
146
150
  const ignoreDisableComments = values["ignore-disable-comments"] || false
147
151
  const configFile = values["config-file"]
@@ -159,7 +163,7 @@ export class ArgumentParser {
159
163
  }
160
164
  }
161
165
 
162
- return { patterns, configFile, formatOption, showTiming, theme, wrapLines, truncateLines, useGitHubActions, fix, ignoreDisableComments, force, init, loadCustomRules, failLevel }
166
+ return { patterns, configFile, formatOption, showTiming, theme, wrapLines, truncateLines, useGitHubActions, fix, fixUnsafe, ignoreDisableComments, force, init, loadCustomRules, failLevel }
163
167
  }
164
168
 
165
169
  private getFilePatterns(positionals: string[]): string[] {
@@ -22,6 +22,7 @@ export interface ProcessingContext {
22
22
  projectPath?: string
23
23
  pattern?: string
24
24
  fix?: boolean
25
+ fixUnsafe?: boolean
25
26
  ignoreDisableComments?: boolean
26
27
  linterConfig?: HerbConfigOptions['linter']
27
28
  config?: Config
@@ -138,7 +139,7 @@ export class FileProcessor {
138
139
  const autofixResult = this.linter.autofix(content, {
139
140
  fileName: filename,
140
141
  ignoreDisableComments: context?.ignoreDisableComments
141
- })
142
+ }, undefined, { includeUnsafe: context?.fixUnsafe })
142
143
 
143
144
  if (autofixResult.fixed.length > 0) {
144
145
  writeFileSync(filePath, autofixResult.source, "utf-8")
package/src/cli.ts CHANGED
@@ -66,9 +66,9 @@ export class CLI {
66
66
  }
67
67
  }
68
68
 
69
- protected adjustPattern(pattern: string | undefined, configGlobPattern: string): string {
69
+ protected adjustPattern(pattern: string | undefined, configGlobPatterns: string[]): string {
70
70
  if (!pattern) {
71
- return configGlobPattern
71
+ return configGlobPatterns.length === 1 ? configGlobPatterns[0] : `{${configGlobPatterns.join(',')}}`
72
72
  }
73
73
 
74
74
  const resolvedPattern = resolve(pattern)
@@ -77,7 +77,15 @@ export class CLI {
77
77
  const stats = statSync(resolvedPattern)
78
78
 
79
79
  if (stats.isDirectory()) {
80
- return configGlobPattern
80
+ const relativeDir = relative(this.projectPath, resolvedPattern)
81
+
82
+ if (relativeDir) {
83
+ const scopedPatterns = configGlobPatterns.map(pattern => `${relativeDir}/${pattern}`)
84
+
85
+ return scopedPatterns.length === 1 ? scopedPatterns[0] : `{${scopedPatterns.join(',')}}`
86
+ }
87
+
88
+ return configGlobPatterns.length === 1 ? configGlobPatterns[0] : `{${configGlobPatterns.join(',')}}`
81
89
  } else if (stats.isFile()) {
82
90
  return relative(this.projectPath, resolvedPattern)
83
91
  }
@@ -96,10 +104,11 @@ export class CLI {
96
104
  }
97
105
 
98
106
  const filesConfig = config.getFilesConfigForTool('linter')
99
- const configGlobPattern = filesConfig.include && filesConfig.include.length > 0
100
- ? (filesConfig.include.length === 1 ? filesConfig.include[0] : `{${filesConfig.include.join(',')}}`)
101
- : '**/*.html.erb'
102
- const adjustedPattern = this.adjustPattern(pattern, configGlobPattern)
107
+ const configGlobPatterns = filesConfig.include && filesConfig.include.length > 0
108
+ ? filesConfig.include
109
+ : ['**/*.html.erb']
110
+
111
+ const adjustedPattern = this.adjustPattern(pattern, configGlobPatterns)
103
112
 
104
113
  let files = await glob(adjustedPattern, {
105
114
  cwd: this.projectPath,
@@ -135,7 +144,7 @@ export class CLI {
135
144
  const startTime = Date.now()
136
145
  const startDate = new Date()
137
146
 
138
- let { patterns, configFile, formatOption, showTiming, theme, wrapLines, truncateLines, useGitHubActions, fix, ignoreDisableComments, force, init, loadCustomRules, failLevel } = this.argumentParser.parse(process.argv)
147
+ let { patterns, configFile, formatOption, showTiming, theme, wrapLines, truncateLines, useGitHubActions, fix, fixUnsafe, ignoreDisableComments, force, init, loadCustomRules, failLevel } = this.argumentParser.parse(process.argv)
139
148
 
140
149
  this.determineProjectPath(patterns)
141
150
 
@@ -239,6 +248,7 @@ export class CLI {
239
248
  projectPath: this.projectPath,
240
249
  pattern: patterns.join(' '),
241
250
  fix,
251
+ fixUnsafe,
242
252
  ignoreDisableComments,
243
253
  linterConfig,
244
254
  config: processingConfig,
@@ -1,5 +1,5 @@
1
1
  import { pathToFileURL } from "url"
2
- import { glob } from "glob"
2
+ import { glob } from "tinyglobby"
3
3
 
4
4
  import type { RuleClass } from "./types.js"
5
5
 
@@ -52,7 +52,7 @@ export class CustomRuleLoader {
52
52
  const files = await glob(pattern, {
53
53
  cwd: this.baseDir,
54
54
  absolute: true,
55
- nodir: true
55
+ onlyFiles: true
56
56
  })
57
57
 
58
58
  allFiles.push(...files)
package/src/linter.ts CHANGED
@@ -459,9 +459,12 @@ export class Linter {
459
459
  * @param source - The source code to fix
460
460
  * @param context - Optional context for linting (e.g., fileName)
461
461
  * @param offensesToFix - Optional array of specific offenses to fix. If not provided, all fixable offenses will be fixed.
462
+ * @param options - Options for autofix behavior
463
+ * @param options.includeUnsafe - If true, also apply unsafe fixes (rules with unsafeAutocorrectable = true)
462
464
  * @returns AutofixResult containing the corrected source and lists of fixed/unfixed offenses
463
465
  */
464
- autofix(source: string, context?: Partial<LintContext>, offensesToFix?: LintOffense[]): AutofixResult {
466
+ autofix(source: string, context?: Partial<LintContext>, offensesToFix?: LintOffense[], options?: { includeUnsafe?: boolean }): AutofixResult {
467
+ const includeUnsafe = options?.includeUnsafe ?? false
465
468
  const lintResult = offensesToFix ? { offenses: offensesToFix } : this.lint(source, context)
466
469
 
467
470
  const parserOffenses: LintOffense[] = []
@@ -503,6 +506,7 @@ export class Linter {
503
506
  }
504
507
 
505
508
  const rule = new RuleClass() as ParserRule
509
+ const isUnsafe = (RuleClass as any).unsafeAutocorrectable === true
506
510
 
507
511
  if (!rule.autofix) {
508
512
  unfixed.push(offense)
@@ -510,6 +514,12 @@ export class Linter {
510
514
  continue
511
515
  }
512
516
 
517
+ if (isUnsafe && !includeUnsafe) {
518
+ unfixed.push(offense)
519
+
520
+ continue
521
+ }
522
+
513
523
  if (offense.autofixContext) {
514
524
  const originalNodeType = offense.autofixContext.node.type
515
525
  const location: Location = offense.autofixContext.node.location ? Location.from(offense.autofixContext.node.location) : offense.location
@@ -562,12 +572,18 @@ export class Linter {
562
572
  }
563
573
 
564
574
  const rule = new RuleClass() as SourceRule
575
+ const isUnsafe = (RuleClass as any).unsafeAutocorrectable === true
565
576
 
566
577
  if (!rule.autofix) {
567
578
  unfixed.push(offense)
568
579
  continue
569
580
  }
570
581
 
582
+ if (isUnsafe && !includeUnsafe) {
583
+ unfixed.push(offense)
584
+ continue
585
+ }
586
+
571
587
  const correctedSource = rule.autofix(offense, currentSource, context)
572
588
 
573
589
  if (correctedSource) {