@herb-tools/linter 0.8.7 → 0.8.9

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 +5413 -15659
  3. package/dist/herb-lint.js.map +1 -1
  4. package/dist/index.cjs +388 -37
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.js +383 -39
  7. package/dist/index.js.map +1 -1
  8. package/dist/loader.cjs +1238 -7911
  9. package/dist/loader.cjs.map +1 -1
  10. package/dist/loader.js +1232 -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 +213 -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 +283 -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
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { getNodesBeforePosition, getNodesAfterPosition, filterNodes, ERBContentNode, isERBOutputNode, Visitor, isToken, isParseResult, getStaticAttributeName, hasDynamicAttributeName as hasDynamicAttributeName$1, getCombinedAttributeName, hasStaticContent, getStaticContentFromNodes, Location, hasERBOutput, isEffectivelyStatic, getValidatableStaticContent, isERBNode, isWhitespaceNode, isCommentNode, isLiteralNode, isHTMLTextNode, Position, filterERBContentNodes, isNode, LiteralNode, didyoumean, filterLiteralNodes, Token, getTagName as getTagName$1, isHTMLElementNode, HTMLCloseTagNode, isHTMLOpenTagNode, WhitespaceNode, filterWhitespaceNodes, HTMLOpenTagNode, isERBCommentNode } from '@herb-tools/core';
1
+ import { getNodesBeforePosition, getNodesAfterPosition, filterNodes, ERBContentNode, isERBOutputNode, Visitor, isToken, isParseResult, getStaticAttributeName, hasDynamicAttributeName as hasDynamicAttributeName$1, getCombinedAttributeName, hasStaticContent, getStaticContentFromNodes, Location, hasERBOutput, isEffectivelyStatic, getValidatableStaticContent, isERBNode, isWhitespaceNode, isCommentNode, isLiteralNode, isHTMLTextNode, Position, filterERBContentNodes, isNode, LiteralNode, didyoumean, filterLiteralNodes, Token, getTagName as getTagName$1, isHTMLElementNode, isHTMLOpenTagNode, HTMLCloseTagNode, WhitespaceNode, filterWhitespaceNodes, HTMLOpenTagNode, isERBCommentNode } from '@herb-tools/core';
2
2
  import picomatch from 'picomatch';
3
3
 
