@herb-tools/linter 0.9.0 → 0.9.2

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 (62) hide show
  1. package/README.md +2 -2
  2. package/dist/herb-lint.js +1525 -98
  3. package/dist/herb-lint.js.map +1 -1
  4. package/dist/index.cjs +546 -87
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.js +465 -87
  7. package/dist/index.js.map +1 -1
  8. package/dist/lint-worker.js +1523 -96
  9. package/dist/lint-worker.js.map +1 -1
  10. package/dist/loader.cjs +1078 -94
  11. package/dist/loader.cjs.map +1 -1
  12. package/dist/loader.js +1057 -95
  13. package/dist/loader.js.map +1 -1
  14. package/dist/rules/actionview-no-silent-render.js +31 -0
  15. package/dist/rules/actionview-no-silent-render.js.map +1 -0
  16. package/dist/rules/erb-no-case-node-children.js +3 -1
  17. package/dist/rules/erb-no-case-node-children.js.map +1 -1
  18. package/dist/rules/erb-no-duplicate-branch-elements.js +95 -11
  19. package/dist/rules/erb-no-duplicate-branch-elements.js.map +1 -1
  20. package/dist/rules/erb-no-empty-control-flow.js +190 -0
  21. package/dist/rules/erb-no-empty-control-flow.js.map +1 -0
  22. package/dist/rules/erb-no-silent-statement.js +44 -0
  23. package/dist/rules/erb-no-silent-statement.js.map +1 -0
  24. package/dist/rules/erb-no-unsafe-script-interpolation.js +37 -3
  25. package/dist/rules/erb-no-unsafe-script-interpolation.js.map +1 -1
  26. package/dist/rules/html-allowed-script-type.js +1 -1
  27. package/dist/rules/html-allowed-script-type.js.map +1 -1
  28. package/dist/rules/index.js +20 -16
  29. package/dist/rules/index.js.map +1 -1
  30. package/dist/rules/rule-utils.js +14 -23
  31. package/dist/rules/rule-utils.js.map +1 -1
  32. package/dist/rules.js +8 -2
  33. package/dist/rules.js.map +1 -1
  34. package/dist/types/index.d.ts +1 -0
  35. package/dist/types/rules/actionview-no-silent-render.d.ts +9 -0
  36. package/dist/types/rules/erb-no-duplicate-branch-elements.d.ts +1 -0
  37. package/dist/types/rules/erb-no-empty-control-flow.d.ts +8 -0
  38. package/dist/types/rules/erb-no-silent-statement.d.ts +9 -0
  39. package/dist/types/rules/erb-no-unsafe-script-interpolation.d.ts +2 -1
  40. package/dist/types/rules/index.d.ts +20 -16
  41. package/dist/types/rules/rule-utils.d.ts +8 -11
  42. package/dist/types/types.d.ts +4 -3
  43. package/dist/types.js +6 -3
  44. package/dist/types.js.map +1 -1
  45. package/docs/rules/README.md +3 -0
  46. package/docs/rules/actionview-no-silent-render.md +47 -0
  47. package/docs/rules/erb-no-empty-control-flow.md +83 -0
  48. package/docs/rules/erb-no-silent-statement.md +53 -0
  49. package/docs/rules/erb-no-unsafe-script-interpolation.md +70 -3
  50. package/package.json +8 -8
  51. package/src/index.ts +21 -0
  52. package/src/rules/actionview-no-silent-render.ts +44 -0
  53. package/src/rules/erb-no-case-node-children.ts +3 -1
  54. package/src/rules/erb-no-duplicate-branch-elements.ts +130 -14
  55. package/src/rules/erb-no-empty-control-flow.ts +255 -0
  56. package/src/rules/erb-no-silent-statement.ts +58 -0
  57. package/src/rules/erb-no-unsafe-script-interpolation.ts +51 -5
  58. package/src/rules/html-allowed-script-type.ts +1 -1
  59. package/src/rules/index.ts +21 -16
  60. package/src/rules/rule-utils.ts +15 -24
  61. package/src/rules.ts +8 -2
  62. package/src/types.ts +7 -3
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import picomatch from 'picomatch';
2
- import { getNodesBeforePosition, getNodesAfterPosition, filterNodes, ERBContentNode, isERBOutputNode, isERBIfNode, isERBUnlessNode, isERBElseNode, isHTMLTextNode, Visitor, isToken, isParseResult, forEachAttribute, getAttributeName, hasDynamicAttributeNameOnAttribute, getStaticAttributeValue, getAttributeValueNodes, hasERBOutput, isEffectivelyStatic, getValidatableStaticContent, getAttributeValue, getCombinedAttributeNameString, Location, Position, isERBOpenTagNode, isERBNode, isWhitespaceNode, isCommentNode, isLiteralNode, isERBCaseNode, isPureWhitespaceNode, isERBWhenNode, isHTMLElementNode, isEquivalentElement, findParentArray, removeNodeFromArray, replaceNodeWithBody, createLiteral, HTMLElementNode, PrismVisitor, splitLiteralsAtWhitespace, groupNodesByClass, filterERBContentNodes, isHTMLOpenTagNode, getTagLocalName, getAttribute, isERBCommentNode, getAttributes, findAttributeByName, isNode, LiteralNode, didyoumean, hasAttributeValue, isRubyLiteralNode, filterHTMLAttributeNodes, filterLiteralNodes, getAttributeValueQuoteType, Token, hasAttribute, isHTMLAttributeValueNode, isERBContentNode, getStaticAttributeName, isERBControlFlowNode, HTMLCloseTagNode, getTagName, createWhitespaceNode, filterWhitespaceNodes, getStaticContentFromNodes, getOpenTag, isHTMLCloseTagNode, HTMLOpenTagNode, DEFAULT_PARSER_OPTIONS } from '@herb-tools/core';
2
+ import { getNodesBeforePosition, getNodesAfterPosition, filterNodes, ERBContentNode, isERBOutputNode, isERBIfNode, isERBUnlessNode, isERBElseNode, isHTMLTextNode, Visitor, isToken, isParseResult, forEachAttribute, getAttributeName, hasDynamicAttributeName, getStaticAttributeValue, getAttributeValueNodes, hasERBOutput, isEffectivelyStatic, getValidatableStaticContent, getAttributeValue, getCombinedAttributeNameString, Location, Position, isERBOpenTagNode, isERBNode, isWhitespaceNode, isCommentNode, isLiteralNode, findParentArray, isHTMLOpenTagNode, isERBCaseNode, isPureWhitespaceNode, isERBWhenNode, isHTMLElementNode, isEquivalentElement, removeNodeFromArray, createLiteral, replaceNodeWithBody, HTMLElementNode, PrismVisitor, splitLiteralsAtWhitespace, groupNodesByClass, filterERBContentNodes, getTagLocalName, getAttribute, isERBCommentNode, getAttributes, findAttributeByName, isNode, LiteralNode, didyoumean, hasAttributeValue, isRubyLiteralNode, filterHTMLAttributeNodes, filterLiteralNodes, getAttributeValueQuoteType, Token, hasAttribute, isHTMLAttributeValueNode, isERBContentNode, isBooleanAttribute, getStaticAttributeName, isERBControlFlowNode, HTMLCloseTagNode, getTagName, createWhitespaceNode, filterWhitespaceNodes, getStaticContentFromNodes, getOpenTag, isHTMLCloseTagNode, HTMLOpenTagNode, DEFAULT_PARSER_OPTIONS } from '@herb-tools/core';
3
+ export { HTML_BOOLEAN_ATTRIBUTES, findAttributeByName, getAttribute, getAttributeName, getAttributeValue, getAttributeValueNodes, getAttributeValueQuoteType, getAttributes, getCombinedAttributeNameString, getStaticAttributeValue, getStaticAttributeValueContent, getTagName, hasAttribute, hasAttributeValue, hasDynamicAttributeName, hasDynamicAttributeValue, hasStaticAttributeValue, hasStaticAttributeValueContent, isAttributeValueQuoted, isBooleanAttribute } from '@herb-tools/core';
3
4
 
