@herb-tools/linter 0.9.0 → 0.9.1

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 +1512 -85
  3. package/dist/herb-lint.js.map +1 -1
  4. package/dist/index.cjs +538 -72
  5. package/dist/index.cjs.map +1 -1
  6. package/dist/index.js +465 -74
  7. package/dist/index.js.map +1 -1
  8. package/dist/lint-worker.js +1510 -83
  9. package/dist/lint-worker.js.map +1 -1
  10. package/dist/loader.cjs +1065 -81
  11. package/dist/loader.cjs.map +1 -1
  12. package/dist/loader.js +1044 -82
  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 +13 -10
  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 +7 -6
  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 +14 -10
  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, getStaticAttributeName, isERBControlFlowNode, HTMLCloseTagNode, getTagName, createWhitespaceNode, filterWhitespaceNodes, getStaticContentFromNodes, getOpenTag, isHTMLCloseTagNode, HTMLOpenTagNode, DEFAULT_PARSER_OPTIONS } from '@herb-tools/core';
3
+ export { findAttributeByName, getAttribute, getAttributeName, getAttributeValue, getAttributeValueNodes, getAttributeValueQuoteType, getAttributes, getCombinedAttributeNameString, getStaticAttributeValue, getStaticAttributeValueContent, getTagName, hasAttribute, hasAttributeValue, hasDynamicAttributeName, hasDynamicAttributeValue, hasStaticAttributeValue, hasStaticAttributeValueContent, isAttributeValueQuoted } 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
  /**
@@ -1376,7 +1387,7 @@ class AttributeVisitorMixin extends BaseRuleVisitor {
1376
1387
  forEachAttribute(node, (attributeNode) => {
1377
1388
  const staticAttributeName = getAttributeName(attributeNode);
1378
1389
  const originalAttributeName = getAttributeName(attributeNode, false) || "";
1379
- const isDynamicName = hasDynamicAttributeNameOnAttribute(attributeNode);
1390
+ const isDynamicName = hasDynamicAttributeName(attributeNode);
1380
1391
  const staticAttributeValue = getStaticAttributeValue(attributeNode);
1381
1392
  const valueNodes = getAttributeValueNodes(attributeNode);
1382
1393
  const hasOutputERB = hasERBOutput(valueNodes);
@@ -1453,7 +1464,7 @@ class BaseLexerRuleVisitor {
1453
1464
  * Helper method to create an unbound lint offense (without severity).
1454
1465
  * The Linter will bind severity based on the rule's config.
1455
1466
  */