4
4
  class PrintContext {
@@ -686,6 +686,8 @@ class ParserRule {
686
686
  static type = "parser";
687
687
  /** Indicates whether this rule supports autofix. Defaults to false. */
688
688
  static autocorrectable = false;
689
+ /** Indicates whether this rule supports unsafe autofix (requires --fix-unsafely). Defaults to false. */
690
+ static unsafeAutocorrectable = false;
689
691
  get defaultConfig() {
690
692
  return DEFAULT_RULE_CONFIG;
691
693
  }
@@ -697,6 +699,8 @@ class LexerRule {
697
699
  static type = "lexer";
698
700
  /** Indicates whether this rule supports autofix. Defaults to false. */
699
701
  static autocorrectable = false;
702
+ /** Indicates whether this rule supports unsafe autofix (requires --fix-unsafely). Defaults to false. */
703
+ static unsafeAutocorrectable = false;
700
704
  get defaultConfig() {
701
705
  return DEFAULT_RULE_CONFIG;
702
706
  }
@@ -714,6 +718,8 @@ class SourceRule {
714
718
  static type = "source";
715
719
  /** Indicates whether this rule supports autofix. Defaults to false. */
716
720
  static autocorrectable = false;
721
+ /** Indicates whether this rule supports unsafe autofix (requires --fix-unsafely). Defaults to false. */
722
+ static unsafeAutocorrectable = false;
717
723
  get defaultConfig() {
718
724
  return DEFAULT_RULE_CONFIG;
719
725
  }
@@ -2187,6 +2193,342 @@ class ERBRightTrimRule extends ParserRule {
2187
2193
  }
2188
2194
  }
2189
2195
 
2196
+ /**
2197
+ * File path and naming utilities for linter rules
2198
+ */
2199
+ /**
2200
+ * Extracts the basename (filename) from a file path
2201
+ * Works with both forward slashes and backslashes
2202
+ */
2203
+ function getBasename(filePath) {
2204
+ const lastSlash = Math.max(filePath.lastIndexOf("/"), filePath.lastIndexOf("\\"));
2205
+ return lastSlash === -1 ? filePath : filePath.slice(lastSlash + 1);
2206
+ }
2207
+ /**
2208
+ * Checks if a file is a Rails partial (filename starts with `_`)
2209
+ * Returns null if fileName is undefined (unknown context)
2210
+ */
2211
+ function isPartialFile(fileName) {
2212
+ if (!fileName)
2213
+ return null;
2214
+ return getBasename(fileName).startsWith("_");
2215
+ }
2216
+
2217
+ /**
2218
+ * Checks if parentheses in a string are balanced
2219
+ * Returns false if there are more closing parens than opening at any point
2220
+ */
2221
+ function hasBalancedParentheses(content) {
2222
+ let depth = 0;
2223
+ for (const char of content) {
2224
+ if (char === "(")
2225
+ depth++;
2226
+ if (char === ")")
2227
+ depth--;
2228
+ if (depth < 0)
2229
+ return false;
2230
+ }
2231
+ return depth === 0;
2232
+ }
2233
+ /**
2234
+ * Splits a string by commas at the top level only
2235
+ * Respects nested parentheses, brackets, braces, and strings
2236
+ *
2237
+ * @example
2238
+ * splitByTopLevelComma("a, b, c") // ["a", " b", " c"]
2239
+ * splitByTopLevelComma("a, (b, c), d") // ["a", " (b, c)", " d"]
2240
+ * splitByTopLevelComma('a, "b, c", d') // ["a", ' "b, c"', " d"]
2241
+ */
2242
+ function splitByTopLevelComma(str) {
2243
+ const result = [];
2244
+ let current = "";
2245
+ let parenDepth = 0;
2246
+ let bracketDepth = 0;
2247
+ let braceDepth = 0;
2248
+ let inString = false;
2249
+ let stringChar = "";
2250
+ for (let i = 0; i < str.length; i++) {
2251
+ const char = str[i];
2252
+ const previousChar = i > 0 ? str[i - 1] : "";
2253
+ if ((char === '"' || char === "'") && previousChar !== "\\") {
2254
+ if (!inString) {
2255
+ inString = true;
2256
+ stringChar = char;
2257
+ }
2258
+ else if (char === stringChar) {
2259
+ inString = false;
2260
+ }
2261
+ }
2262
+ if (!inString) {
2263
+ if (char === "(")
2264
+ parenDepth++;
2265
+ if (char === ")")
2266
+ parenDepth--;
2267
+ if (char === "[")
2268
+ bracketDepth++;
2269
+ if (char === "]")
2270
+ bracketDepth--;
2271
+ if (char === "{")
2272
+ braceDepth++;
2273
+ if (char === "}")
2274
+ braceDepth--;
2275
+ if (char === "," && parenDepth === 0 && bracketDepth === 0 && braceDepth === 0) {
2276
+ result.push(current);
2277
+ current = "";
2278
+ continue;
2279
+ }
2280
+ }
2281
+ current += char;
2282
+ }
2283
+ if (current) {
2284
+ result.push(current);
2285
+ }
2286
+ return result;
2287
+ }
2288
+
2289
+ const STRICT_LOCALS_PATTERN = /^locals:\s+\([^)]*\)\s*$/;
2290
+ function isValidStrictLocalsFormat(content) {
2291
+ return STRICT_LOCALS_PATTERN.test(content);
2292
+ }
2293
+ function extractERBCommentContent(content) {
2294
+ return content.trim();
2295
+ }
2296
+ function extractRubyCommentContent(content) {
2297
+ const match = content.match(/^\s*#\s*(.*)$/);
2298
+ return match ? match[1].trim() : null;
2299
+ }
2300
+ function extractLocalsRemainder(content) {
2301
+ const match = content.match(/^locals?\b(.*)$/);
2302
+ return match ? match[1] : null;
2303
+ }
2304
+ function looksLikeLocalsDeclaration(content) {
2305
+ return /^locals?\b/.test(content) && /[(:)]/.test(content);
2306
+ }
2307
+ function hasLocalsLikeSyntax(remainder) {
2308
+ return /[(:)]/.test(remainder);
2309
+ }
2310
+ function detectLocalsWithoutColon(content) {
2311
+ return /^locals?\(/.test(content);
2312
+ }
2313
+ function detectSingularLocal(content) {
2314
+ return /^local:/.test(content);
2315
+ }
2316
+ function detectMissingColonBeforeParens(content) {
2317
+ return /^locals\s+\(/.test(content);
2318
+ }
2319
+ function detectMissingSpaceAfterColon(content) {
2320
+ return /^locals:\(/.test(content);
2321
+ }
2322
+ function detectMissingParentheses(content) {
2323
+ return /^locals:\s*[^(]/.test(content);
2324
+ }
2325
+ function detectEmptyLocalsWithoutParens(content) {
2326
+ return /^locals:\s*$/.test(content);
2327
+ }
2328
+ function validateCommaUsage(inner) {
2329
+ if (inner.startsWith(",") || inner.endsWith(",") || /,,/.test(inner)) {
2330
+ return "Unexpected comma in `locals:` parameters.";
2331
+ }
2332
+ return null;
2333
+ }
2334
+ function validateBlockArgument(param) {
2335
+ if (param.startsWith("&")) {
2336
+ return `Block argument \`${param}\` is not allowed. Strict locals only support keyword arguments.`;
2337
+ }
2338
+ return null;
2339
+ }
2340
+ function validateSplatArgument(param) {
2341
+ if (param.startsWith("*") && !param.startsWith("**")) {
2342
+ return `Splat argument \`${param}\` is not allowed. Strict locals only support keyword arguments.`;
2343
+ }
2344
+ return null;
2345
+ }
2346
+ function validateDoubleSplatArgument(param) {
2347
+ if (param.startsWith("**")) {
2348
+ if (/^\*\*\w+$/.test(param)) {
2349
+ return null; // Valid double-splat
2350
+ }
2351
+ return `Invalid double-splat syntax \`${param}\`. Use \`**name\` format (e.g., \`**attributes\`).`;
2352
+ }
2353
+ return null;
2354
+ }
2355
+ function validateKeywordArgument(param) {
2356
+ if (!/^\w+:\s*/.test(param)) {
2357
+ if (/^\w+$/.test(param)) {
2358
+ return `Positional argument \`${param}\` is not allowed. Use keyword argument format: \`${param}:\`.`;
2359
+ }
2360
+ return `Invalid parameter \`${param}\`. Use keyword argument format: \`name:\` or \`name: default\`.`;
2361
+ }
2362
+ return null;
2363
+ }
2364
+ function validateParameter(param) {
2365
+ const trimmed = param.trim();
2366
+ if (!trimmed)
2367
+ return null;
2368
+ return (validateBlockArgument(trimmed) ||
2369
+ validateSplatArgument(trimmed) ||
2370
+ validateDoubleSplatArgument(trimmed) ||
2371
+ (trimmed.startsWith("**") ? null : validateKeywordArgument(trimmed)));
2372
+ }
2373
+ function validateLocalsSignature(paramsContent) {
2374
+ const match = paramsContent.match(/^\s*\(([\s\S]*)\)\s*$/);
2375
+ if (!match)
2376
+ return null;
2377
+ const inner = match[1].trim();
2378
+ if (!inner)
2379
+ return null; // Empty locals is valid: locals: ()
2380
+ const commaError = validateCommaUsage(inner);
2381
+ if (commaError)
2382
+ return commaError;
2383
+ const params = splitByTopLevelComma(inner);
2384
+ for (const param of params) {
2385
+ const error = validateParameter(param);
2386
+ if (error)
2387
+ return error;
2388
+ }
2389
+ return null;
2390
+ }
2391
+ class ERBStrictLocalsCommentSyntaxVisitor extends BaseRuleVisitor {
2392
+ seenStrictLocalsComment = false;
2393
+ firstStrictLocalsLocation = null;
2394
+ visitERBContentNode(node) {
2395
+ const openingTag = node.tag_opening?.value;
2396
+ const content = node.content?.value;
2397
+ if (!content)
2398
+ return;
2399
+ const commentContent = this.extractCommentContent(openingTag, content, node);
2400
+ if (!commentContent)
2401
+ return;
2402
+ const remainder = extractLocalsRemainder(commentContent);
2403
+ if (!remainder || !hasLocalsLikeSyntax(remainder))
2404
+ return;
2405
+ this.validateLocalsComment(commentContent, node);
2406
+ }
2407
+ extractCommentContent(openingTag, content, node) {
2408
+ if (openingTag === "<%#") {
2409
+ return extractERBCommentContent(content);
2410
+ }
2411
+ if (openingTag === "<%" || openingTag === "<%-") {
2412
+ const rubyComment = extractRubyCommentContent(content);
2413
+ if (rubyComment && looksLikeLocalsDeclaration(rubyComment)) {
2414
+ this.addOffense(`Use \`<%#\` instead of \`${openingTag} #\` for strict locals comments. Only ERB comment syntax is recognized by Rails.`, node.location);
2415
+ }
2416
+ }
2417
+ return null;
2418
+ }
2419
+ validateLocalsComment(commentContent, node) {
2420
+ this.checkPartialFile(node);
2421
+ if (!hasBalancedParentheses(commentContent)) {
2422
+ this.addOffense("Unbalanced parentheses in `locals:` comment. Ensure all opening parentheses have matching closing parentheses.", node.location);
2423
+ return;
2424
+ }
2425
+ if (isValidStrictLocalsFormat(commentContent)) {
2426
+ this.handleValidFormat(commentContent, node);
2427
+ return;
2428
+ }
2429
+ this.handleInvalidFormat(commentContent, node);
2430
+ }
2431
+ checkPartialFile(node) {
2432
+ const isPartial = isPartialFile(this.context.fileName);
2433
+ if (isPartial === false) {
2434
+ this.addOffense("Strict locals (`locals:`) only work in partials (files starting with `_`). This declaration will be ignored.", node.location);
2435
+ }
2436
+ }
2437
+ handleValidFormat(commentContent, node) {
2438
+ if (this.seenStrictLocalsComment) {
2439
+ this.addOffense(`Duplicate \`locals:\` declaration. Only one \`locals:\` comment is allowed per partial (first declaration at line ${this.firstStrictLocalsLocation?.line}).`, node.location);
2440
+ return;
2441
+ }
2442
+ this.seenStrictLocalsComment = true;
2443
+ this.firstStrictLocalsLocation = {
2444
+ line: node.location.start.line,
2445
+ column: node.location.start.column
2446
+ };
2447
+ const paramsMatch = commentContent.match(/^locals:\s*(\([\s\S]*\))\s*$/);
2448
+ if (paramsMatch) {
2449
+ const error = validateLocalsSignature(paramsMatch[1]);
2450
+ if (error) {
2451
+ this.addOffense(error, node.location);
2452
+ }
2453
+ }
2454
+ }
2455
+ handleInvalidFormat(commentContent, node) {
2456
+ if (detectLocalsWithoutColon(commentContent)) {
2457
+ this.addOffense("Use `locals:` with a colon, not `locals()`. Correct format: `<%# locals: (...) %>`.", node.location);
2458
+ return;
2459
+ }
2460
+ if (detectSingularLocal(commentContent)) {
2461
+ this.addOffense("Use `locals:` (plural), not `local:`.", node.location);
2462
+ return;
2463
+ }
2464
+ if (detectMissingColonBeforeParens(commentContent)) {
2465
+ this.addOffense("Use `locals:` with a colon before the parentheses, not `locals (`.", node.location);
2466
+ return;
2467
+ }
2468
+ if (detectMissingSpaceAfterColon(commentContent)) {
2469
+ this.addOffense("Missing space after `locals:`. Rails Strict Locals require a space after the colon: `<%# locals: (...) %>`.", node.location);
2470
+ return;
2471
+ }
2472
+ if (detectMissingParentheses(commentContent)) {
2473
+ this.addOffense("Wrap parameters in parentheses: `locals: (name:)` or `locals: (name: default)`.", node.location);
2474
+ return;
2475
+ }
2476
+ if (detectEmptyLocalsWithoutParens(commentContent)) {
2477
+ this.addOffense("Add parameters after `locals:`. Use `locals: (name:)` or `locals: ()` for no locals.", node.location);
2478
+ return;
2479
+ }
2480
+ this.addOffense("Invalid `locals:` syntax. Use format: `locals: (name:, option: default)`.", node.location);
2481
+ }
2482
+ }
2483
+ class ERBStrictLocalsCommentSyntaxRule extends ParserRule {
2484
+ name = "erb-strict-locals-comment-syntax";
2485
+ get defaultConfig() {
2486
+ return {
2487
+ enabled: true,
2488
+ severity: "error"
2489
+ };
2490
+ }
2491
+ check(result, context) {
2492
+ const visitor = new ERBStrictLocalsCommentSyntaxVisitor(this.name, context);
2493
+ visitor.visit(result.value);
2494
+ return visitor.offenses;
2495
+ }
2496
+ }
2497
+
2498
+ function hasStrictLocals(source) {
2499
+ return source.includes("<%# locals:") || source.includes("<%#locals:");
2500
+ }
2501
+ class ERBStrictLocalsRequiredVisitor extends BaseSourceRuleVisitor {
2502
+ visitSource(source) {
2503
+ const isPartial = isPartialFile(this.context.fileName);
2504
+ if (isPartial !== true)
2505
+ return;
2506
+ if (hasStrictLocals(source))
2507
+ return;
2508
+ const firstLineLength = source.indexOf("\n") === -1 ? source.length : source.indexOf("\n");
2509
+ const location = Location.from(1, 0, 1, firstLineLength);
2510
+ this.addOffense("Partial is missing a strict locals declaration. Add `<%# locals: (...) %>` at the top of the file.", location);
2511
+ }
2512
+ }
2513
+ class ERBStrictLocalsRequiredRule extends SourceRule {
2514
+ static unsafeAutocorrectable = true;
2515
+ name = "erb-strict-locals-required";
2516
+ get defaultConfig() {
2517
+ return {
2518
+ enabled: false,
2519
+ severity: "error",
2520
+ };
2521
+ }
2522
+ check(source, context) {
2523
+ const visitor = new ERBStrictLocalsRequiredVisitor(this.name, context);
2524
+ visitor.visit(source);
2525
+ return visitor.offenses;
2526
+ }
2527
+ autofix(_offense, source, _context) {
2528
+ return `<%# locals: () %>\n\n${source}`;
2529
+ }
2530
+ }
2531
+
2190
2532
  /**
2191
2533
  * Utilities for parsing herb:disable comments
2192
2534
  */
@@ -3100,6 +3442,8 @@ class HeadOnlyElementsVisitor extends BaseRuleVisitor {
3100
3442
  return;
3101
3443
  if (tagName === "title" && this.insideSVG)
3102
3444
  return;
3445
+ if (tagName === "style" && this.insideSVG)
3446
+ return;
3103
3447
  if (tagName === "meta" && this.hasItempropAttribute(node))
3104
3448
  return;
3105
3449
  this.addOffense(`Element \`<${tagName}>\` must be placed inside the \`<head>\` tag.`, node.location);
@@ -3968,20 +4312,22 @@ class HTMLNoEmptyAttributesRule extends ParserRule {
3968
4312
 
3969
4313
  class NoEmptyHeadingsVisitor extends BaseRuleVisitor {
3970
4314
  visitHTMLElementNode(node) {
4315
+ const tagName = getTagName(node.open_tag)?.toLowerCase();
4316
+ if (tagName === "template")
4317
+ return;
3971
4318
  this.checkHeadingElement(node);
3972
4319
  super.visitHTMLElementNode(node);
3973
4320
  }
3974
4321
  checkHeadingElement(node) {
3975
- if (!node.open_tag || node.open_tag.type !== "AST_HTML_OPEN_TAG_NODE") {
4322
+ if (!node.open_tag)
3976
4323
  return;
3977
- }
3978
- const openTag = node.open_tag;
3979
- const tagName = getTagName(openTag);
3980
- if (!tagName) {
4324
+ if (!isHTMLOpenTagNode(node.open_tag))
4325
+ return;
4326
+ const tagName = getTagName(node.open_tag);
4327
+ if (!tagName)
3981
4328
  return;
3982
- }
3983
4329
  const isStandardHeading = HEADING_TAGS.has(tagName);
3984
- const isAriaHeading = this.hasHeadingRole(openTag);
4330
+ const isAriaHeading = this.hasHeadingRole(node.open_tag);
3985
4331
  if (!isStandardHeading && !isAriaHeading) {
3986
4332
  return;
3987
4333
  }
@@ -3998,23 +4344,14 @@ class NoEmptyHeadingsVisitor extends BaseRuleVisitor {
3998
4344
  }
3999
4345
  let hasAccessibleContent = false;
4000
4346
  for (const child of node.body) {
4001
- if (child.type === "AST_LITERAL_NODE") {
4002
- const literalNode = child;
4003
- if (literalNode.content.trim().length > 0) {
4004
- hasAccessibleContent = true;
4005
- break;
4006
- }
4007
- }
4008
- else if (child.type === "AST_HTML_TEXT_NODE") {
4009
- const textNode = child;
4010
- if (textNode.content.trim().length > 0) {
4347
+ if (isLiteralNode(child) || isHTMLTextNode(child)) {
4348
+ if (child.content.trim().length > 0) {
4011
4349
  hasAccessibleContent = true;
4012
4350
  break;
4013
4351
  }
4014
4352
  }
4015
- else if (child.type === "AST_HTML_ELEMENT_NODE") {
4016
- const elementNode = child;
4017
- if (this.isElementAccessible(elementNode)) {
4353
+ else if (isHTMLElementNode(child)) {
4354
+ if (this.isElementAccessible(child)) {
4018
4355
  hasAccessibleContent = true;
4019
4356
  break;
4020
4357
  }
@@ -4036,11 +4373,11 @@ class NoEmptyHeadingsVisitor extends BaseRuleVisitor {
4036
4373
  return roleValue === "heading";
4037
4374
  }
4038
4375
  isElementAccessible(node) {
4039
- if (!node.open_tag || node.open_tag.type !== "AST_HTML_OPEN_TAG_NODE") {
4376
+ if (!node.open_tag)
4040
4377
  return true;
4041
- }
4042
- const openTag = node.open_tag;
4043
- const attributes = getAttributes(openTag);
4378
+ if (!isHTMLOpenTagNode(node.open_tag))
4379
+ return true;
4380
+ const attributes = getAttributes(node.open_tag);
4044
4381
  const ariaHiddenAttribute = findAttributeByName(attributes, "aria-hidden");
4045
4382
  if (ariaHiddenAttribute) {
4046
4383
  const ariaHiddenValue = getAttributeValue(ariaHiddenAttribute);
@@ -4052,21 +4389,13 @@ class NoEmptyHeadingsVisitor extends BaseRuleVisitor {
4052
4389
  return false;
4053
4390
  }
4054
4391
  for (const child of node.body) {
4055
- if (child.type === "AST_LITERAL_NODE") {
4056
- const literalNode = child;
4057
- if (literalNode.content.trim().length > 0) {
4392
+ if (isLiteralNode(child) || isHTMLTextNode(child)) {
4393
+ if (child.content.trim().length > 0) {
4058
4394
  return true;
4059
4395
  }
4060
4396
  }
4061
- else if (child.type === "AST_HTML_TEXT_NODE") {
4062
- const textNode = child;
4063
- if (textNode.content.trim().length > 0) {
4064
- return true;
4065
- }
4066
- }
4067
- else if (child.type === "AST_HTML_ELEMENT_NODE") {
4068
- const elementNode = child;
4069
- if (this.isElementAccessible(elementNode)) {
4397
+ else if (isHTMLElementNode(child)) {
4398
+ if (this.isElementAccessible(child)) {
4070
4399
  return true;
4071
4400
  }
4072
4401
  }
@@ -4671,6 +5000,8 @@ const rules = [
4671
5000
  ERBRequireTrailingNewlineRule,
4672
5001
  ERBRequireWhitespaceRule,
4673
5002
  ERBRightTrimRule,
5003
+ ERBStrictLocalsCommentSyntaxRule,
5004
+ ERBStrictLocalsRequiredRule,
4674
5005
  HerbDisableCommentValidRuleNameRule,
4675
5006
  HerbDisableCommentNoRedundantAllRule,
4676
5007
  HerbDisableCommentNoDuplicateRulesRule,
@@ -5084,9 +5415,12 @@ class Linter {
5084
5415
  * @param source - The source code to fix
5085
5416
  * @param context - Optional context for linting (e.g., fileName)
5086
5417
  * @param offensesToFix - Optional array of specific offenses to fix. If not provided, all fixable offenses will be fixed.
5418
+ * @param options - Options for autofix behavior
5419
+ * @param options.includeUnsafe - If true, also apply unsafe fixes (rules with unsafeAutocorrectable = true)
5087
5420
  * @returns AutofixResult containing the corrected source and lists of fixed/unfixed offenses
5088
5421
  */
5089
- autofix(source, context, offensesToFix) {
5422
+ autofix(source, context, offensesToFix, options) {
5423
+ const includeUnsafe = options?.includeUnsafe ?? false;
5090
5424
  const lintResult = offensesToFix ? { offenses: offensesToFix } : this.lint(source, context);
5091
5425
  const parserOffenses = [];
5092
5426
  const sourceOffenses = [];
@@ -5117,10 +5451,15 @@ class Linter {
5117
5451
  continue;
5118
5452
  }
5119
5453
  const rule = new RuleClass();
5454
+ const isUnsafe = RuleClass.unsafeAutocorrectable === true;
5120
5455
  if (!rule.autofix) {
5121
5456
  unfixed.push(offense);
5122
5457
  continue;
5123
5458
  }
5459
+ if (isUnsafe && !includeUnsafe) {
5460
+ unfixed.push(offense);
5461
+ continue;
5462
+ }
5124
5463
  if (offense.autofixContext) {
5125
5464
  const originalNodeType = offense.autofixContext.node.type;
5126
5465
  const location = offense.autofixContext.node.location ? Location.from(offense.autofixContext.node.location) : offense.location;
@@ -5160,10 +5499,15 @@ class Linter {
5160
5499
  continue;
5161
5500
  }
5162
5501
  const rule = new RuleClass();
5502
+ const isUnsafe = RuleClass.unsafeAutocorrectable === true;
5163
5503
  if (!rule.autofix) {
5164
5504
  unfixed.push(offense);
5165
5505
  continue;
5166
5506
  }
5507
+ if (isUnsafe && !includeUnsafe) {
5508
+ unfixed.push(offense);
5509
+ continue;
5510
+ }
5167
5511
  const correctedSource = rule.autofix(offense, currentSource, context);
5168
5512
  if (correctedSource) {
5169
5513
  currentSource = correctedSource;
@@ -5182,5 +5526,5 @@ class Linter {
5182
5526
  }
5183
5527
  }
5184
5528
 
5185
- export { ARIA_ATTRIBUTES, AttributeVisitorMixin, BaseLexerRuleVisitor, BaseRuleVisitor, BaseSourceRuleVisitor, ControlFlowTrackingVisitor, ControlFlowType, DEFAULT_LINT_CONTEXT, DEFAULT_RULE_CONFIG, DOCUMENT_ONLY_TAG_NAMES, ERBCommentSyntax, ERBNoCaseNodeChildrenRule, ERBNoEmptyTagsRule, ERBNoExtraNewLineRule, ERBNoExtraWhitespaceRule, ERBNoOutputControlFlowRule, ERBNoSilentTagInAttributeNameRule, ERBPreferImageTagHelperRule, ERBRequireTrailingNewlineRule, ERBRequireWhitespaceRule, ERBRightTrimRule, HEADING_TAGS, HEAD_AND_BODY_TAG_NAMES, HEAD_ONLY_TAG_NAMES, HTMLAnchorRequireHrefRule, HTMLAriaLabelIsWellFormattedRule, HTMLAriaLevelMustBeValidRule, HTMLAriaRoleHeadingRequiresLevelRule, HTMLAriaRoleMustBeValidRule, HTMLAttributeDoubleQuotesRule, HTMLAttributeEqualsSpacingRule, HTMLAttributeValuesRequireQuotesRule, HTMLAvoidBothDisabledAndAriaDisabledRule, HTMLBodyOnlyElementsRule, HTMLBooleanAttributesNoValueRule, HTMLHeadOnlyElementsRule, HTMLIframeHasTitleRule, HTMLImgRequireAltRule, HTMLInputRequireAutocompleteRule, HTMLNavigationHasLabelRule, HTMLNoAriaHiddenOnFocusableRule, HTMLNoBlockInsideInlineRule, HTMLNoDuplicateAttributesRule, HTMLNoDuplicateIdsRule, HTMLNoDuplicateMetaNamesRule, HTMLNoEmptyAttributesRule, HTMLNoEmptyHeadingsRule, HTMLNoNestedLinksRule, HTMLNoPositiveTabIndexRule, HTMLNoSelfClosingRule, HTMLNoSpaceInTagRule, HTMLNoTitleAttributeRule, HTMLNoUnderscoresInAttributeNamesRule, HTMLTagNameLowercaseRule, HTML_BLOCK_ELEMENTS, HTML_BOOLEAN_ATTRIBUTES, HTML_INLINE_ELEMENTS, HTML_ONLY_TAG_NAMES, HTML_VOID_ELEMENTS, HerbDisableCommentBaseVisitor, HerbDisableCommentMalformedRule, HerbDisableCommentMissingRulesRule, HerbDisableCommentNoDuplicateRulesRule, HerbDisableCommentNoRedundantAllRule, HerbDisableCommentParsedVisitor, HerbDisableCommentUnnecessaryRule, HerbDisableCommentValidRuleNameRule, LexerRule, Linter, ParserRule, SVGTagNameCapitalizationRule, SVG_CAMEL_CASE_ELEMENTS, SVG_LOWERCASE_TO_CAMELCASE, SourceRule, VALID_ARIA_ROLES, createEndOfFileLocation, findAttributeByName, findNodeByLocation, findParent, forEachAttribute, getAttribute, getAttributeName, getAttributeValue, getAttributeValueNodes, getAttributeValueQuoteType, getAttributes, getCombinedAttributeNameString, getStaticAttributeValue, getStaticAttributeValueContent, getTagName, hasAttribute, hasAttributeValue, hasDynamicAttributeName, hasDynamicAttributeValue, hasStaticAttributeValue, hasStaticAttributeValueContent, isAttributeValueQuoted, isBlockElement, isBodyOnlyTag, isBodyTag, isBooleanAttribute, isDocumentOnlyTag, isHeadAndBodyTag, isHeadOnlyTag, isHeadTag, isHtmlOnlyTag, isInlineElement, isVoidElement, locationsEqual, rules };
5529
+ export { ARIA_ATTRIBUTES, AttributeVisitorMixin, BaseLexerRuleVisitor, BaseRuleVisitor, BaseSourceRuleVisitor, ControlFlowTrackingVisitor, ControlFlowType, DEFAULT_LINT_CONTEXT, DEFAULT_RULE_CONFIG, DOCUMENT_ONLY_TAG_NAMES, ERBCommentSyntax, ERBNoCaseNodeChildrenRule, ERBNoEmptyTagsRule, ERBNoExtraNewLineRule, ERBNoExtraWhitespaceRule, ERBNoOutputControlFlowRule, ERBNoSilentTagInAttributeNameRule, ERBPreferImageTagHelperRule, ERBRequireTrailingNewlineRule, ERBRequireWhitespaceRule, ERBRightTrimRule, ERBStrictLocalsCommentSyntaxRule, ERBStrictLocalsRequiredRule, HEADING_TAGS, HEAD_AND_BODY_TAG_NAMES, HEAD_ONLY_TAG_NAMES, HTMLAnchorRequireHrefRule, HTMLAriaLabelIsWellFormattedRule, HTMLAriaLevelMustBeValidRule, HTMLAriaRoleHeadingRequiresLevelRule, HTMLAriaRoleMustBeValidRule, HTMLAttributeDoubleQuotesRule, HTMLAttributeEqualsSpacingRule, HTMLAttributeValuesRequireQuotesRule, HTMLAvoidBothDisabledAndAriaDisabledRule, HTMLBodyOnlyElementsRule, HTMLBooleanAttributesNoValueRule, HTMLHeadOnlyElementsRule, HTMLIframeHasTitleRule, HTMLImgRequireAltRule, HTMLInputRequireAutocompleteRule, HTMLNavigationHasLabelRule, HTMLNoAriaHiddenOnFocusableRule, HTMLNoBlockInsideInlineRule, HTMLNoDuplicateAttributesRule, HTMLNoDuplicateIdsRule, HTMLNoDuplicateMetaNamesRule, HTMLNoEmptyAttributesRule, HTMLNoEmptyHeadingsRule, HTMLNoNestedLinksRule, HTMLNoPositiveTabIndexRule, HTMLNoSelfClosingRule, HTMLNoSpaceInTagRule, HTMLNoTitleAttributeRule, HTMLNoUnderscoresInAttributeNamesRule, HTMLTagNameLowercaseRule, HTML_BLOCK_ELEMENTS, HTML_BOOLEAN_ATTRIBUTES, HTML_INLINE_ELEMENTS, HTML_ONLY_TAG_NAMES, HTML_VOID_ELEMENTS, HerbDisableCommentBaseVisitor, HerbDisableCommentMalformedRule, HerbDisableCommentMissingRulesRule, HerbDisableCommentNoDuplicateRulesRule, HerbDisableCommentNoRedundantAllRule, HerbDisableCommentParsedVisitor, HerbDisableCommentUnnecessaryRule, HerbDisableCommentValidRuleNameRule, LexerRule, Linter, ParserRule, STRICT_LOCALS_PATTERN, SVGTagNameCapitalizationRule, SVG_CAMEL_CASE_ELEMENTS, SVG_LOWERCASE_TO_CAMELCASE, SourceRule, VALID_ARIA_ROLES, createEndOfFileLocation, findAttributeByName, findNodeByLocation, findParent, forEachAttribute, getAttribute, getAttributeName, getAttributeValue, getAttributeValueNodes, getAttributeValueQuoteType, getAttributes, getBasename, getCombinedAttributeNameString, getStaticAttributeValue, getStaticAttributeValueContent, getTagName, hasAttribute, hasAttributeValue, hasBalancedParentheses, hasDynamicAttributeName, hasDynamicAttributeValue, hasStaticAttributeValue, hasStaticAttributeValueContent, isAttributeValueQuoted, isBlockElement, isBodyOnlyTag, isBodyTag, isBooleanAttribute, isDocumentOnlyTag, isHeadAndBodyTag, isHeadOnlyTag, isHeadTag, isHtmlOnlyTag, isInlineElement, isPartialFile, isVoidElement, locationsEqual, rules, splitByTopLevelComma };
5186
5530
  //# sourceMappingURL=index.js.map