4
5
  class PrintContext {
5
6
  output = "";
@@ -439,6 +440,12 @@ class IdentityPrinter extends Printer {
439
440
  this.visit(node.end_node);
440
441
  }
441
442
  }
443
+ visitERBRenderNode(node) {
444
+ this.printERBNode(node);
445
+ }
446
+ visitRubyRenderLocalNode(_node) {
447
+ // extracted metadata, nothing to print
448
+ }
442
449
  visitERBYieldNode(node) {
443
450
  this.printERBNode(node);
444
451
  }
@@ -989,7 +996,7 @@ class ParserRule {
989
996
  get parserOptions() {
990
997
  return DEFAULT_LINTER_PARSER_OPTIONS;
991
998
  }
992
- createOffense(message, location, autofixContext, severity) {
999
+ createOffense(message, location, autofixContext, severity, tags) {
993
1000
  return {
994
1001
  rule: this.ruleName,
995
1002
  code: this.ruleName,
@@ -998,6 +1005,7 @@ class ParserRule {
998
1005
  location,
999
1006
  autofixContext,
1000
1007
  severity,
1008
+ tags,
1001
1009
  };
1002
1010
  }
1003
1011
  }
@@ -1017,7 +1025,7 @@ class LexerRule {
1017
1025
  get defaultConfig() {
1018
1026
  return DEFAULT_RULE_CONFIG;
1019
1027
  }
1020
- createOffense(message, location, autofixContext, severity) {
1028
+ createOffense(message, location, autofixContext, severity, tags) {
1021
1029
  return {
1022
1030
  rule: this.ruleName,
1023
1031
  code: this.ruleName,
@@ -1026,6 +1034,7 @@ class LexerRule {
1026
1034
  location,
1027
1035
  autofixContext,
1028
1036
  severity,
1037
+ tags,
1029
1038
  };
1030
1039
  }
1031
1040
  }
@@ -1051,7 +1060,7 @@ class SourceRule {
1051
1060
  get defaultConfig() {
1052
1061
  return DEFAULT_RULE_CONFIG;
1053
1062
  }
1054
- createOffense(message, location, autofixContext, severity) {
1063
+ createOffense(message, location, autofixContext, severity, tags) {
1055
1064
  return {
1056
1065
  rule: this.ruleName,
1057
1066
  code: this.ruleName,
@@ -1060,6 +1069,7 @@ class SourceRule {
1060
1069
  location,
1061
1070
  autofixContext,
1062
1071
  severity,
1072
+ tags,
1063
1073
  };
1064
1074
  }
1065
1075
  }
@@ -1085,7 +1095,7 @@ class BaseRuleVisitor extends Visitor {
1085
1095
  * Helper method to create an unbound lint offense (without severity).
1086
1096
  * The Linter will bind severity based on the rule's config.
1087
1097
  */
1088
- createOffense(message, location, autofixContext, severity) {
1098
+ createOffense(message, location, autofixContext, severity, tags) {
1089
1099
  return {
1090
1100
  rule: this.ruleName,
1091
1101
  code: this.ruleName,
@@ -1094,13 +1104,14 @@ class BaseRuleVisitor extends Visitor {
1094
1104
  location,
1095
1105
  autofixContext,
1096
1106
  severity,
1107
+ tags,
1097
1108
  };
1098
1109
  }
1099
1110
  /**
1100
1111
  * Helper method to add an offense to the offenses array
1101
1112
  */
1102
- addOffense(message, location, autofixContext, severity) {
1103
- this.offenses.push(this.createOffense(message, location, autofixContext, severity));
1113
+ addOffense(message, location, autofixContext, severity, tags) {
1114
+ this.offenses.push(this.createOffense(message, location, autofixContext, severity, tags));
1104
1115
  }
1105
1116
  }
1106
1117
  /**
@@ -1187,13 +1198,6 @@ const HTML_VOID_ELEMENTS = new Set([
1187
1198
  "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta",
1188
1199
  "param", "source", "track", "wbr",
1189
1200
  ]);
1190
- const HTML_BOOLEAN_ATTRIBUTES = new Set([
1191
- "autofocus", "autoplay", "checked", "controls", "defer", "disabled", "hidden",
1192
- "loop", "multiple", "muted", "readonly", "required", "reversed", "selected",
1193
- "open", "default", "formnovalidate", "novalidate", "itemscope", "scoped",
1194
- "seamless", "allowfullscreen", "async", "compact", "declare", "nohref",
1195
- "noresize", "noshade", "nowrap", "sortable", "truespeed", "typemustmatch"
1196
- ]);
1197
1201
  const HEADING_TAGS = new Set(["h1", "h2", "h3", "h4", "h5", "h6"]);
1198
1202
  /**
1199
1203
  * SVG elements that use camelCase naming
@@ -1348,12 +1352,6 @@ function isBlockElement(tagName) {
1348
1352
  function isVoidElement(tagName) {
1349
1353
  return HTML_VOID_ELEMENTS.has(tagName.toLowerCase());
1350
1354
  }
1351
- /**
1352
- * Checks if an attribute is a boolean attribute
1353
- */
1354
- function isBooleanAttribute(attributeName) {
1355
- return HTML_BOOLEAN_ATTRIBUTES.has(attributeName.toLowerCase());
1356
- }
1357
1355
  /**
1358
1356
  * Attribute visitor that provides granular processing based on both
1359
1357
  * attribute name type (static/dynamic) and value type (static/dynamic)
@@ -1376,7 +1374,7 @@ class AttributeVisitorMixin extends BaseRuleVisitor {
1376
1374
  forEachAttribute(node, (attributeNode) => {
1377
1375
  const staticAttributeName = getAttributeName(attributeNode);
1378
1376
  const originalAttributeName = getAttributeName(attributeNode, false) || "";
1379
- const isDynamicName = hasDynamicAttributeNameOnAttribute(attributeNode);
1377
+ const isDynamicName = hasDynamicAttributeName(attributeNode);
1380
1378
  const staticAttributeValue = getStaticAttributeValue(attributeNode);
1381
1379
  const valueNodes = getAttributeValueNodes(attributeNode);
1382
1380
  const hasOutputERB = hasERBOutput(valueNodes);
@@ -1453,7 +1451,7 @@ class BaseLexerRuleVisitor {
1453
1451
  * Helper method to create an unbound lint offense (without severity).
1454
1452
  * The Linter will bind severity based on the rule's config.
1455
1453
  */
1456
- createOffense(message, location, autofixContext, severity) {
1454
+ createOffense(message, location, autofixContext, severity, tags) {
1457
1455
  return {
1458
1456
  rule: this.ruleName,
1459
1457
  code: this.ruleName,
@@ -1462,13 +1460,14 @@ class BaseLexerRuleVisitor {
1462
1460
  location,
1463
1461
  autofixContext,
1464
1462
  severity,
1463
+ tags,
1465
1464
  };
1466
1465
  }
1467
1466
  /**
1468
1467
  * Helper method to add an offense to the offenses array
1469
1468
  */
1470
- addOffense(message, location, autofixContext, severity) {
1471
- this.offenses.push(this.createOffense(message, location, autofixContext, severity));
1469
+ addOffense(message, location, autofixContext, severity, tags) {
1470
+ this.offenses.push(this.createOffense(message, location, autofixContext, severity, tags));
1472
1471
  }
1473
1472
  /**
1474
1473
  * Main entry point for lexer rule visitors
@@ -1509,7 +1508,7 @@ class BaseSourceRuleVisitor {
1509
1508
  * Helper method to create an unbound lint offense (without severity).
1510
1509
  * The Linter will bind severity based on the rule's config.
1511
1510
  */
1512
- createOffense(message, location, autofixContext, severity) {
1511
+ createOffense(message, location, autofixContext, severity, tags) {
1513
1512
  return {
1514
1513
  rule: this.ruleName,
1515
1514
  code: this.ruleName,
@@ -1518,13 +1517,14 @@ class BaseSourceRuleVisitor {
1518
1517
  location,
1519
1518
  autofixContext,
1520
1519
  severity,
1520
+ tags,
1521
1521
  };
1522
1522
  }
1523
1523
  /**
1524
1524
  * Helper method to add an offense to the offenses array
1525
1525
  */
1526
- addOffense(message, location, autofixContext, severity) {
1527
- this.offenses.push(this.createOffense(message, location, autofixContext, severity));
1526
+ addOffense(message, location, autofixContext, severity, tags) {
1527
+ this.offenses.push(this.createOffense(message, location, autofixContext, severity, tags));
1528
1528
  }
1529
1529
  /**
1530
1530
  * Main entry point for source rule visitors
@@ -1874,6 +1874,34 @@ class ActionViewNoSilentHelperRule extends ParserRule {
1874
1874
  }
1875
1875
  }
1876
1876
 
1877
+ class ActionViewNoSilentRenderVisitor extends BaseRuleVisitor {
1878
+ visitERBRenderNode(node) {
1879
+ if (!isERBOutputNode(node)) {
1880
+ this.addOffense(`Avoid using \`${node.tag_opening?.value} %>\` with \`render\`. Use \`<%= %>\` to ensure the rendered content is output.`, node.location);
1881
+ }
1882
+ this.visitChildNodes(node);
1883
+ }
1884
+ }
1885
+ class ActionViewNoSilentRenderRule extends ParserRule {
1886
+ static ruleName = "actionview-no-silent-render";
1887
+ get defaultConfig() {
1888
+ return {
1889
+ enabled: true,
1890
+ severity: "error"
1891
+ };
1892
+ }
1893
+ get parserOptions() {
1894
+ return {
1895
+ render_nodes: true,
1896
+ };
1897
+ }
1898
+ check(result, context) {
1899
+ const visitor = new ActionViewNoSilentRenderVisitor(this.ruleName, context);
1900
+ visitor.visit(result.value);
1901
+ return visitor.offenses;
1902
+ }
1903
+ }
1904
+
1877
1905
  class ERBCommentSyntaxVisitor extends BaseRuleVisitor {
1878
1906
  visitERBContentNode(node) {
1879
1907
  const content = node.content?.value || "";
@@ -1938,7 +1966,9 @@ class ERBNoCaseNodeChildrenVisitor extends BaseRuleVisitor {
1938
1966
  for (const child of node.children) {
1939
1967
  if (!this.isAllowedContent(child)) {
1940
1968
  const childCode = IdentityPrinter.print(child).trim();
1941
- this.addOffense(`Do not place \`${childCode}\` between \`${caseCode}\` and \`${conditionCode}\`. Content here is not part of any branch and will not be rendered.`, child.location);
1969
+ const offense = this.createOffense(`Do not place \`${childCode}\` between \`${caseCode}\` and \`${conditionCode}\`. Content here is not part of any branch and will not be rendered.`, child.location);
1970
+ offense.tags = ["unnecessary"];
1971
+ this.offenses.push(offense);
1942
1972
  }
1943
1973
  }
1944
1974
  }
@@ -1968,6 +1998,193 @@ class ERBNoCaseNodeChildrenRule extends ParserRule {
1968
1998
  }
1969
1999
  }
1970
2000
 
2001
+ class ERBNoEmptyControlFlowVisitor extends BaseRuleVisitor {
2002
+ processedIfNodes = new Set();
2003
+ processedElseNodes = new Set();
2004
+ visitERBIfNode(node) {
2005
+ if (this.processedIfNodes.has(node)) {
2006
+ return;
2007
+ }
2008
+ this.markIfChainAsProcessed(node);
2009
+ this.markElseNodesInIfChain(node);
2010
+ const entireChainEmpty = this.isEntireIfChainEmpty(node);
2011
+ if (entireChainEmpty) {
2012
+ this.addEmptyBlockOffense(node, node.statements, "if");
2013
+ }
2014
+ else {
2015
+ this.checkIfChainParts(node);
2016
+ }
2017
+ this.visitChildNodes(node);
2018
+ }
2019
+ visitERBElseNode(node) {
2020
+ if (this.processedElseNodes.has(node)) {
2021
+ this.visitChildNodes(node);
2022
+ return;
2023
+ }
2024
+ this.addEmptyBlockOffense(node, node.statements, "else");
2025
+ this.visitChildNodes(node);
2026
+ }
2027
+ visitERBUnlessNode(node) {
2028
+ const unlessHasContent = this.statementsHaveContent(node.statements);
2029
+ const elseHasContent = node.else_clause && this.statementsHaveContent(node.else_clause.statements);
2030
+ if (node.else_clause) {
2031
+ this.processedElseNodes.add(node.else_clause);
2032
+ }
2033
+ const entireBlockEmpty = !unlessHasContent && !elseHasContent;
2034
+ if (entireBlockEmpty) {
2035
+ this.addEmptyBlockOffense(node, node.statements, "unless");
2036
+ }
2037
+ else {
2038
+ if (!unlessHasContent) {
2039
+ this.addEmptyBlockOffenseWithEnd(node, node.statements, "unless", node.else_clause);
2040
+ }
2041
+ if (node.else_clause && !elseHasContent) {
2042
+ this.addEmptyBlockOffense(node.else_clause, node.else_clause.statements, "else");
2043
+ }
2044
+ }
2045
+ this.visitChildNodes(node);
2046
+ }
2047
+ visitERBForNode(node) {
2048
+ this.addEmptyBlockOffense(node, node.statements, "for");
2049
+ this.visitChildNodes(node);
2050
+ }
2051
+ visitERBWhileNode(node) {
2052
+ this.addEmptyBlockOffense(node, node.statements, "while");
2053
+ this.visitChildNodes(node);
2054
+ }
2055
+ visitERBUntilNode(node) {
2056
+ this.addEmptyBlockOffense(node, node.statements, "until");
2057
+ this.visitChildNodes(node);
2058
+ }
2059
+ visitERBWhenNode(node) {
2060
+ if (!node.then_keyword) {
2061
+ this.addEmptyBlockOffense(node, node.statements, "when");
2062
+ }
2063
+ this.visitChildNodes(node);
2064
+ }
2065
+ visitERBInNode(node) {
2066
+ if (!node.then_keyword) {
2067
+ this.addEmptyBlockOffense(node, node.statements, "in");
2068
+ }
2069
+ this.visitChildNodes(node);
2070
+ }
2071
+ visitERBBeginNode(node) {
2072
+ this.addEmptyBlockOffense(node, node.statements, "begin");
2073
+ this.visitChildNodes(node);
2074
+ }
2075
+ visitERBRescueNode(node) {
2076
+ this.addEmptyBlockOffense(node, node.statements, "rescue");
2077
+ this.visitChildNodes(node);
2078
+ }
2079
+ visitERBEnsureNode(node) {
2080
+ this.addEmptyBlockOffense(node, node.statements, "ensure");
2081
+ this.visitChildNodes(node);
2082
+ }
2083
+ visitERBBlockNode(node) {
2084
+ this.addEmptyBlockOffense(node, node.body, "do");
2085
+ this.visitChildNodes(node);
2086
+ }
2087
+ addEmptyBlockOffense(node, statements, blockType) {
2088
+ this.addEmptyBlockOffenseWithEnd(node, statements, blockType, null);
2089
+ }
2090
+ addEmptyBlockOffenseWithEnd(node, statements, blockType, subsequentNode) {
2091
+ if (this.statementsHaveContent(statements)) {
2092
+ return;
2093
+ }
2094
+ const startLocation = node.location.start;
2095
+ const endLocation = subsequentNode
2096
+ ? subsequentNode.location.start
2097
+ : node.location.end;
2098
+ const location = Location.from(startLocation.line, startLocation.column, endLocation.line, endLocation.column);
2099
+ const offense = this.createOffense(`Empty ${blockType} block: this control flow statement has no content`, location);
2100
+ offense.tags = ["unnecessary"];
2101
+ this.offenses.push(offense);
2102
+ }
2103
+ statementsHaveContent(statements) {
2104
+ return statements.some(statement => {
2105
+ if (isHTMLTextNode(statement)) {
2106
+ return statement.content.trim() !== "";
2107
+ }
2108
+ return true;
2109
+ });
2110
+ }
2111
+ markIfChainAsProcessed(node) {
2112
+ this.processedIfNodes.add(node);
2113
+ this.traverseSubsequentNodes(node.subsequent, (current) => {
2114
+ if (isERBIfNode(current)) {
2115
+ this.processedIfNodes.add(current);
2116
+ }
2117
+ });
2118
+ }
2119
+ markElseNodesInIfChain(node) {
2120
+ this.traverseSubsequentNodes(node.subsequent, (current) => {
2121
+ if (isERBElseNode(current)) {
2122
+ this.processedElseNodes.add(current);
2123
+ }
2124
+ });
2125
+ }
2126
+ traverseSubsequentNodes(startNode, callback) {
2127
+ let current = startNode;
2128
+ while (current) {
2129
+ if (isERBIfNode(current)) {
2130
+ callback(current);
2131
+ current = current.subsequent;
2132
+ }
2133
+ else if (isERBElseNode(current)) {
2134
+ callback(current);
2135
+ break;
2136
+ }
2137
+ else {
2138
+ break;
2139
+ }
2140
+ }
2141
+ }
2142
+ checkIfChainParts(node) {
2143
+ if (!this.statementsHaveContent(node.statements)) {
2144
+ this.addEmptyBlockOffenseWithEnd(node, node.statements, "if", node.subsequent);
2145
+ }
2146
+ this.traverseSubsequentNodes(node.subsequent, (current) => {
2147
+ if (this.statementsHaveContent(current.statements)) {
2148
+ return;
2149
+ }
2150
+ const blockType = isERBIfNode(current) ? "elsif" : "else";
2151
+ const nextSubsequent = isERBIfNode(current) ? current.subsequent : null;
2152
+ if (nextSubsequent) {
2153
+ this.addEmptyBlockOffenseWithEnd(current, current.statements, blockType, nextSubsequent);
2154
+ }
2155
+ else {
2156
+ this.addEmptyBlockOffense(current, current.statements, blockType);
2157
+ }
2158
+ });
2159
+ }
2160
+ isEntireIfChainEmpty(node) {
2161
+ if (this.statementsHaveContent(node.statements)) {
2162
+ return false;
2163
+ }
2164
+ let hasContent = false;
2165
+ this.traverseSubsequentNodes(node.subsequent, (current) => {
2166
+ if (this.statementsHaveContent(current.statements)) {
2167
+ hasContent = true;
2168
+ }
2169
+ });
2170
+ return !hasContent;
2171
+ }
2172
+ }
2173
+ class ERBNoEmptyControlFlowRule extends ParserRule {
2174
+ static ruleName = "erb-no-empty-control-flow";
2175
+ get defaultConfig() {
2176
+ return {
2177
+ enabled: true,
2178
+ severity: "hint"
2179
+ };
2180
+ }
2181
+ check(result, context) {
2182
+ const visitor = new ERBNoEmptyControlFlowVisitor(this.ruleName, context);
2183
+ visitor.visit(result.value);
2184
+ return visitor.offenses;
2185
+ }
2186
+ }
2187
+
1971
2188
  function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
1972
2189
  function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
1973
2190
  function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
@@ -2126,6 +2343,15 @@ class ERBNoConditionalOpenTagRule extends ParserRule {
2126
2343
  function getSignificantNodes(statements) {
2127
2344
  return statements.filter(node => !isPureWhitespaceNode(node));
2128
2345
  }
2346
+ function trimWhitespaceNodes(nodes) {
2347
+ let start = 0;
2348
+ let end = nodes.length;
2349
+ while (start < end && isPureWhitespaceNode(nodes[start]))
2350
+ start++;
2351
+ while (end > start && isPureWhitespaceNode(nodes[end - 1]))
2352
+ end--;
2353
+ return nodes.slice(start, end);
2354
+ }
2129
2355
  function allEquivalentElements(nodes) {
2130
2356
  if (nodes.length < 2)
2131
2357
  return false;
@@ -2243,9 +2469,19 @@ class ERBNoDuplicateBranchElementsVisitor extends BaseRuleVisitor {
2243
2469
  if (isERBIfNode(node)) {
2244
2470
  this.markSubsequentIfNodesAsProcessed(node);
2245
2471
  }
2472
+ if (this.allBranchesIdentical(branches)) {
2473
+ this.addOffense("All branches of this conditional have identical content. The conditional can be removed.", node.location, { node: node, allIdentical: true }, "warning");
2474
+ return;
2475
+ }
2246
2476
  const state = { isFirstOffense: true };
2247
2477
  this.checkBranches(branches, node, state);
2248
2478
  }
2479
+ allBranchesIdentical(branches) {
2480
+ if (branches.length < 2)
2481
+ return false;
2482
+ const first = branches[0].map(node => IdentityPrinter.print(node)).join("");
2483
+ return branches.slice(1).every(branch => branch.map(node => IdentityPrinter.print(node)).join("") === first);
2484
+ }
2249
2485
  markSubsequentIfNodesAsProcessed(node) {
2250
2486
  let current = node.subsequent;
2251
2487
  while (current) {
@@ -2279,11 +2515,23 @@ class ERBNoDuplicateBranchElementsVisitor extends BaseRuleVisitor {
2279
2515
  const bodiesMatch = elements.every(element => IdentityPrinter.print(element) === IdentityPrinter.print(elements[0]));
2280
2516
  for (const element of elements) {
2281
2517
  const printed = IdentityPrinter.print(element.open_tag);
2282
- const autofixContext = state.isFirstOffense
2283
- ? { node: conditionalNode }
2284
- : undefined;
2285
- this.addOffense(`The \`${printed}\` element is duplicated across all branches of this conditional and can be moved outside.`, bodiesMatch ? element.location : (element?.open_tag?.location || element.location), autofixContext);
2286
- state.isFirstOffense = false;
2518
+ if (bodiesMatch) {
2519
+ const autofixContext = state.isFirstOffense
2520
+ ? { node: conditionalNode }
2521
+ : undefined;
2522
+ this.addOffense(`The \`${printed}\` element is duplicated across all branches of this conditional and can be moved outside.`, element.location, autofixContext);
2523
+ state.isFirstOffense = false;
2524
+ }
2525
+ else {
2526
+ const autofixContext = state.isFirstOffense
2527
+ ? { node: conditionalNode }
2528
+ : undefined;
2529
+ const tagNameLocation = isHTMLOpenTagNode(element.open_tag) && element.open_tag.tag_name?.location
2530
+ ? element.open_tag.tag_name.location
2531
+ : element?.open_tag?.location || element.location;
2532
+ this.addOffense(`The \`${printed}\` tag is repeated across all branches with different content. Consider extracting the shared tag outside the conditional.`, tagNameLocation, autofixContext, "hint");
2533
+ state.isFirstOffense = false;
2534
+ }
2287
2535
  }
2288
2536
  if (!bodiesMatch && bodies.every(body => body.length > 0)) {
2289
2537
  this.checkBranches(bodies, conditionalNode, state);
@@ -2312,6 +2560,15 @@ class ERBNoDuplicateBranchElementsRule extends ParserRule {
2312
2560
  const branches = collectBranches(conditionalNode);
2313
2561
  if (!branches)
2314
2562
  return null;
2563
+ if (offense.autofixContext.allIdentical) {
2564
+ const parentInfo = findParentArray(result.value, conditionalNode);
2565
+ if (!parentInfo)
2566
+ return null;
2567
+ const { array: parentArray, index: conditionalIndex } = parentInfo;
2568
+ const firstBranchContent = trimWhitespaceNodes(branches[0]);
2569
+ parentArray.splice(conditionalIndex, 1, ...firstBranchContent);
2570
+ return result;
2571
+ }
2315
2572
  const significantBranches = branches.map(getSignificantNodes);
2316
2573
  if (significantBranches.some(branch => branch.length === 0))
2317
2574
  return null;
@@ -2325,23 +2582,51 @@ class ERBNoDuplicateBranchElementsRule extends ParserRule {
2325
2582
  return null;
2326
2583
  let { array: parentArray, index: conditionalIndex } = parentInfo;
2327
2584
  let hasWrapped = false;
2585
+ let didMutate = false;
2586
+ let failedToHoistPrefix = false;
2587
+ let hoistedBefore = false;
2328
2588
  const hoistElement = (elements, position) => {
2589
+ const actualPosition = (position === "before" && failedToHoistPrefix) ? "after" : position;
2329
2590
  const bodiesMatch = elements.every(element => IdentityPrinter.print(element) === IdentityPrinter.print(elements[0]));
2330
2591
  if (bodiesMatch) {
2592
+ if (actualPosition === "after") {
2593
+ const currentLengths = branches.map(b => getSignificantNodes(b).length);
2594
+ if (currentLengths.some(l => l !== currentLengths[0]))
2595
+ return;
2596
+ }
2597
+ if (actualPosition === "after" && position === "before") {
2598
+ const isAtEnd = branches.every((branch, index) => {
2599
+ const nodes = getSignificantNodes(branch);
2600
+ return nodes.length > 0 && nodes[nodes.length - 1] === elements[index];
2601
+ });
2602
+ if (!isAtEnd)
2603
+ return;
2604
+ }
2331
2605
  for (let i = 0; i < branches.length; i++) {
2332
2606
  removeNodeFromArray(branches[i], elements[i]);
2333
2607
  }
2334
- if (position === "before") {
2335
- parentArray.splice(conditionalIndex, 0, elements[0]);
2336
- conditionalIndex++;
2608
+ if (actualPosition === "before") {
2609
+ parentArray.splice(conditionalIndex, 0, elements[0], createLiteral("\n"));
2610
+ conditionalIndex += 2;
2611
+ hoistedBefore = true;
2337
2612
  }
2338
2613
  else {
2339
- parentArray.splice(conditionalIndex + 1, 0, elements[0]);
2614
+ parentArray.splice(conditionalIndex + 1, 0, createLiteral("\n"), elements[0]);
2340
2615
  }
2616
+ didMutate = true;
2341
2617
  }
2342
2618
  else {
2343
2619
  if (hasWrapped)
2344
2620
  return;
2621
+ const canWrap = branches.every((branch, index) => {
2622
+ const remaining = getSignificantNodes(branch);
2623
+ return remaining.length === 1 && remaining[0] === elements[index];
2624
+ });
2625
+ if (!canWrap) {
2626
+ if (position === "before")
2627
+ failedToHoistPrefix = true;
2628
+ return;
2629
+ }
2345
2630
  for (let i = 0; i < branches.length; i++) {
2346
2631
  replaceNodeWithBody(branches[i], elements[i]);
2347
2632
  }
@@ -2350,6 +2635,7 @@ class ERBNoDuplicateBranchElementsRule extends ParserRule {
2350
2635
  parentArray = wrapper.body;
2351
2636
  conditionalIndex = 1;
2352
2637
  hasWrapped = true;
2638
+ didMutate = true;
2353
2639
  }
2354
2640
  };
2355
2641
  for (let index = 0; index < prefixCount; index++) {
@@ -2360,7 +2646,22 @@ class ERBNoDuplicateBranchElementsRule extends ParserRule {
2360
2646
  const elements = significantBranches.map(branch => branch[branch.length - 1 - offset]);
2361
2647
  hoistElement(elements, "after");
2362
2648
  }
2363
- return result;
2649
+ if (!hasWrapped && hoistedBefore) {
2650
+ const remaining = branches.map(branch => getSignificantNodes(branch));
2651
+ if (remaining.every(branch => branch.length === 1) && allEquivalentElements(remaining.map(b => b[0]))) {
2652
+ const elements = remaining.map(b => b[0]);
2653
+ const bodiesMatch = elements.every(el => IdentityPrinter.print(el) === IdentityPrinter.print(elements[0]));
2654
+ if (!bodiesMatch && elements.every(el => el.body.length > 0)) {
2655
+ for (let i = 0; i < branches.length; i++) {
2656
+ replaceNodeWithBody(branches[i], elements[i]);
2657
+ }
2658
+ const wrapper = createWrapper(elements[0], [createLiteral("\n"), conditionalNode, createLiteral("\n")]);
2659
+ parentArray[conditionalIndex] = wrapper;
2660
+ didMutate = true;
2661
+ }
2662
+ }
2663
+ }
2664
+ return didMutate ? result : null;
2364
2665
  }
2365
2666
  }
2366
2667
 
@@ -2886,6 +3187,47 @@ class ERBNoRawOutputInAttributeValueRule extends ParserRule {
2886
3187
  }
2887
3188
  }
2888
3189
 
3190
+ function isAssignmentNode(prismNode) {
3191
+ const type = prismNode?.constructor?.name;
3192
+ if (!type)
3193
+ return false;
3194
+ return type.endsWith("WriteNode");
3195
+ }
3196
+ class ERBNoSilentStatementVisitor extends BaseRuleVisitor {
3197
+ visitERBContentNode(node) {
3198
+ if (isERBOutputNode(node))
3199
+ return;
3200
+ const prismNode = node.prismNode;
3201
+ if (!prismNode)
3202
+ return;
3203
+ if (isAssignmentNode(prismNode))
3204
+ return;
3205
+ const content = node.content?.value?.trim();
3206
+ if (!content)
3207
+ return;
3208
+ this.addOffense(`Avoid using silent ERB tags for statements. Move \`${content}\` to a controller, helper, or presenter.`, node.location);
3209
+ }
3210
+ }
3211
+ class ERBNoSilentStatementRule extends ParserRule {
3212
+ static ruleName = "erb-no-silent-statement";
3213
+ get defaultConfig() {
3214
+ return {
3215
+ enabled: false,
3216
+ severity: "warning"
3217
+ };
3218
+ }
3219
+ get parserOptions() {
3220
+ return {
3221
+ prism_nodes: true,
3222
+ };
3223
+ }
3224
+ check(result, context) {
3225
+ const visitor = new ERBNoSilentStatementVisitor(this.ruleName, context);
3226
+ visitor.visit(result.value);
3227
+ return visitor.offenses;
3228
+ }
3229
+ }
3230
+
2889
3231
  class ERBNoSilentTagInAttributeNameVisitor extends BaseRuleVisitor {
2890
3232
  visitHTMLAttributeNameNode(node) {
2891
3233
  const erbNodes = filterERBContentNodes(node.children);
@@ -3148,7 +3490,7 @@ class ERBNoTrailingWhitespaceRule extends ParserRule {
3148
3490
  }
3149
3491
 
3150
3492
  const JS_ATTRIBUTE_PATTERN = /^on/i;
3151
- const SAFE_PATTERN$1 = /\.to_json\s*$|\bj\s*[\s(]|\bescape_javascript\s*[\s(]/;
3493
+ const SAFE_PATTERN = /\.to_json\s*$|\bj\s*[\s(]|\bescape_javascript\s*[\s(]/;
3152
3494
  class ERBNoUnsafeJSAttributeVisitor extends AttributeVisitorMixin {
3153
3495
  checkStaticAttributeDynamicValue({ attributeName, valueNodes }) {
3154
3496
  if (!JS_ATTRIBUTE_PATTERN.test(attributeName))
@@ -3159,7 +3501,7 @@ class ERBNoUnsafeJSAttributeVisitor extends AttributeVisitorMixin {
3159
3501
  if (!isERBOutputNode(node))
3160
3502
  continue;
3161
3503
  const content = node.content?.value?.trim() || "";
3162
- if (SAFE_PATTERN$1.test(content))
3504
+ if (SAFE_PATTERN.test(content))
3163
3505
  continue;
3164
3506
  this.addOffense(`Unsafe ERB output in \`${attributeName}\` attribute. Use \`.to_json\`, \`j()\`, or \`escape_javascript()\` to safely encode values.`, node.location);
3165
3507
  }
@@ -3240,7 +3582,27 @@ class ERBNoUnsafeRawRule extends ParserRule {
3240
3582
  }
3241
3583
  }
3242
3584
 
3243
- const SAFE_PATTERN = /\.to_json\b/;
3585
+ const SAFE_METHOD_NAMES = new Set([
3586
+ "to_json",
3587
+ "json_escape",
3588
+ ]);
3589
+ const ESCAPE_JAVASCRIPT_METHOD_NAMES = new Set([
3590
+ "j",
3591
+ "escape_javascript",
3592
+ ]);
3593
+ class SafeCallDetector extends PrismVisitor {
3594
+ hasSafeCall = false;
3595
+ hasEscapeJavascriptCall = false;
3596
+ visitCallNode(node) {
3597
+ if (SAFE_METHOD_NAMES.has(node.name)) {
3598
+ this.hasSafeCall = true;
3599
+ }
3600
+ if (ESCAPE_JAVASCRIPT_METHOD_NAMES.has(node.name)) {
3601
+ this.hasEscapeJavascriptCall = true;
3602
+ }
3603
+ this.visitChildNodes(node);
3604
+ }
3605
+ }
3244
3606
  class ERBNoUnsafeScriptInterpolationVisitor extends BaseRuleVisitor {
3245
3607
  visitHTMLElementNode(node) {
3246
3608
  if (!isHTMLOpenTagNode(node.open_tag)) {
@@ -3269,9 +3631,17 @@ class ERBNoUnsafeScriptInterpolationVisitor extends BaseRuleVisitor {
3269
3631
  continue;
3270
3632
  if (!isERBOutputNode(child))
3271
3633
  continue;
3272
- const content = child.content?.value?.trim() || "";
3273
- if (SAFE_PATTERN.test(content))
3634
+ const erbContent = child;
3635
+ const prismNode = erbContent.prismNode;
3636
+ const detector = new SafeCallDetector();
3637
+ if (prismNode)
3638
+ detector.visit(prismNode);
3639
+ if (detector.hasSafeCall)
3640
+ continue;
3641
+ if (detector.hasEscapeJavascriptCall) {
3642
+ this.addOffense("Avoid `j()` / `escape_javascript()` in `<script>` tags. It is only safe inside quoted string literals. Use `.to_json` instead, which is safe in any position.", child.location);
3274
3643
  continue;
3644
+ }
3275
3645
  this.addOffense("Unsafe ERB output in `<script>` tag. Use `.to_json` to safely serialize values into JavaScript.", child.location);
3276
3646
  }
3277
3647
  }
@@ -3284,6 +3654,11 @@ class ERBNoUnsafeScriptInterpolationRule extends ParserRule {
3284
3654
  severity: "error"
3285
3655
  };
3286
3656
  }
3657
+ get parserOptions() {
3658
+ return {
3659
+ prism_nodes: true,
3660
+ };
3661
+ }
3287
3662
  check(result, context) {
3288
3663
  const visitor = new ERBNoUnsafeScriptInterpolationVisitor(this.ruleName, context);
3289
3664
  visitor.visit(result.value);
@@ -4291,7 +4666,7 @@ class HerbDisableCommentValidRuleNameRule extends ParserRule {
4291
4666
  }
4292
4667
  }
4293
4668
 
4294
- const ALLOWED_TYPES = ["text/javascript"];
4669
+ const ALLOWED_TYPES = ["text/javascript", "module", "importmap", "speculationrules"];
4295
4670
  class AllowedScriptTypeVisitor extends BaseRuleVisitor {
4296
4671
  visitHTMLOpenTagNode(node) {
4297
4672
  if (getTagLocalName(node) === "script") {
@@ -4829,6 +5204,47 @@ class HTMLBodyOnlyElementsRule extends ParserRule {
4829
5204
  }
4830
5205
  }
4831
5206
 
5207
+ class BooleanAttributesNoValueVisitor extends AttributeVisitorMixin {
5208
+ checkStaticAttributeStaticValue({ originalAttributeName, attributeNode }) {
5209
+ this.checkAttribute(originalAttributeName, attributeNode);
5210
+ }
5211
+ checkStaticAttributeDynamicValue({ originalAttributeName, attributeNode }) {
5212
+ this.checkAttribute(originalAttributeName, attributeNode);
5213
+ }
5214
+ checkAttribute(attributeName, attributeNode) {
5215
+ if (!isBooleanAttribute(attributeName))
5216
+ return;
5217
+ if (!hasAttributeValue(attributeNode))
5218
+ return;
5219
+ this.addOffense(`Boolean attribute \`${IdentityPrinter.print(attributeNode.name)}\` should not have a value. Use \`${attributeName.toLowerCase()}\` instead of \`${IdentityPrinter.print(attributeNode)}\`.`, attributeNode.value.location, {
5220
+ node: attributeNode
5221
+ });
5222
+ }
5223
+ }
5224
+ class HTMLBooleanAttributesNoValueRule extends ParserRule {
5225
+ static autocorrectable = true;
5226
+ static ruleName = "html-boolean-attributes-no-value";
5227
+ get defaultConfig() {
5228
+ return {
5229
+ enabled: true,
5230
+ severity: "error"
5231
+ };
5232
+ }
5233
+ check(result, context) {
5234
+ const visitor = new BooleanAttributesNoValueVisitor(this.ruleName, context);
5235
+ visitor.visit(result.value);
5236
+ return visitor.offenses;
5237
+ }
5238
+ autofix(offense, result, _context) {
5239
+ if (!offense.autofixContext)
5240
+ return null;
5241
+ const { node } = offense.autofixContext;
5242
+ node.equals = null;
5243
+ node.value = null;
5244
+ return result;
5245
+ }
5246
+ }
5247
+
4832
5248
  class DetailsHasSummaryVisitor extends BaseRuleVisitor {
4833
5249
  visitHTMLElementNode(node) {
4834
5250
  this.checkDetailsElement(node);
@@ -4878,47 +5294,6 @@ class HTMLDetailsHasSummaryRule extends ParserRule {
4878
5294
  }
4879
5295
  }
4880
5296
 
4881
- class BooleanAttributesNoValueVisitor extends AttributeVisitorMixin {
4882
- checkStaticAttributeStaticValue({ originalAttributeName, attributeNode }) {
4883
- this.checkAttribute(originalAttributeName, attributeNode);
4884
- }
4885
- checkStaticAttributeDynamicValue({ originalAttributeName, attributeNode }) {
4886
- this.checkAttribute(originalAttributeName, attributeNode);
4887
- }
4888
- checkAttribute(attributeName, attributeNode) {
4889
- if (!isBooleanAttribute(attributeName))
4890
- return;
4891
- if (!hasAttributeValue(attributeNode))
4892
- return;
4893
- this.addOffense(`Boolean attribute \`${IdentityPrinter.print(attributeNode.name)}\` should not have a value. Use \`${attributeName.toLowerCase()}\` instead of \`${IdentityPrinter.print(attributeNode)}\`.`, attributeNode.value.location, {
4894
- node: attributeNode
4895
- });
4896
- }
4897
- }
4898
- class HTMLBooleanAttributesNoValueRule extends ParserRule {
4899
- static autocorrectable = true;
4900
- static ruleName = "html-boolean-attributes-no-value";
4901
- get defaultConfig() {
4902
- return {
4903
- enabled: true,
4904
- severity: "error"
4905
- };
4906
- }
4907
- check(result, context) {
4908
- const visitor = new BooleanAttributesNoValueVisitor(this.ruleName, context);
4909
- visitor.visit(result.value);
4910
- return visitor.offenses;
4911
- }
4912
- autofix(offense, result, _context) {
4913
- if (!offense.autofixContext)
4914
- return null;
4915
- const { node } = offense.autofixContext;
4916
- node.equals = null;
4917
- node.value = null;
4918
- return result;
4919
- }
4920
- }
4921
-
4922
5297
  class HeadOnlyElementsVisitor extends BaseRuleVisitor {
4923
5298
  elementStack = [];
4924
5299
  visitHTMLElementNode(node) {
@@ -6601,8 +6976,10 @@ class TurboPermanentRequireIdRule extends ParserRule {
6601
6976
 
6602
6977
  const rules = [
6603
6978
  ActionViewNoSilentHelperRule,
6979
+ ActionViewNoSilentRenderRule,
6604
6980
  ERBCommentSyntax,
6605
6981
  ERBNoCaseNodeChildrenRule,
6982
+ ERBNoEmptyControlFlowRule,
6606
6983
  ERBNoConditionalHTMLElementRule,
6607
6984
  ERBNoConditionalOpenTagRule,
6608
6985
  ERBNoDuplicateBranchElementsRule,
@@ -6617,6 +6994,7 @@ const rules = [
6617
6994
  ERBNoOutputInAttributeNameRule,
6618
6995
  ERBNoOutputInAttributePositionRule,
6619
6996
  ERBNoRawOutputInAttributeValueRule,
6997
+ ERBNoSilentStatementRule,
6620
6998
  ERBNoSilentTagInAttributeNameRule,
6621
6999
  ERBNoStatementInScriptRule,
6622
7000
  ERBNoThenInControlFlowRule,
@@ -6648,8 +7026,8 @@ const rules = [
6648
7026
  HTMLAttributeValuesRequireQuotesRule,
6649
7027
  HTMLAvoidBothDisabledAndAriaDisabledRule,
6650
7028
  HTMLBodyOnlyElementsRule,
6651
- HTMLDetailsHasSummaryRule,
6652
7029
  HTMLBooleanAttributesNoValueRule,
7030
+ HTMLDetailsHasSummaryRule,
6653
7031
  HTMLHeadOnlyElementsRule,
6654
7032
  HTMLIframeHasTitleRule,
6655
7033
  HTMLImgRequireAltRule,
@@ -7207,5 +7585,5 @@ function ruleDocumentationUrl(ruleId) {
7207
7585
  return `${DOCS_BASE_URL}/${ruleId}`;
7208
7586
  }
7209
7587
 
7210
- export { ABSTRACT_ARIA_ROLES, ARIA_ATTRIBUTES, AttributeVisitorMixin, BaseLexerRuleVisitor, BaseRuleVisitor, BaseSourceRuleVisitor, ControlFlowTrackingVisitor, ControlFlowType, DEFAULT_LINTER_PARSER_OPTIONS, DEFAULT_LINT_CONTEXT, DEFAULT_RULE_CONFIG, DOCUMENT_ONLY_TAG_NAMES, ERBCommentSyntax, ERBNoCaseNodeChildrenRule, ERBNoConditionalOpenTagRule, ERBNoDuplicateBranchElementsRule, ERBNoEmptyTagsRule, ERBNoExtraNewLineRule, ERBNoExtraWhitespaceRule, ERBNoInlineCaseConditionsRule, ERBNoInstanceVariablesInPartialsRule, ERBNoJavascriptTagHelperRule, ERBNoOutputControlFlowRule, ERBNoOutputInAttributeNameRule, ERBNoOutputInAttributePositionRule, ERBNoRawOutputInAttributeValueRule, ERBNoSilentTagInAttributeNameRule, ERBNoStatementInScriptRule, ERBNoThenInControlFlowRule, ERBNoTrailingWhitespaceRule, ERBNoUnsafeJSAttributeRule, ERBNoUnsafeRawRule, ERBNoUnsafeScriptInterpolationRule, ERBPreferImageTagHelperRule, ERBRequireTrailingNewlineRule, ERBRequireWhitespaceRule, ERBRightTrimRule, ERBStrictLocalsCommentSyntaxRule, ERBStrictLocalsRequiredRule, HEADING_TAGS, HEAD_AND_BODY_TAG_NAMES, HEAD_ONLY_TAG_NAMES, HTMLAllowedScriptTypeRule, HTMLAnchorRequireHrefRule, HTMLAriaLabelIsWellFormattedRule, HTMLAriaLevelMustBeValidRule, HTMLAriaRoleHeadingRequiresLevelRule, HTMLAriaRoleMustBeValidRule, HTMLAttributeDoubleQuotesRule, HTMLAttributeEqualsSpacingRule, HTMLAttributeValuesRequireQuotesRule, HTMLAvoidBothDisabledAndAriaDisabledRule, HTMLBodyOnlyElementsRule, HTMLBooleanAttributesNoValueRule, HTMLDetailsHasSummaryRule, HTMLHeadOnlyElementsRule, HTMLIframeHasTitleRule, HTMLImgRequireAltRule, HTMLInputRequireAutocompleteRule, HTMLNavigationHasLabelRule, HTMLNoAbstractRolesRule, HTMLNoAriaHiddenOnBodyRule, HTMLNoAriaHiddenOnFocusableRule, HTMLNoBlockInsideInlineRule, HTMLNoDuplicateAttributesRule, HTMLNoDuplicateIdsRule, HTMLNoDuplicateMetaNamesRule, HTMLNoEmptyAttributesRule, HTMLNoEmptyHeadingsRule, HTMLNoNestedLinksRule, HTMLNoPositiveTabIndexRule, HTMLNoSelfClosingRule, HTMLNoSpaceInTagRule, HTMLNoTitleAttributeRule, HTMLNoUnderscoresInAttributeNamesRule, HTMLRequireClosingTagsRule, 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, findNodeAtPosition, findNodeByLocation, findParent, getBasename, hasBalancedParentheses, isBlockElement, isBodyOnlyTag, isBodyTag, isBooleanAttribute, isDocumentOnlyTag, isHeadAndBodyTag, isHeadOnlyTag, isHeadTag, isHtmlOnlyTag, isInlineElement, isPartialFile, isVoidElement, locationFromOffset, locationsEqual, positionFromOffset, ruleDocumentationUrl, rules, splitByTopLevelComma };
7588
+ export { ABSTRACT_ARIA_ROLES, ARIA_ATTRIBUTES, ActionViewNoSilentHelperRule, ActionViewNoSilentRenderRule, AttributeVisitorMixin, BaseLexerRuleVisitor, BaseRuleVisitor, BaseSourceRuleVisitor, ControlFlowTrackingVisitor, ControlFlowType, DEFAULT_LINTER_PARSER_OPTIONS, DEFAULT_LINT_CONTEXT, DEFAULT_RULE_CONFIG, DOCUMENT_ONLY_TAG_NAMES, ERBCommentSyntax, ERBNoCaseNodeChildrenRule, ERBNoConditionalOpenTagRule, ERBNoDuplicateBranchElementsRule, ERBNoEmptyControlFlowRule, ERBNoEmptyTagsRule, ERBNoExtraNewLineRule, ERBNoExtraWhitespaceRule, ERBNoInlineCaseConditionsRule, ERBNoInstanceVariablesInPartialsRule, ERBNoJavascriptTagHelperRule, ERBNoOutputControlFlowRule, ERBNoOutputInAttributeNameRule, ERBNoOutputInAttributePositionRule, ERBNoRawOutputInAttributeValueRule, ERBNoSilentStatementRule, ERBNoSilentTagInAttributeNameRule, ERBNoStatementInScriptRule, ERBNoThenInControlFlowRule, ERBNoTrailingWhitespaceRule, ERBNoUnsafeJSAttributeRule, ERBNoUnsafeRawRule, ERBNoUnsafeScriptInterpolationRule, ERBPreferImageTagHelperRule, ERBRequireTrailingNewlineRule, ERBRequireWhitespaceRule, ERBRightTrimRule, ERBStrictLocalsCommentSyntaxRule, ERBStrictLocalsRequiredRule, HEADING_TAGS, HEAD_AND_BODY_TAG_NAMES, HEAD_ONLY_TAG_NAMES, HTMLAllowedScriptTypeRule, HTMLAnchorRequireHrefRule, HTMLAriaLabelIsWellFormattedRule, HTMLAriaLevelMustBeValidRule, HTMLAriaRoleHeadingRequiresLevelRule, HTMLAriaRoleMustBeValidRule, HTMLAttributeDoubleQuotesRule, HTMLAttributeEqualsSpacingRule, HTMLAttributeValuesRequireQuotesRule, HTMLAvoidBothDisabledAndAriaDisabledRule, HTMLBodyOnlyElementsRule, HTMLBooleanAttributesNoValueRule, HTMLDetailsHasSummaryRule, HTMLHeadOnlyElementsRule, HTMLIframeHasTitleRule, HTMLImgRequireAltRule, HTMLInputRequireAutocompleteRule, HTMLNavigationHasLabelRule, HTMLNoAbstractRolesRule, HTMLNoAriaHiddenOnBodyRule, HTMLNoAriaHiddenOnFocusableRule, HTMLNoBlockInsideInlineRule, HTMLNoDuplicateAttributesRule, HTMLNoDuplicateIdsRule, HTMLNoDuplicateMetaNamesRule, HTMLNoEmptyAttributesRule, HTMLNoEmptyHeadingsRule, HTMLNoNestedLinksRule, HTMLNoPositiveTabIndexRule, HTMLNoSelfClosingRule, HTMLNoSpaceInTagRule, HTMLNoTitleAttributeRule, HTMLNoUnderscoresInAttributeNamesRule, HTMLRequireClosingTagsRule, HTMLTagNameLowercaseRule, HTML_BLOCK_ELEMENTS, 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, findNodeAtPosition, findNodeByLocation, findParent, getBasename, hasBalancedParentheses, isBlockElement, isBodyOnlyTag, isBodyTag, isDocumentOnlyTag, isHeadAndBodyTag, isHeadOnlyTag, isHeadTag, isHtmlOnlyTag, isInlineElement, isPartialFile, isVoidElement, locationFromOffset, locationsEqual, positionFromOffset, ruleDocumentationUrl, rules, splitByTopLevelComma };
7211
7589
  //# sourceMappingURL=index.js.map