1456
- createOffense(message, location, autofixContext, severity) {
1467
+ createOffense(message, location, autofixContext, severity, tags) {
1457
1468
  return {
1458
1469
  rule: this.ruleName,
1459
1470
  code: this.ruleName,
@@ -1462,13 +1473,14 @@ class BaseLexerRuleVisitor {
1462
1473
  location,
1463
1474
  autofixContext,
1464
1475
  severity,
1476
+ tags,
1465
1477
  };
1466
1478
  }
1467
1479
  /**
1468
1480
  * Helper method to add an offense to the offenses array
1469
1481
  */
1470
- addOffense(message, location, autofixContext, severity) {
1471
- this.offenses.push(this.createOffense(message, location, autofixContext, severity));
1482
+ addOffense(message, location, autofixContext, severity, tags) {
1483
+ this.offenses.push(this.createOffense(message, location, autofixContext, severity, tags));
1472
1484
  }
1473
1485
  /**
1474
1486
  * Main entry point for lexer rule visitors
@@ -1509,7 +1521,7 @@ class BaseSourceRuleVisitor {
1509
1521
  * Helper method to create an unbound lint offense (without severity).
1510
1522
  * The Linter will bind severity based on the rule's config.
1511
1523
  */
1512
- createOffense(message, location, autofixContext, severity) {
1524
+ createOffense(message, location, autofixContext, severity, tags) {
1513
1525
  return {
1514
1526
  rule: this.ruleName,
1515
1527
  code: this.ruleName,
@@ -1518,13 +1530,14 @@ class BaseSourceRuleVisitor {
1518
1530
  location,
1519
1531
  autofixContext,
1520
1532
  severity,
1533
+ tags,
1521
1534
  };
1522
1535
  }
1523
1536
  /**
1524
1537
  * Helper method to add an offense to the offenses array
1525
1538
  */
1526
- addOffense(message, location, autofixContext, severity) {
1527
- this.offenses.push(this.createOffense(message, location, autofixContext, severity));
1539
+ addOffense(message, location, autofixContext, severity, tags) {
1540
+ this.offenses.push(this.createOffense(message, location, autofixContext, severity, tags));
1528
1541
  }
1529
1542
  /**
1530
1543
  * Main entry point for source rule visitors
@@ -1874,6 +1887,34 @@ class ActionViewNoSilentHelperRule extends ParserRule {
1874
1887
  }
1875
1888
  }
1876
1889
 
1890
+ class ActionViewNoSilentRenderVisitor extends BaseRuleVisitor {
1891
+ visitERBRenderNode(node) {
1892
+ if (!isERBOutputNode(node)) {
1893
+ this.addOffense(`Avoid using \`${node.tag_opening?.value} %>\` with \`render\`. Use \`<%= %>\` to ensure the rendered content is output.`, node.location);
1894
+ }
1895
+ this.visitChildNodes(node);
1896
+ }
1897
+ }
1898
+ class ActionViewNoSilentRenderRule extends ParserRule {
1899
+ static ruleName = "actionview-no-silent-render";
1900
+ get defaultConfig() {
1901
+ return {
1902
+ enabled: true,
1903
+ severity: "error"
1904
+ };
1905
+ }
1906
+ get parserOptions() {
1907
+ return {
1908
+ render_nodes: true,
1909
+ };
1910
+ }
1911
+ check(result, context) {
1912
+ const visitor = new ActionViewNoSilentRenderVisitor(this.ruleName, context);
1913
+ visitor.visit(result.value);
1914
+ return visitor.offenses;
1915
+ }
1916
+ }
1917
+
1877
1918
  class ERBCommentSyntaxVisitor extends BaseRuleVisitor {
1878
1919
  visitERBContentNode(node) {
1879
1920
  const content = node.content?.value || "";
@@ -1938,7 +1979,9 @@ class ERBNoCaseNodeChildrenVisitor extends BaseRuleVisitor {
1938
1979
  for (const child of node.children) {
1939
1980
  if (!this.isAllowedContent(child)) {
1940
1981
  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);
1982
+ 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);
1983
+ offense.tags = ["unnecessary"];
1984
+ this.offenses.push(offense);
1942
1985
  }
1943
1986
  }
1944
1987
  }
@@ -1968,6 +2011,193 @@ class ERBNoCaseNodeChildrenRule extends ParserRule {
1968
2011
  }
1969
2012
  }
1970
2013
 
2014
+ class ERBNoEmptyControlFlowVisitor extends BaseRuleVisitor {
2015
+ processedIfNodes = new Set();
2016
+ processedElseNodes = new Set();
2017
+ visitERBIfNode(node) {
2018
+ if (this.processedIfNodes.has(node)) {
2019
+ return;
2020
+ }
2021
+ this.markIfChainAsProcessed(node);
2022
+ this.markElseNodesInIfChain(node);
2023
+ const entireChainEmpty = this.isEntireIfChainEmpty(node);
2024
+ if (entireChainEmpty) {
2025
+ this.addEmptyBlockOffense(node, node.statements, "if");
2026
+ }
2027
+ else {
2028
+ this.checkIfChainParts(node);
2029
+ }
2030
+ this.visitChildNodes(node);
2031
+ }
2032
+ visitERBElseNode(node) {
2033
+ if (this.processedElseNodes.has(node)) {
2034
+ this.visitChildNodes(node);
2035
+ return;
2036
+ }
2037
+ this.addEmptyBlockOffense(node, node.statements, "else");
2038
+ this.visitChildNodes(node);
2039
+ }
2040
+ visitERBUnlessNode(node) {
2041
+ const unlessHasContent = this.statementsHaveContent(node.statements);
2042
+ const elseHasContent = node.else_clause && this.statementsHaveContent(node.else_clause.statements);
2043
+ if (node.else_clause) {
2044
+ this.processedElseNodes.add(node.else_clause);
2045
+ }
2046
+ const entireBlockEmpty = !unlessHasContent && !elseHasContent;
2047
+ if (entireBlockEmpty) {
2048
+ this.addEmptyBlockOffense(node, node.statements, "unless");
2049
+ }
2050
+ else {
2051
+ if (!unlessHasContent) {
2052
+ this.addEmptyBlockOffenseWithEnd(node, node.statements, "unless", node.else_clause);
2053
+ }
2054
+ if (node.else_clause && !elseHasContent) {
2055
+ this.addEmptyBlockOffense(node.else_clause, node.else_clause.statements, "else");
2056
+ }
2057
+ }
2058
+ this.visitChildNodes(node);
2059
+ }
2060
+ visitERBForNode(node) {
2061
+ this.addEmptyBlockOffense(node, node.statements, "for");
2062
+ this.visitChildNodes(node);
2063
+ }
2064
+ visitERBWhileNode(node) {
2065
+ this.addEmptyBlockOffense(node, node.statements, "while");
2066
+ this.visitChildNodes(node);
2067
+ }
2068
+ visitERBUntilNode(node) {
2069
+ this.addEmptyBlockOffense(node, node.statements, "until");
2070
+ this.visitChildNodes(node);
2071
+ }
2072
+ visitERBWhenNode(node) {
2073
+ if (!node.then_keyword) {
2074
+ this.addEmptyBlockOffense(node, node.statements, "when");
2075
+ }
2076
+ this.visitChildNodes(node);
2077
+ }
2078
+ visitERBInNode(node) {
2079
+ if (!node.then_keyword) {
2080
+ this.addEmptyBlockOffense(node, node.statements, "in");
2081
+ }
2082
+ this.visitChildNodes(node);
2083
+ }
2084
+ visitERBBeginNode(node) {
2085
+ this.addEmptyBlockOffense(node, node.statements, "begin");
2086
+ this.visitChildNodes(node);
2087
+ }
2088
+ visitERBRescueNode(node) {
2089
+ this.addEmptyBlockOffense(node, node.statements, "rescue");
2090
+ this.visitChildNodes(node);
2091
+ }
2092
+ visitERBEnsureNode(node) {
2093
+ this.addEmptyBlockOffense(node, node.statements, "ensure");
2094
+ this.visitChildNodes(node);
2095
+ }
2096
+ visitERBBlockNode(node) {
2097
+ this.addEmptyBlockOffense(node, node.body, "do");
2098
+ this.visitChildNodes(node);
2099
+ }
2100
+ addEmptyBlockOffense(node, statements, blockType) {
2101
+ this.addEmptyBlockOffenseWithEnd(node, statements, blockType, null);
2102
+ }
2103
+ addEmptyBlockOffenseWithEnd(node, statements, blockType, subsequentNode) {
2104
+ if (this.statementsHaveContent(statements)) {
2105
+ return;
2106
+ }
2107
+ const startLocation = node.location.start;
2108
+ const endLocation = subsequentNode
2109
+ ? subsequentNode.location.start
2110
+ : node.location.end;
2111
+ const location = Location.from(startLocation.line, startLocation.column, endLocation.line, endLocation.column);
2112
+ const offense = this.createOffense(`Empty ${blockType} block: this control flow statement has no content`, location);
2113
+ offense.tags = ["unnecessary"];
2114
+ this.offenses.push(offense);
2115
+ }
2116
+ statementsHaveContent(statements) {
2117
+ return statements.some(statement => {
2118
+ if (isHTMLTextNode(statement)) {
2119
+ return statement.content.trim() !== "";
2120
+ }
2121
+ return true;
2122
+ });
2123
+ }
2124
+ markIfChainAsProcessed(node) {
2125
+ this.processedIfNodes.add(node);
2126
+ this.traverseSubsequentNodes(node.subsequent, (current) => {
2127
+ if (isERBIfNode(current)) {
2128
+ this.processedIfNodes.add(current);
2129
+ }
2130
+ });
2131
+ }
2132
+ markElseNodesInIfChain(node) {
2133
+ this.traverseSubsequentNodes(node.subsequent, (current) => {
2134
+ if (isERBElseNode(current)) {
2135
+ this.processedElseNodes.add(current);
2136
+ }
2137
+ });
2138
+ }
2139
+ traverseSubsequentNodes(startNode, callback) {
2140
+ let current = startNode;
2141
+ while (current) {
2142
+ if (isERBIfNode(current)) {
2143
+ callback(current);
2144
+ current = current.subsequent;
2145
+ }
2146
+ else if (isERBElseNode(current)) {
2147
+ callback(current);
2148
+ break;
2149
+ }
2150
+ else {
2151
+ break;
2152
+ }
2153
+ }
2154
+ }
2155
+ checkIfChainParts(node) {
2156
+ if (!this.statementsHaveContent(node.statements)) {
2157
+ this.addEmptyBlockOffenseWithEnd(node, node.statements, "if", node.subsequent);
2158
+ }
2159
+ this.traverseSubsequentNodes(node.subsequent, (current) => {
2160
+ if (this.statementsHaveContent(current.statements)) {
2161
+ return;
2162
+ }
2163
+ const blockType = isERBIfNode(current) ? "elsif" : "else";
2164
+ const nextSubsequent = isERBIfNode(current) ? current.subsequent : null;
2165
+ if (nextSubsequent) {
2166
+ this.addEmptyBlockOffenseWithEnd(current, current.statements, blockType, nextSubsequent);
2167
+ }
2168
+ else {
2169
+ this.addEmptyBlockOffense(current, current.statements, blockType);
2170
+ }
2171
+ });
2172
+ }
2173
+ isEntireIfChainEmpty(node) {
2174
+ if (this.statementsHaveContent(node.statements)) {
2175
+ return false;
2176
+ }
2177
+ let hasContent = false;
2178
+ this.traverseSubsequentNodes(node.subsequent, (current) => {
2179
+ if (this.statementsHaveContent(current.statements)) {
2180
+ hasContent = true;
2181
+ }
2182
+ });
2183
+ return !hasContent;
2184
+ }
2185
+ }
2186
+ class ERBNoEmptyControlFlowRule extends ParserRule {
2187
+ static ruleName = "erb-no-empty-control-flow";
2188
+ get defaultConfig() {
2189
+ return {
2190
+ enabled: true,
2191
+ severity: "hint"
2192
+ };
2193
+ }
2194
+ check(result, context) {
2195
+ const visitor = new ERBNoEmptyControlFlowVisitor(this.ruleName, context);
2196
+ visitor.visit(result.value);
2197
+ return visitor.offenses;
2198
+ }
2199
+ }
2200
+
1971
2201
  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
2202
  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
2203
  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 +2356,15 @@ class ERBNoConditionalOpenTagRule extends ParserRule {
2126
2356
  function getSignificantNodes(statements) {
2127
2357
  return statements.filter(node => !isPureWhitespaceNode(node));
2128
2358
  }
2359
+ function trimWhitespaceNodes(nodes) {
2360
+ let start = 0;
2361
+ let end = nodes.length;
2362
+ while (start < end && isPureWhitespaceNode(nodes[start]))
2363
+ start++;
2364
+ while (end > start && isPureWhitespaceNode(nodes[end - 1]))
2365
+ end--;
2366
+ return nodes.slice(start, end);
2367
+ }
2129
2368
  function allEquivalentElements(nodes) {
2130
2369
  if (nodes.length < 2)
2131
2370
  return false;
@@ -2243,9 +2482,19 @@ class ERBNoDuplicateBranchElementsVisitor extends BaseRuleVisitor {
2243
2482
  if (isERBIfNode(node)) {
2244
2483
  this.markSubsequentIfNodesAsProcessed(node);
2245
2484
  }
2485
+ if (this.allBranchesIdentical(branches)) {
2486
+ this.addOffense("All branches of this conditional have identical content. The conditional can be removed.", node.location, { node: node, allIdentical: true }, "warning");
2487
+ return;
2488
+ }
2246
2489
  const state = { isFirstOffense: true };
2247
2490
  this.checkBranches(branches, node, state);
2248
2491
  }
2492
+ allBranchesIdentical(branches) {
2493
+ if (branches.length < 2)
2494
+ return false;
2495
+ const first = branches[0].map(node => IdentityPrinter.print(node)).join("");
2496
+ return branches.slice(1).every(branch => branch.map(node => IdentityPrinter.print(node)).join("") === first);
2497
+ }
2249
2498
  markSubsequentIfNodesAsProcessed(node) {
2250
2499
  let current = node.subsequent;
2251
2500
  while (current) {
@@ -2279,11 +2528,23 @@ class ERBNoDuplicateBranchElementsVisitor extends BaseRuleVisitor {
2279
2528
  const bodiesMatch = elements.every(element => IdentityPrinter.print(element) === IdentityPrinter.print(elements[0]));
2280
2529
  for (const element of elements) {
2281
2530
  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;
2531
+ if (bodiesMatch) {
2532
+ const autofixContext = state.isFirstOffense
2533
+ ? { node: conditionalNode }
2534
+ : undefined;
2535
+ this.addOffense(`The \`${printed}\` element is duplicated across all branches of this conditional and can be moved outside.`, element.location, autofixContext);
2536
+ state.isFirstOffense = false;
2537
+ }
2538
+ else {
2539
+ const autofixContext = state.isFirstOffense
2540
+ ? { node: conditionalNode }
2541
+ : undefined;
2542
+ const tagNameLocation = isHTMLOpenTagNode(element.open_tag) && element.open_tag.tag_name?.location
2543
+ ? element.open_tag.tag_name.location
2544
+ : element?.open_tag?.location || element.location;
2545
+ this.addOffense(`The \`${printed}\` tag is repeated across all branches with different content. Consider extracting the shared tag outside the conditional.`, tagNameLocation, autofixContext, "hint");
2546
+ state.isFirstOffense = false;
2547
+ }
2287
2548
  }
2288
2549
  if (!bodiesMatch && bodies.every(body => body.length > 0)) {
2289
2550
  this.checkBranches(bodies, conditionalNode, state);
@@ -2312,6 +2573,15 @@ class ERBNoDuplicateBranchElementsRule extends ParserRule {
2312
2573
  const branches = collectBranches(conditionalNode);
2313
2574
  if (!branches)
2314
2575
  return null;
2576
+ if (offense.autofixContext.allIdentical) {
2577
+ const parentInfo = findParentArray(result.value, conditionalNode);
2578
+ if (!parentInfo)
2579
+ return null;
2580
+ const { array: parentArray, index: conditionalIndex } = parentInfo;
2581
+ const firstBranchContent = trimWhitespaceNodes(branches[0]);
2582
+ parentArray.splice(conditionalIndex, 1, ...firstBranchContent);
2583
+ return result;
2584
+ }
2315
2585
  const significantBranches = branches.map(getSignificantNodes);
2316
2586
  if (significantBranches.some(branch => branch.length === 0))
2317
2587
  return null;
@@ -2325,23 +2595,51 @@ class ERBNoDuplicateBranchElementsRule extends ParserRule {
2325
2595
  return null;
2326
2596
  let { array: parentArray, index: conditionalIndex } = parentInfo;
2327
2597
  let hasWrapped = false;
2598
+ let didMutate = false;
2599
+ let failedToHoistPrefix = false;
2600
+ let hoistedBefore = false;
2328
2601
  const hoistElement = (elements, position) => {
2602
+ const actualPosition = (position === "before" && failedToHoistPrefix) ? "after" : position;
2329
2603
  const bodiesMatch = elements.every(element => IdentityPrinter.print(element) === IdentityPrinter.print(elements[0]));
2330
2604
  if (bodiesMatch) {
2605
+ if (actualPosition === "after") {
2606
+ const currentLengths = branches.map(b => getSignificantNodes(b).length);
2607
+ if (currentLengths.some(l => l !== currentLengths[0]))
2608
+ return;
2609
+ }
2610
+ if (actualPosition === "after" && position === "before") {
2611
+ const isAtEnd = branches.every((branch, index) => {
2612
+ const nodes = getSignificantNodes(branch);
2613
+ return nodes.length > 0 && nodes[nodes.length - 1] === elements[index];
2614
+ });
2615
+ if (!isAtEnd)
2616
+ return;
2617
+ }
2331
2618
  for (let i = 0; i < branches.length; i++) {
2332
2619
  removeNodeFromArray(branches[i], elements[i]);
2333
2620
  }
2334
- if (position === "before") {
2335
- parentArray.splice(conditionalIndex, 0, elements[0]);
2336
- conditionalIndex++;
2621
+ if (actualPosition === "before") {
2622
+ parentArray.splice(conditionalIndex, 0, elements[0], createLiteral("\n"));
2623
+ conditionalIndex += 2;
2624
+ hoistedBefore = true;
2337
2625
  }
2338
2626
  else {
2339
- parentArray.splice(conditionalIndex + 1, 0, elements[0]);
2627
+ parentArray.splice(conditionalIndex + 1, 0, createLiteral("\n"), elements[0]);
2340
2628
  }
2629
+ didMutate = true;
2341
2630
  }
2342
2631
  else {
2343
2632
  if (hasWrapped)
2344
2633
  return;
2634
+ const canWrap = branches.every((branch, index) => {
2635
+ const remaining = getSignificantNodes(branch);
2636
+ return remaining.length === 1 && remaining[0] === elements[index];
2637
+ });
2638
+ if (!canWrap) {
2639
+ if (position === "before")
2640
+ failedToHoistPrefix = true;
2641
+ return;
2642
+ }
2345
2643
  for (let i = 0; i < branches.length; i++) {
2346
2644
  replaceNodeWithBody(branches[i], elements[i]);
2347
2645
  }
@@ -2350,6 +2648,7 @@ class ERBNoDuplicateBranchElementsRule extends ParserRule {
2350
2648
  parentArray = wrapper.body;
2351
2649
  conditionalIndex = 1;
2352
2650
  hasWrapped = true;
2651
+ didMutate = true;
2353
2652
  }
2354
2653
  };
2355
2654
  for (let index = 0; index < prefixCount; index++) {
@@ -2360,7 +2659,22 @@ class ERBNoDuplicateBranchElementsRule extends ParserRule {
2360
2659
  const elements = significantBranches.map(branch => branch[branch.length - 1 - offset]);
2361
2660
  hoistElement(elements, "after");
2362
2661
  }
2363
- return result;
2662
+ if (!hasWrapped && hoistedBefore) {
2663
+ const remaining = branches.map(branch => getSignificantNodes(branch));
2664
+ if (remaining.every(branch => branch.length === 1) && allEquivalentElements(remaining.map(b => b[0]))) {
2665
+ const elements = remaining.map(b => b[0]);
2666
+ const bodiesMatch = elements.every(el => IdentityPrinter.print(el) === IdentityPrinter.print(elements[0]));
2667
+ if (!bodiesMatch && elements.every(el => el.body.length > 0)) {
2668
+ for (let i = 0; i < branches.length; i++) {
2669
+ replaceNodeWithBody(branches[i], elements[i]);
2670
+ }
2671
+ const wrapper = createWrapper(elements[0], [createLiteral("\n"), conditionalNode, createLiteral("\n")]);
2672
+ parentArray[conditionalIndex] = wrapper;
2673
+ didMutate = true;
2674
+ }
2675
+ }
2676
+ }
2677
+ return didMutate ? result : null;
2364
2678
  }
2365
2679
  }
2366
2680
 
@@ -2886,6 +3200,47 @@ class ERBNoRawOutputInAttributeValueRule extends ParserRule {
2886
3200
  }
2887
3201
  }
2888
3202
 
3203
+ function isAssignmentNode(prismNode) {
3204
+ const type = prismNode?.constructor?.name;
3205
+ if (!type)
3206
+ return false;
3207
+ return type.endsWith("WriteNode");
3208
+ }
3209
+ class ERBNoSilentStatementVisitor extends BaseRuleVisitor {
3210
+ visitERBContentNode(node) {
3211
+ if (isERBOutputNode(node))
3212
+ return;
3213
+ const prismNode = node.prismNode;
3214
+ if (!prismNode)
3215
+ return;
3216
+ if (isAssignmentNode(prismNode))
3217
+ return;
3218
+ const content = node.content?.value?.trim();
3219
+ if (!content)
3220
+ return;
3221
+ this.addOffense(`Avoid using silent ERB tags for statements. Move \`${content}\` to a controller, helper, or presenter.`, node.location);
3222
+ }
3223
+ }
3224
+ class ERBNoSilentStatementRule extends ParserRule {
3225
+ static ruleName = "erb-no-silent-statement";
3226
+ get defaultConfig() {
3227
+ return {
3228
+ enabled: false,
3229
+ severity: "warning"
3230
+ };
3231
+ }
3232
+ get parserOptions() {
3233
+ return {
3234
+ prism_nodes: true,
3235
+ };
3236
+ }
3237
+ check(result, context) {
3238
+ const visitor = new ERBNoSilentStatementVisitor(this.ruleName, context);
3239
+ visitor.visit(result.value);
3240
+ return visitor.offenses;
3241
+ }
3242
+ }
3243
+
2889
3244
  class ERBNoSilentTagInAttributeNameVisitor extends BaseRuleVisitor {
2890
3245
  visitHTMLAttributeNameNode(node) {
2891
3246
  const erbNodes = filterERBContentNodes(node.children);
@@ -3148,7 +3503,7 @@ class ERBNoTrailingWhitespaceRule extends ParserRule {
3148
3503
  }
3149
3504
 
3150
3505
  const JS_ATTRIBUTE_PATTERN = /^on/i;
3151
- const SAFE_PATTERN$1 = /\.to_json\s*$|\bj\s*[\s(]|\bescape_javascript\s*[\s(]/;
3506
+ const SAFE_PATTERN = /\.to_json\s*$|\bj\s*[\s(]|\bescape_javascript\s*[\s(]/;
3152
3507
  class ERBNoUnsafeJSAttributeVisitor extends AttributeVisitorMixin {
3153
3508
  checkStaticAttributeDynamicValue({ attributeName, valueNodes }) {
3154
3509
  if (!JS_ATTRIBUTE_PATTERN.test(attributeName))
@@ -3159,7 +3514,7 @@ class ERBNoUnsafeJSAttributeVisitor extends AttributeVisitorMixin {
3159
3514
  if (!isERBOutputNode(node))
3160
3515
  continue;
3161
3516
  const content = node.content?.value?.trim() || "";
3162
- if (SAFE_PATTERN$1.test(content))
3517
+ if (SAFE_PATTERN.test(content))
3163
3518
  continue;
3164
3519
  this.addOffense(`Unsafe ERB output in \`${attributeName}\` attribute. Use \`.to_json\`, \`j()\`, or \`escape_javascript()\` to safely encode values.`, node.location);
3165
3520
  }
@@ -3240,7 +3595,27 @@ class ERBNoUnsafeRawRule extends ParserRule {
3240
3595
  }
3241
3596
  }
3242
3597
 
3243
- const SAFE_PATTERN = /\.to_json\b/;
3598
+ const SAFE_METHOD_NAMES = new Set([
3599
+ "to_json",
3600
+ "json_escape",
3601
+ ]);
3602
+ const ESCAPE_JAVASCRIPT_METHOD_NAMES = new Set([
3603
+ "j",
3604
+ "escape_javascript",
3605
+ ]);
3606
+ class SafeCallDetector extends PrismVisitor {
3607
+ hasSafeCall = false;
3608
+ hasEscapeJavascriptCall = false;
3609
+ visitCallNode(node) {
3610
+ if (SAFE_METHOD_NAMES.has(node.name)) {
3611
+ this.hasSafeCall = true;
3612
+ }
3613
+ if (ESCAPE_JAVASCRIPT_METHOD_NAMES.has(node.name)) {
3614
+ this.hasEscapeJavascriptCall = true;
3615
+ }
3616
+ this.visitChildNodes(node);
3617
+ }
3618
+ }
3244
3619
  class ERBNoUnsafeScriptInterpolationVisitor extends BaseRuleVisitor {
3245
3620
  visitHTMLElementNode(node) {
3246
3621
  if (!isHTMLOpenTagNode(node.open_tag)) {
@@ -3269,9 +3644,17 @@ class ERBNoUnsafeScriptInterpolationVisitor extends BaseRuleVisitor {
3269
3644
  continue;
3270
3645
  if (!isERBOutputNode(child))
3271
3646
  continue;
3272
- const content = child.content?.value?.trim() || "";
3273
- if (SAFE_PATTERN.test(content))
3647
+ const erbContent = child;
3648
+ const prismNode = erbContent.prismNode;
3649
+ const detector = new SafeCallDetector();
3650
+ if (prismNode)
3651
+ detector.visit(prismNode);
3652
+ if (detector.hasSafeCall)
3274
3653
  continue;
3654
+ if (detector.hasEscapeJavascriptCall) {
3655
+ 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);
3656
+ continue;
3657
+ }
3275
3658
  this.addOffense("Unsafe ERB output in `<script>` tag. Use `.to_json` to safely serialize values into JavaScript.", child.location);
3276
3659
  }
3277
3660
  }
@@ -3284,6 +3667,11 @@ class ERBNoUnsafeScriptInterpolationRule extends ParserRule {
3284
3667
  severity: "error"
3285
3668
  };
3286
3669
  }
3670
+ get parserOptions() {
3671
+ return {
3672
+ prism_nodes: true,
3673
+ };
3674
+ }
3287
3675
  check(result, context) {
3288
3676
  const visitor = new ERBNoUnsafeScriptInterpolationVisitor(this.ruleName, context);
3289
3677
  visitor.visit(result.value);
@@ -4291,7 +4679,7 @@ class HerbDisableCommentValidRuleNameRule extends ParserRule {
4291
4679
  }
4292
4680
  }
4293
4681
 
4294
- const ALLOWED_TYPES = ["text/javascript"];
4682
+ const ALLOWED_TYPES = ["text/javascript", "module", "importmap", "speculationrules"];
4295
4683
  class AllowedScriptTypeVisitor extends BaseRuleVisitor {
4296
4684
  visitHTMLOpenTagNode(node) {
4297
4685
  if (getTagLocalName(node) === "script") {
@@ -4829,6 +5217,47 @@ class HTMLBodyOnlyElementsRule extends ParserRule {
4829
5217
  }
4830
5218
  }
4831
5219
 
5220
+ class BooleanAttributesNoValueVisitor extends AttributeVisitorMixin {
5221
+ checkStaticAttributeStaticValue({ originalAttributeName, attributeNode }) {
5222
+ this.checkAttribute(originalAttributeName, attributeNode);
5223
+ }
5224
+ checkStaticAttributeDynamicValue({ originalAttributeName, attributeNode }) {
5225
+ this.checkAttribute(originalAttributeName, attributeNode);
5226
+ }
5227
+ checkAttribute(attributeName, attributeNode) {
5228
+ if (!isBooleanAttribute(attributeName))
5229
+ return;
5230
+ if (!hasAttributeValue(attributeNode))
5231
+ return;
5232
+ this.addOffense(`Boolean attribute \`${IdentityPrinter.print(attributeNode.name)}\` should not have a value. Use \`${attributeName.toLowerCase()}\` instead of \`${IdentityPrinter.print(attributeNode)}\`.`, attributeNode.value.location, {
5233
+ node: attributeNode
5234
+ });
5235
+ }
5236
+ }
5237
+ class HTMLBooleanAttributesNoValueRule extends ParserRule {
5238
+ static autocorrectable = true;
5239
+ static ruleName = "html-boolean-attributes-no-value";
5240
+ get defaultConfig() {
5241
+ return {
5242
+ enabled: true,
5243
+ severity: "error"
5244
+ };
5245
+ }
5246
+ check(result, context) {
5247
+ const visitor = new BooleanAttributesNoValueVisitor(this.ruleName, context);
5248
+ visitor.visit(result.value);
5249
+ return visitor.offenses;
5250
+ }
5251
+ autofix(offense, result, _context) {
5252
+ if (!offense.autofixContext)
5253
+ return null;
5254
+ const { node } = offense.autofixContext;
5255
+ node.equals = null;
5256
+ node.value = null;
5257
+ return result;
5258
+ }
5259
+ }
5260
+
4832
5261
  class DetailsHasSummaryVisitor extends BaseRuleVisitor {
4833
5262
  visitHTMLElementNode(node) {
4834
5263
  this.checkDetailsElement(node);
@@ -4878,47 +5307,6 @@ class HTMLDetailsHasSummaryRule extends ParserRule {
4878
5307
  }
4879
5308
  }
4880
5309
 
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
5310
  class HeadOnlyElementsVisitor extends BaseRuleVisitor {
4923
5311
  elementStack = [];
4924
5312
  visitHTMLElementNode(node) {
@@ -6601,8 +6989,10 @@ class TurboPermanentRequireIdRule extends ParserRule {
6601
6989
 
6602
6990
  const rules = [
6603
6991
  ActionViewNoSilentHelperRule,
6992
+ ActionViewNoSilentRenderRule,
6604
6993
  ERBCommentSyntax,
6605
6994
  ERBNoCaseNodeChildrenRule,
6995
+ ERBNoEmptyControlFlowRule,
6606
6996
  ERBNoConditionalHTMLElementRule,
6607
6997
  ERBNoConditionalOpenTagRule,
6608
6998
  ERBNoDuplicateBranchElementsRule,
@@ -6617,6 +7007,7 @@ const rules = [
6617
7007
  ERBNoOutputInAttributeNameRule,
6618
7008
  ERBNoOutputInAttributePositionRule,
6619
7009
  ERBNoRawOutputInAttributeValueRule,
7010
+ ERBNoSilentStatementRule,
6620
7011
  ERBNoSilentTagInAttributeNameRule,
6621
7012
  ERBNoStatementInScriptRule,
6622
7013
  ERBNoThenInControlFlowRule,
@@ -6648,8 +7039,8 @@ const rules = [
6648
7039
  HTMLAttributeValuesRequireQuotesRule,
6649
7040
  HTMLAvoidBothDisabledAndAriaDisabledRule,
6650
7041
  HTMLBodyOnlyElementsRule,
6651
- HTMLDetailsHasSummaryRule,
6652
7042
  HTMLBooleanAttributesNoValueRule,
7043
+ HTMLDetailsHasSummaryRule,
6653
7044
  HTMLHeadOnlyElementsRule,
6654
7045
  HTMLIframeHasTitleRule,
6655
7046
  HTMLImgRequireAltRule,
@@ -7207,5 +7598,5 @@ function ruleDocumentationUrl(ruleId) {
7207
7598
  return `${DOCS_BASE_URL}/${ruleId}`;
7208
7599
  }
7209
7600
 
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 };
7601
+ 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_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 };
7211
7602
  //# sourceMappingURL=index.js.map