@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.cjs CHANGED
@@ -441,6 +441,12 @@ class IdentityPrinter extends Printer {
441
441
  this.visit(node.end_node);
442
442
  }
443
443
  }
444
+ visitERBRenderNode(node) {
445
+ this.printERBNode(node);
446
+ }
447
+ visitRubyRenderLocalNode(_node) {
448
+ // extracted metadata, nothing to print
449
+ }
444
450
  visitERBYieldNode(node) {
445
451
  this.printERBNode(node);
446
452
  }
@@ -991,7 +997,7 @@ class ParserRule {
991
997
  get parserOptions() {
992
998
  return DEFAULT_LINTER_PARSER_OPTIONS;
993
999
  }
994
- createOffense(message, location, autofixContext, severity) {
1000
+ createOffense(message, location, autofixContext, severity, tags) {
995
1001
  return {
996
1002
  rule: this.ruleName,
997
1003
  code: this.ruleName,
@@ -1000,6 +1006,7 @@ class ParserRule {
1000
1006
  location,
1001
1007
  autofixContext,
1002
1008
  severity,
1009
+ tags,
1003
1010
  };
1004
1011
  }
1005
1012
  }
@@ -1019,7 +1026,7 @@ class LexerRule {
1019
1026
  get defaultConfig() {
1020
1027
  return DEFAULT_RULE_CONFIG;
1021
1028
  }
1022
- createOffense(message, location, autofixContext, severity) {
1029
+ createOffense(message, location, autofixContext, severity, tags) {
1023
1030
  return {
1024
1031
  rule: this.ruleName,
1025
1032
  code: this.ruleName,
@@ -1028,6 +1035,7 @@ class LexerRule {
1028
1035
  location,
1029
1036
  autofixContext,
1030
1037
  severity,
1038
+ tags,
1031
1039
  };
1032
1040
  }
1033
1041
  }
@@ -1053,7 +1061,7 @@ class SourceRule {
1053
1061
  get defaultConfig() {
1054
1062
  return DEFAULT_RULE_CONFIG;
1055
1063
  }
1056
- createOffense(message, location, autofixContext, severity) {
1064
+ createOffense(message, location, autofixContext, severity, tags) {
1057
1065
  return {
1058
1066
  rule: this.ruleName,
1059
1067
  code: this.ruleName,
@@ -1062,6 +1070,7 @@ class SourceRule {
1062
1070
  location,
1063
1071
  autofixContext,
1064
1072
  severity,
1073
+ tags,
1065
1074
  };
1066
1075
  }
1067
1076
  }
@@ -1087,7 +1096,7 @@ class BaseRuleVisitor extends core.Visitor {
1087
1096
  * Helper method to create an unbound lint offense (without severity).
1088
1097
  * The Linter will bind severity based on the rule's config.
1089
1098
  */
1090
- createOffense(message, location, autofixContext, severity) {
1099
+ createOffense(message, location, autofixContext, severity, tags) {
1091
1100
  return {
1092
1101
  rule: this.ruleName,
1093
1102
  code: this.ruleName,
@@ -1096,13 +1105,14 @@ class BaseRuleVisitor extends core.Visitor {
1096
1105
  location,
1097
1106
  autofixContext,
1098
1107
  severity,
1108
+ tags,
1099
1109
  };
1100
1110
  }
1101
1111
  /**
1102
1112
  * Helper method to add an offense to the offenses array
1103
1113
  */
1104
- addOffense(message, location, autofixContext, severity) {
1105
- this.offenses.push(this.createOffense(message, location, autofixContext, severity));
1114
+ addOffense(message, location, autofixContext, severity, tags) {
1115
+ this.offenses.push(this.createOffense(message, location, autofixContext, severity, tags));
1106
1116
  }
1107
1117
  }
1108
1118
  /**
@@ -1378,7 +1388,7 @@ class AttributeVisitorMixin extends BaseRuleVisitor {
1378
1388
  core.forEachAttribute(node, (attributeNode) => {
1379
1389
  const staticAttributeName = core.getAttributeName(attributeNode);
1380
1390
  const originalAttributeName = core.getAttributeName(attributeNode, false) || "";
1381
- const isDynamicName = core.hasDynamicAttributeNameOnAttribute(attributeNode);
1391
+ const isDynamicName = core.hasDynamicAttributeName(attributeNode);
1382
1392
  const staticAttributeValue = core.getStaticAttributeValue(attributeNode);
1383
1393
  const valueNodes = core.getAttributeValueNodes(attributeNode);
1384
1394
  const hasOutputERB = core.hasERBOutput(valueNodes);
@@ -1455,7 +1465,7 @@ class BaseLexerRuleVisitor {
1455
1465
  * Helper method to create an unbound lint offense (without severity).
1456
1466
  * The Linter will bind severity based on the rule's config.
1457
1467
  */
1458
- createOffense(message, location, autofixContext, severity) {
1468
+ createOffense(message, location, autofixContext, severity, tags) {
1459
1469
  return {
1460
1470
  rule: this.ruleName,
1461
1471
  code: this.ruleName,
@@ -1464,13 +1474,14 @@ class BaseLexerRuleVisitor {
1464
1474
  location,
1465
1475
  autofixContext,
1466
1476
  severity,
1477
+ tags,
1467
1478
  };
1468
1479
  }
1469
1480
  /**
1470
1481
  * Helper method to add an offense to the offenses array
1471
1482
  */
1472
- addOffense(message, location, autofixContext, severity) {
1473
- this.offenses.push(this.createOffense(message, location, autofixContext, severity));
1483
+ addOffense(message, location, autofixContext, severity, tags) {
1484
+ this.offenses.push(this.createOffense(message, location, autofixContext, severity, tags));
1474
1485
  }
1475
1486
  /**
1476
1487
  * Main entry point for lexer rule visitors
@@ -1511,7 +1522,7 @@ class BaseSourceRuleVisitor {
1511
1522
  * Helper method to create an unbound lint offense (without severity).
1512
1523
  * The Linter will bind severity based on the rule's config.
1513
1524
  */
1514
- createOffense(message, location, autofixContext, severity) {
1525
+ createOffense(message, location, autofixContext, severity, tags) {
1515
1526
  return {
1516
1527
  rule: this.ruleName,
1517
1528
  code: this.ruleName,
@@ -1520,13 +1531,14 @@ class BaseSourceRuleVisitor {
1520
1531
  location,
1521
1532
  autofixContext,
1522
1533
  severity,
1534
+ tags,
1523
1535
  };
1524
1536
  }
1525
1537
  /**
1526
1538
  * Helper method to add an offense to the offenses array
1527
1539
  */
1528
- addOffense(message, location, autofixContext, severity) {
1529
- this.offenses.push(this.createOffense(message, location, autofixContext, severity));
1540
+ addOffense(message, location, autofixContext, severity, tags) {
1541
+ this.offenses.push(this.createOffense(message, location, autofixContext, severity, tags));
1530
1542
  }
1531
1543
  /**
1532
1544
  * Main entry point for source rule visitors
@@ -1876,6 +1888,34 @@ class ActionViewNoSilentHelperRule extends ParserRule {
1876
1888
  }
1877
1889
  }
1878
1890
 
1891
+ class ActionViewNoSilentRenderVisitor extends BaseRuleVisitor {
1892
+ visitERBRenderNode(node) {
1893
+ if (!core.isERBOutputNode(node)) {
1894
+ this.addOffense(`Avoid using \`${node.tag_opening?.value} %>\` with \`render\`. Use \`<%= %>\` to ensure the rendered content is output.`, node.location);
1895
+ }
1896
+ this.visitChildNodes(node);
1897
+ }
1898
+ }
1899
+ class ActionViewNoSilentRenderRule extends ParserRule {
1900
+ static ruleName = "actionview-no-silent-render";
1901
+ get defaultConfig() {
1902
+ return {
1903
+ enabled: true,
1904
+ severity: "error"
1905
+ };
1906
+ }
1907
+ get parserOptions() {
1908
+ return {
1909
+ render_nodes: true,
1910
+ };
1911
+ }
1912
+ check(result, context) {
1913
+ const visitor = new ActionViewNoSilentRenderVisitor(this.ruleName, context);
1914
+ visitor.visit(result.value);
1915
+ return visitor.offenses;
1916
+ }
1917
+ }
1918
+
1879
1919
  class ERBCommentSyntaxVisitor extends BaseRuleVisitor {
1880
1920
  visitERBContentNode(node) {
1881
1921
  const content = node.content?.value || "";
@@ -1940,7 +1980,9 @@ class ERBNoCaseNodeChildrenVisitor extends BaseRuleVisitor {
1940
1980
  for (const child of node.children) {
1941
1981
  if (!this.isAllowedContent(child)) {
1942
1982
  const childCode = IdentityPrinter.print(child).trim();
1943
- 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);
1983
+ 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);
1984
+ offense.tags = ["unnecessary"];
1985
+ this.offenses.push(offense);
1944
1986
  }
1945
1987
  }
1946
1988
  }
@@ -1970,6 +2012,193 @@ class ERBNoCaseNodeChildrenRule extends ParserRule {
1970
2012
  }
1971
2013
  }
1972
2014
 
2015
+ class ERBNoEmptyControlFlowVisitor extends BaseRuleVisitor {
2016
+ processedIfNodes = new Set();
2017
+ processedElseNodes = new Set();
2018
+ visitERBIfNode(node) {
2019
+ if (this.processedIfNodes.has(node)) {
2020
+ return;
2021
+ }
2022
+ this.markIfChainAsProcessed(node);
2023
+ this.markElseNodesInIfChain(node);
2024
+ const entireChainEmpty = this.isEntireIfChainEmpty(node);
2025
+ if (entireChainEmpty) {
2026
+ this.addEmptyBlockOffense(node, node.statements, "if");
2027
+ }
2028
+ else {
2029
+ this.checkIfChainParts(node);
2030
+ }
2031
+ this.visitChildNodes(node);
2032
+ }
2033
+ visitERBElseNode(node) {
2034
+ if (this.processedElseNodes.has(node)) {
2035
+ this.visitChildNodes(node);
2036
+ return;
2037
+ }
2038
+ this.addEmptyBlockOffense(node, node.statements, "else");
2039
+ this.visitChildNodes(node);
2040
+ }
2041
+ visitERBUnlessNode(node) {
2042
+ const unlessHasContent = this.statementsHaveContent(node.statements);
2043
+ const elseHasContent = node.else_clause && this.statementsHaveContent(node.else_clause.statements);
2044
+ if (node.else_clause) {
2045
+ this.processedElseNodes.add(node.else_clause);
2046
+ }
2047
+ const entireBlockEmpty = !unlessHasContent && !elseHasContent;
2048
+ if (entireBlockEmpty) {
2049
+ this.addEmptyBlockOffense(node, node.statements, "unless");
2050
+ }
2051
+ else {
2052
+ if (!unlessHasContent) {
2053
+ this.addEmptyBlockOffenseWithEnd(node, node.statements, "unless", node.else_clause);
2054
+ }
2055
+ if (node.else_clause && !elseHasContent) {
2056
+ this.addEmptyBlockOffense(node.else_clause, node.else_clause.statements, "else");
2057
+ }
2058
+ }
2059
+ this.visitChildNodes(node);
2060
+ }
2061
+ visitERBForNode(node) {
2062
+ this.addEmptyBlockOffense(node, node.statements, "for");
2063
+ this.visitChildNodes(node);
2064
+ }
2065
+ visitERBWhileNode(node) {
2066
+ this.addEmptyBlockOffense(node, node.statements, "while");
2067
+ this.visitChildNodes(node);
2068
+ }
2069
+ visitERBUntilNode(node) {
2070
+ this.addEmptyBlockOffense(node, node.statements, "until");
2071
+ this.visitChildNodes(node);
2072
+ }
2073
+ visitERBWhenNode(node) {
2074
+ if (!node.then_keyword) {
2075
+ this.addEmptyBlockOffense(node, node.statements, "when");
2076
+ }
2077
+ this.visitChildNodes(node);
2078
+ }
2079
+ visitERBInNode(node) {
2080
+ if (!node.then_keyword) {
2081
+ this.addEmptyBlockOffense(node, node.statements, "in");
2082
+ }
2083
+ this.visitChildNodes(node);
2084
+ }
2085
+ visitERBBeginNode(node) {
2086
+ this.addEmptyBlockOffense(node, node.statements, "begin");
2087
+ this.visitChildNodes(node);
2088
+ }
2089
+ visitERBRescueNode(node) {
2090
+ this.addEmptyBlockOffense(node, node.statements, "rescue");
2091
+ this.visitChildNodes(node);
2092
+ }
2093
+ visitERBEnsureNode(node) {
2094
+ this.addEmptyBlockOffense(node, node.statements, "ensure");
2095
+ this.visitChildNodes(node);
2096
+ }
2097
+ visitERBBlockNode(node) {
2098
+ this.addEmptyBlockOffense(node, node.body, "do");
2099
+ this.visitChildNodes(node);
2100
+ }
2101
+ addEmptyBlockOffense(node, statements, blockType) {
2102
+ this.addEmptyBlockOffenseWithEnd(node, statements, blockType, null);
2103
+ }
2104
+ addEmptyBlockOffenseWithEnd(node, statements, blockType, subsequentNode) {
2105
+ if (this.statementsHaveContent(statements)) {
2106
+ return;
2107
+ }
2108
+ const startLocation = node.location.start;
2109
+ const endLocation = subsequentNode
2110
+ ? subsequentNode.location.start
2111
+ : node.location.end;
2112
+ const location = core.Location.from(startLocation.line, startLocation.column, endLocation.line, endLocation.column);
2113
+ const offense = this.createOffense(`Empty ${blockType} block: this control flow statement has no content`, location);
2114
+ offense.tags = ["unnecessary"];
2115
+ this.offenses.push(offense);
2116
+ }
2117
+ statementsHaveContent(statements) {
2118
+ return statements.some(statement => {
2119
+ if (core.isHTMLTextNode(statement)) {
2120
+ return statement.content.trim() !== "";
2121
+ }
2122
+ return true;
2123
+ });
2124
+ }
2125
+ markIfChainAsProcessed(node) {
2126
+ this.processedIfNodes.add(node);
2127
+ this.traverseSubsequentNodes(node.subsequent, (current) => {
2128
+ if (core.isERBIfNode(current)) {
2129
+ this.processedIfNodes.add(current);
2130
+ }
2131
+ });
2132
+ }
2133
+ markElseNodesInIfChain(node) {
2134
+ this.traverseSubsequentNodes(node.subsequent, (current) => {
2135
+ if (core.isERBElseNode(current)) {
2136
+ this.processedElseNodes.add(current);
2137
+ }
2138
+ });
2139
+ }
2140
+ traverseSubsequentNodes(startNode, callback) {
2141
+ let current = startNode;
2142
+ while (current) {
2143
+ if (core.isERBIfNode(current)) {
2144
+ callback(current);
2145
+ current = current.subsequent;
2146
+ }
2147
+ else if (core.isERBElseNode(current)) {
2148
+ callback(current);
2149
+ break;
2150
+ }
2151
+ else {
2152
+ break;
2153
+ }
2154
+ }
2155
+ }
2156
+ checkIfChainParts(node) {
2157
+ if (!this.statementsHaveContent(node.statements)) {
2158
+ this.addEmptyBlockOffenseWithEnd(node, node.statements, "if", node.subsequent);
2159
+ }
2160
+ this.traverseSubsequentNodes(node.subsequent, (current) => {
2161
+ if (this.statementsHaveContent(current.statements)) {
2162
+ return;
2163
+ }
2164
+ const blockType = core.isERBIfNode(current) ? "elsif" : "else";
2165
+ const nextSubsequent = core.isERBIfNode(current) ? current.subsequent : null;
2166
+ if (nextSubsequent) {
2167
+ this.addEmptyBlockOffenseWithEnd(current, current.statements, blockType, nextSubsequent);
2168
+ }
2169
+ else {
2170
+ this.addEmptyBlockOffense(current, current.statements, blockType);
2171
+ }
2172
+ });
2173
+ }
2174
+ isEntireIfChainEmpty(node) {
2175
+ if (this.statementsHaveContent(node.statements)) {
2176
+ return false;
2177
+ }
2178
+ let hasContent = false;
2179
+ this.traverseSubsequentNodes(node.subsequent, (current) => {
2180
+ if (this.statementsHaveContent(current.statements)) {
2181
+ hasContent = true;
2182
+ }
2183
+ });
2184
+ return !hasContent;
2185
+ }
2186
+ }
2187
+ class ERBNoEmptyControlFlowRule extends ParserRule {
2188
+ static ruleName = "erb-no-empty-control-flow";
2189
+ get defaultConfig() {
2190
+ return {
2191
+ enabled: true,
2192
+ severity: "hint"
2193
+ };
2194
+ }
2195
+ check(result, context) {
2196
+ const visitor = new ERBNoEmptyControlFlowVisitor(this.ruleName, context);
2197
+ visitor.visit(result.value);
2198
+ return visitor.offenses;
2199
+ }
2200
+ }
2201
+
1973
2202
  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; }
1974
2203
  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; }
1975
2204
  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; }
@@ -2128,6 +2357,15 @@ class ERBNoConditionalOpenTagRule extends ParserRule {
2128
2357
  function getSignificantNodes(statements) {
2129
2358
  return statements.filter(node => !core.isPureWhitespaceNode(node));
2130
2359
  }
2360
+ function trimWhitespaceNodes(nodes) {
2361
+ let start = 0;
2362
+ let end = nodes.length;
2363
+ while (start < end && core.isPureWhitespaceNode(nodes[start]))
2364
+ start++;
2365
+ while (end > start && core.isPureWhitespaceNode(nodes[end - 1]))
2366
+ end--;
2367
+ return nodes.slice(start, end);
2368
+ }
2131
2369
  function allEquivalentElements(nodes) {
2132
2370
  if (nodes.length < 2)
2133
2371
  return false;
@@ -2245,9 +2483,19 @@ class ERBNoDuplicateBranchElementsVisitor extends BaseRuleVisitor {
2245
2483
  if (core.isERBIfNode(node)) {
2246
2484
  this.markSubsequentIfNodesAsProcessed(node);
2247
2485
  }
2486
+ if (this.allBranchesIdentical(branches)) {
2487
+ this.addOffense("All branches of this conditional have identical content. The conditional can be removed.", node.location, { node: node, allIdentical: true }, "warning");
2488
+ return;
2489
+ }
2248
2490
  const state = { isFirstOffense: true };
2249
2491
  this.checkBranches(branches, node, state);
2250
2492
  }
2493
+ allBranchesIdentical(branches) {
2494
+ if (branches.length < 2)
2495
+ return false;
2496
+ const first = branches[0].map(node => IdentityPrinter.print(node)).join("");
2497
+ return branches.slice(1).every(branch => branch.map(node => IdentityPrinter.print(node)).join("") === first);
2498
+ }
2251
2499
  markSubsequentIfNodesAsProcessed(node) {
2252
2500
  let current = node.subsequent;
2253
2501
  while (current) {
@@ -2281,11 +2529,23 @@ class ERBNoDuplicateBranchElementsVisitor extends BaseRuleVisitor {
2281
2529
  const bodiesMatch = elements.every(element => IdentityPrinter.print(element) === IdentityPrinter.print(elements[0]));
2282
2530
  for (const element of elements) {
2283
2531
  const printed = IdentityPrinter.print(element.open_tag);
2284
- const autofixContext = state.isFirstOffense
2285
- ? { node: conditionalNode }
2286
- : undefined;
2287
- 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);
2288
- state.isFirstOffense = false;
2532
+ if (bodiesMatch) {
2533
+ const autofixContext = state.isFirstOffense
2534
+ ? { node: conditionalNode }
2535
+ : undefined;
2536
+ this.addOffense(`The \`${printed}\` element is duplicated across all branches of this conditional and can be moved outside.`, element.location, autofixContext);
2537
+ state.isFirstOffense = false;
2538
+ }
2539
+ else {
2540
+ const autofixContext = state.isFirstOffense
2541
+ ? { node: conditionalNode }
2542
+ : undefined;
2543
+ const tagNameLocation = core.isHTMLOpenTagNode(element.open_tag) && element.open_tag.tag_name?.location
2544
+ ? element.open_tag.tag_name.location
2545
+ : element?.open_tag?.location || element.location;
2546
+ this.addOffense(`The \`${printed}\` tag is repeated across all branches with different content. Consider extracting the shared tag outside the conditional.`, tagNameLocation, autofixContext, "hint");
2547
+ state.isFirstOffense = false;
2548
+ }
2289
2549
  }
2290
2550
  if (!bodiesMatch && bodies.every(body => body.length > 0)) {
2291
2551
  this.checkBranches(bodies, conditionalNode, state);
@@ -2314,6 +2574,15 @@ class ERBNoDuplicateBranchElementsRule extends ParserRule {
2314
2574
  const branches = collectBranches(conditionalNode);
2315
2575
  if (!branches)
2316
2576
  return null;
2577
+ if (offense.autofixContext.allIdentical) {
2578
+ const parentInfo = core.findParentArray(result.value, conditionalNode);
2579
+ if (!parentInfo)
2580
+ return null;
2581
+ const { array: parentArray, index: conditionalIndex } = parentInfo;
2582
+ const firstBranchContent = trimWhitespaceNodes(branches[0]);
2583
+ parentArray.splice(conditionalIndex, 1, ...firstBranchContent);
2584
+ return result;
2585
+ }
2317
2586
  const significantBranches = branches.map(getSignificantNodes);
2318
2587
  if (significantBranches.some(branch => branch.length === 0))
2319
2588
  return null;
@@ -2327,23 +2596,51 @@ class ERBNoDuplicateBranchElementsRule extends ParserRule {
2327
2596
  return null;
2328
2597
  let { array: parentArray, index: conditionalIndex } = parentInfo;
2329
2598
  let hasWrapped = false;
2599
+ let didMutate = false;
2600
+ let failedToHoistPrefix = false;
2601
+ let hoistedBefore = false;
2330
2602
  const hoistElement = (elements, position) => {
2603
+ const actualPosition = (position === "before" && failedToHoistPrefix) ? "after" : position;
2331
2604
  const bodiesMatch = elements.every(element => IdentityPrinter.print(element) === IdentityPrinter.print(elements[0]));
2332
2605
  if (bodiesMatch) {
2606
+ if (actualPosition === "after") {
2607
+ const currentLengths = branches.map(b => getSignificantNodes(b).length);
2608
+ if (currentLengths.some(l => l !== currentLengths[0]))
2609
+ return;
2610
+ }
2611
+ if (actualPosition === "after" && position === "before") {
2612
+ const isAtEnd = branches.every((branch, index) => {
2613
+ const nodes = getSignificantNodes(branch);
2614
+ return nodes.length > 0 && nodes[nodes.length - 1] === elements[index];
2615
+ });
2616
+ if (!isAtEnd)
2617
+ return;
2618
+ }
2333
2619
  for (let i = 0; i < branches.length; i++) {
2334
2620
  core.removeNodeFromArray(branches[i], elements[i]);
2335
2621
  }
2336
- if (position === "before") {
2337
- parentArray.splice(conditionalIndex, 0, elements[0]);
2338
- conditionalIndex++;
2622
+ if (actualPosition === "before") {
2623
+ parentArray.splice(conditionalIndex, 0, elements[0], core.createLiteral("\n"));
2624
+ conditionalIndex += 2;
2625
+ hoistedBefore = true;
2339
2626
  }
2340
2627
  else {
2341
- parentArray.splice(conditionalIndex + 1, 0, elements[0]);
2628
+ parentArray.splice(conditionalIndex + 1, 0, core.createLiteral("\n"), elements[0]);
2342
2629
  }
2630
+ didMutate = true;
2343
2631
  }
2344
2632
  else {
2345
2633
  if (hasWrapped)
2346
2634
  return;
2635
+ const canWrap = branches.every((branch, index) => {
2636
+ const remaining = getSignificantNodes(branch);
2637
+ return remaining.length === 1 && remaining[0] === elements[index];
2638
+ });
2639
+ if (!canWrap) {
2640
+ if (position === "before")
2641
+ failedToHoistPrefix = true;
2642
+ return;
2643
+ }
2347
2644
  for (let i = 0; i < branches.length; i++) {
2348
2645
  core.replaceNodeWithBody(branches[i], elements[i]);
2349
2646
  }
@@ -2352,6 +2649,7 @@ class ERBNoDuplicateBranchElementsRule extends ParserRule {
2352
2649
  parentArray = wrapper.body;
2353
2650
  conditionalIndex = 1;
2354
2651
  hasWrapped = true;
2652
+ didMutate = true;
2355
2653
  }
2356
2654
  };
2357
2655
  for (let index = 0; index < prefixCount; index++) {
@@ -2362,7 +2660,22 @@ class ERBNoDuplicateBranchElementsRule extends ParserRule {
2362
2660
  const elements = significantBranches.map(branch => branch[branch.length - 1 - offset]);
2363
2661
  hoistElement(elements, "after");
2364
2662
  }
2365
- return result;
2663
+ if (!hasWrapped && hoistedBefore) {
2664
+ const remaining = branches.map(branch => getSignificantNodes(branch));
2665
+ if (remaining.every(branch => branch.length === 1) && allEquivalentElements(remaining.map(b => b[0]))) {
2666
+ const elements = remaining.map(b => b[0]);
2667
+ const bodiesMatch = elements.every(el => IdentityPrinter.print(el) === IdentityPrinter.print(elements[0]));
2668
+ if (!bodiesMatch && elements.every(el => el.body.length > 0)) {
2669
+ for (let i = 0; i < branches.length; i++) {
2670
+ core.replaceNodeWithBody(branches[i], elements[i]);
2671
+ }
2672
+ const wrapper = createWrapper(elements[0], [core.createLiteral("\n"), conditionalNode, core.createLiteral("\n")]);
2673
+ parentArray[conditionalIndex] = wrapper;
2674
+ didMutate = true;
2675
+ }
2676
+ }
2677
+ }
2678
+ return didMutate ? result : null;
2366
2679
  }
2367
2680
  }
2368
2681
 
@@ -2888,6 +3201,47 @@ class ERBNoRawOutputInAttributeValueRule extends ParserRule {
2888
3201
  }
2889
3202
  }
2890
3203
 
3204
+ function isAssignmentNode(prismNode) {
3205
+ const type = prismNode?.constructor?.name;
3206
+ if (!type)
3207
+ return false;
3208
+ return type.endsWith("WriteNode");
3209
+ }
3210
+ class ERBNoSilentStatementVisitor extends BaseRuleVisitor {
3211
+ visitERBContentNode(node) {
3212
+ if (core.isERBOutputNode(node))
3213
+ return;
3214
+ const prismNode = node.prismNode;
3215
+ if (!prismNode)
3216
+ return;
3217
+ if (isAssignmentNode(prismNode))
3218
+ return;
3219
+ const content = node.content?.value?.trim();
3220
+ if (!content)
3221
+ return;
3222
+ this.addOffense(`Avoid using silent ERB tags for statements. Move \`${content}\` to a controller, helper, or presenter.`, node.location);
3223
+ }
3224
+ }
3225
+ class ERBNoSilentStatementRule extends ParserRule {
3226
+ static ruleName = "erb-no-silent-statement";
3227
+ get defaultConfig() {
3228
+ return {
3229
+ enabled: false,
3230
+ severity: "warning"
3231
+ };
3232
+ }
3233
+ get parserOptions() {
3234
+ return {
3235
+ prism_nodes: true,
3236
+ };
3237
+ }
3238
+ check(result, context) {
3239
+ const visitor = new ERBNoSilentStatementVisitor(this.ruleName, context);
3240
+ visitor.visit(result.value);
3241
+ return visitor.offenses;
3242
+ }
3243
+ }
3244
+
2891
3245
  class ERBNoSilentTagInAttributeNameVisitor extends BaseRuleVisitor {
2892
3246
  visitHTMLAttributeNameNode(node) {
2893
3247
  const erbNodes = core.filterERBContentNodes(node.children);
@@ -3150,7 +3504,7 @@ class ERBNoTrailingWhitespaceRule extends ParserRule {
3150
3504
  }
3151
3505
 
3152
3506
  const JS_ATTRIBUTE_PATTERN = /^on/i;
3153
- const SAFE_PATTERN$1 = /\.to_json\s*$|\bj\s*[\s(]|\bescape_javascript\s*[\s(]/;
3507
+ const SAFE_PATTERN = /\.to_json\s*$|\bj\s*[\s(]|\bescape_javascript\s*[\s(]/;
3154
3508
  class ERBNoUnsafeJSAttributeVisitor extends AttributeVisitorMixin {
3155
3509
  checkStaticAttributeDynamicValue({ attributeName, valueNodes }) {
3156
3510
  if (!JS_ATTRIBUTE_PATTERN.test(attributeName))
@@ -3161,7 +3515,7 @@ class ERBNoUnsafeJSAttributeVisitor extends AttributeVisitorMixin {
3161
3515
  if (!core.isERBOutputNode(node))
3162
3516
  continue;
3163
3517
  const content = node.content?.value?.trim() || "";
3164
- if (SAFE_PATTERN$1.test(content))
3518
+ if (SAFE_PATTERN.test(content))
3165
3519
  continue;
3166
3520
  this.addOffense(`Unsafe ERB output in \`${attributeName}\` attribute. Use \`.to_json\`, \`j()\`, or \`escape_javascript()\` to safely encode values.`, node.location);
3167
3521
  }
@@ -3242,7 +3596,27 @@ class ERBNoUnsafeRawRule extends ParserRule {
3242
3596
  }
3243
3597
  }
3244
3598
 
3245
- const SAFE_PATTERN = /\.to_json\b/;
3599
+ const SAFE_METHOD_NAMES = new Set([
3600
+ "to_json",
3601
+ "json_escape",
3602
+ ]);
3603
+ const ESCAPE_JAVASCRIPT_METHOD_NAMES = new Set([
3604
+ "j",
3605
+ "escape_javascript",
3606
+ ]);
3607
+ class SafeCallDetector extends core.PrismVisitor {
3608
+ hasSafeCall = false;
3609
+ hasEscapeJavascriptCall = false;
3610
+ visitCallNode(node) {
3611
+ if (SAFE_METHOD_NAMES.has(node.name)) {
3612
+ this.hasSafeCall = true;
3613
+ }
3614
+ if (ESCAPE_JAVASCRIPT_METHOD_NAMES.has(node.name)) {
3615
+ this.hasEscapeJavascriptCall = true;
3616
+ }
3617
+ this.visitChildNodes(node);
3618
+ }
3619
+ }
3246
3620
  class ERBNoUnsafeScriptInterpolationVisitor extends BaseRuleVisitor {
3247
3621
  visitHTMLElementNode(node) {
3248
3622
  if (!core.isHTMLOpenTagNode(node.open_tag)) {
@@ -3271,9 +3645,17 @@ class ERBNoUnsafeScriptInterpolationVisitor extends BaseRuleVisitor {
3271
3645
  continue;
3272
3646
  if (!core.isERBOutputNode(child))
3273
3647
  continue;
3274
- const content = child.content?.value?.trim() || "";
3275
- if (SAFE_PATTERN.test(content))
3648
+ const erbContent = child;
3649
+ const prismNode = erbContent.prismNode;
3650
+ const detector = new SafeCallDetector();
3651
+ if (prismNode)
3652
+ detector.visit(prismNode);
3653
+ if (detector.hasSafeCall)
3276
3654
  continue;
3655
+ if (detector.hasEscapeJavascriptCall) {
3656
+ 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);
3657
+ continue;
3658
+ }
3277
3659
  this.addOffense("Unsafe ERB output in `<script>` tag. Use `.to_json` to safely serialize values into JavaScript.", child.location);
3278
3660
  }
3279
3661
  }
@@ -3286,6 +3668,11 @@ class ERBNoUnsafeScriptInterpolationRule extends ParserRule {
3286
3668
  severity: "error"
3287
3669
  };
3288
3670
  }
3671
+ get parserOptions() {
3672
+ return {
3673
+ prism_nodes: true,
3674
+ };
3675
+ }
3289
3676
  check(result, context) {
3290
3677
  const visitor = new ERBNoUnsafeScriptInterpolationVisitor(this.ruleName, context);
3291
3678
  visitor.visit(result.value);
@@ -4293,7 +4680,7 @@ class HerbDisableCommentValidRuleNameRule extends ParserRule {
4293
4680
  }
4294
4681
  }
4295
4682
 
4296
- const ALLOWED_TYPES = ["text/javascript"];
4683
+ const ALLOWED_TYPES = ["text/javascript", "module", "importmap", "speculationrules"];
4297
4684
  class AllowedScriptTypeVisitor extends BaseRuleVisitor {
4298
4685
  visitHTMLOpenTagNode(node) {
4299
4686
  if (core.getTagLocalName(node) === "script") {
@@ -4831,6 +5218,47 @@ class HTMLBodyOnlyElementsRule extends ParserRule {
4831
5218
  }
4832
5219
  }
4833
5220
 
5221
+ class BooleanAttributesNoValueVisitor extends AttributeVisitorMixin {
5222
+ checkStaticAttributeStaticValue({ originalAttributeName, attributeNode }) {
5223
+ this.checkAttribute(originalAttributeName, attributeNode);
5224
+ }
5225
+ checkStaticAttributeDynamicValue({ originalAttributeName, attributeNode }) {
5226
+ this.checkAttribute(originalAttributeName, attributeNode);
5227
+ }
5228
+ checkAttribute(attributeName, attributeNode) {
5229
+ if (!isBooleanAttribute(attributeName))
5230
+ return;
5231
+ if (!core.hasAttributeValue(attributeNode))
5232
+ return;
5233
+ this.addOffense(`Boolean attribute \`${IdentityPrinter.print(attributeNode.name)}\` should not have a value. Use \`${attributeName.toLowerCase()}\` instead of \`${IdentityPrinter.print(attributeNode)}\`.`, attributeNode.value.location, {
5234
+ node: attributeNode
5235
+ });
5236
+ }
5237
+ }
5238
+ class HTMLBooleanAttributesNoValueRule extends ParserRule {
5239
+ static autocorrectable = true;
5240
+ static ruleName = "html-boolean-attributes-no-value";
5241
+ get defaultConfig() {
5242
+ return {
5243
+ enabled: true,
5244
+ severity: "error"
5245
+ };
5246
+ }
5247
+ check(result, context) {
5248
+ const visitor = new BooleanAttributesNoValueVisitor(this.ruleName, context);
5249
+ visitor.visit(result.value);
5250
+ return visitor.offenses;
5251
+ }
5252
+ autofix(offense, result, _context) {
5253
+ if (!offense.autofixContext)
5254
+ return null;
5255
+ const { node } = offense.autofixContext;
5256
+ node.equals = null;
5257
+ node.value = null;
5258
+ return result;
5259
+ }
5260
+ }
5261
+
4834
5262
  class DetailsHasSummaryVisitor extends BaseRuleVisitor {
4835
5263
  visitHTMLElementNode(node) {
4836
5264
  this.checkDetailsElement(node);
@@ -4880,47 +5308,6 @@ class HTMLDetailsHasSummaryRule extends ParserRule {
4880
5308
  }
4881
5309
  }
4882
5310
 
4883
- class BooleanAttributesNoValueVisitor extends AttributeVisitorMixin {
4884
- checkStaticAttributeStaticValue({ originalAttributeName, attributeNode }) {
4885
- this.checkAttribute(originalAttributeName, attributeNode);
4886
- }
4887
- checkStaticAttributeDynamicValue({ originalAttributeName, attributeNode }) {
4888
- this.checkAttribute(originalAttributeName, attributeNode);
4889
- }
4890
- checkAttribute(attributeName, attributeNode) {
4891
- if (!isBooleanAttribute(attributeName))
4892
- return;
4893
- if (!core.hasAttributeValue(attributeNode))
4894
- return;
4895
- this.addOffense(`Boolean attribute \`${IdentityPrinter.print(attributeNode.name)}\` should not have a value. Use \`${attributeName.toLowerCase()}\` instead of \`${IdentityPrinter.print(attributeNode)}\`.`, attributeNode.value.location, {
4896
- node: attributeNode
4897
- });
4898
- }
4899
- }
4900
- class HTMLBooleanAttributesNoValueRule extends ParserRule {
4901
- static autocorrectable = true;
4902
- static ruleName = "html-boolean-attributes-no-value";
4903
- get defaultConfig() {
4904
- return {
4905
- enabled: true,
4906
- severity: "error"
4907
- };
4908
- }
4909
- check(result, context) {
4910
- const visitor = new BooleanAttributesNoValueVisitor(this.ruleName, context);
4911
- visitor.visit(result.value);
4912
- return visitor.offenses;
4913
- }
4914
- autofix(offense, result, _context) {
4915
- if (!offense.autofixContext)
4916
- return null;
4917
- const { node } = offense.autofixContext;
4918
- node.equals = null;
4919
- node.value = null;
4920
- return result;
4921
- }
4922
- }
4923
-
4924
5311
  class HeadOnlyElementsVisitor extends BaseRuleVisitor {
4925
5312
  elementStack = [];
4926
5313
  visitHTMLElementNode(node) {
@@ -6603,8 +6990,10 @@ class TurboPermanentRequireIdRule extends ParserRule {
6603
6990
 
6604
6991
  const rules = [
6605
6992
  ActionViewNoSilentHelperRule,
6993
+ ActionViewNoSilentRenderRule,
6606
6994
  ERBCommentSyntax,
6607
6995
  ERBNoCaseNodeChildrenRule,
6996
+ ERBNoEmptyControlFlowRule,
6608
6997
  ERBNoConditionalHTMLElementRule,
6609
6998
  ERBNoConditionalOpenTagRule,
6610
6999
  ERBNoDuplicateBranchElementsRule,
@@ -6619,6 +7008,7 @@ const rules = [
6619
7008
  ERBNoOutputInAttributeNameRule,
6620
7009
  ERBNoOutputInAttributePositionRule,
6621
7010
  ERBNoRawOutputInAttributeValueRule,
7011
+ ERBNoSilentStatementRule,
6622
7012
  ERBNoSilentTagInAttributeNameRule,
6623
7013
  ERBNoStatementInScriptRule,
6624
7014
  ERBNoThenInControlFlowRule,
@@ -6650,8 +7040,8 @@ const rules = [
6650
7040
  HTMLAttributeValuesRequireQuotesRule,
6651
7041
  HTMLAvoidBothDisabledAndAriaDisabledRule,
6652
7042
  HTMLBodyOnlyElementsRule,
6653
- HTMLDetailsHasSummaryRule,
6654
7043
  HTMLBooleanAttributesNoValueRule,
7044
+ HTMLDetailsHasSummaryRule,
6655
7045
  HTMLHeadOnlyElementsRule,
6656
7046
  HTMLIframeHasTitleRule,
6657
7047
  HTMLImgRequireAltRule,
@@ -7209,8 +7599,82 @@ function ruleDocumentationUrl(ruleId) {
7209
7599
  return `${DOCS_BASE_URL}/${ruleId}`;
7210
7600
  }
7211
7601
 
7602
+ Object.defineProperty(exports, "findAttributeByName", {
7603
+ enumerable: true,
7604
+ get: function () { return core.findAttributeByName; }
7605
+ });
7606
+ Object.defineProperty(exports, "getAttribute", {
7607
+ enumerable: true,
7608
+ get: function () { return core.getAttribute; }
7609
+ });
7610
+ Object.defineProperty(exports, "getAttributeName", {
7611
+ enumerable: true,
7612
+ get: function () { return core.getAttributeName; }
7613
+ });
7614
+ Object.defineProperty(exports, "getAttributeValue", {
7615
+ enumerable: true,
7616
+ get: function () { return core.getAttributeValue; }
7617
+ });
7618
+ Object.defineProperty(exports, "getAttributeValueNodes", {
7619
+ enumerable: true,
7620
+ get: function () { return core.getAttributeValueNodes; }
7621
+ });
7622
+ Object.defineProperty(exports, "getAttributeValueQuoteType", {
7623
+ enumerable: true,
7624
+ get: function () { return core.getAttributeValueQuoteType; }
7625
+ });
7626
+ Object.defineProperty(exports, "getAttributes", {
7627
+ enumerable: true,
7628
+ get: function () { return core.getAttributes; }
7629
+ });
7630
+ Object.defineProperty(exports, "getCombinedAttributeNameString", {
7631
+ enumerable: true,
7632
+ get: function () { return core.getCombinedAttributeNameString; }
7633
+ });
7634
+ Object.defineProperty(exports, "getStaticAttributeValue", {
7635
+ enumerable: true,
7636
+ get: function () { return core.getStaticAttributeValue; }
7637
+ });
7638
+ Object.defineProperty(exports, "getStaticAttributeValueContent", {
7639
+ enumerable: true,
7640
+ get: function () { return core.getStaticAttributeValueContent; }
7641
+ });
7642
+ Object.defineProperty(exports, "getTagName", {
7643
+ enumerable: true,
7644
+ get: function () { return core.getTagName; }
7645
+ });
7646
+ Object.defineProperty(exports, "hasAttribute", {
7647
+ enumerable: true,
7648
+ get: function () { return core.hasAttribute; }
7649
+ });
7650
+ Object.defineProperty(exports, "hasAttributeValue", {
7651
+ enumerable: true,
7652
+ get: function () { return core.hasAttributeValue; }
7653
+ });
7654
+ Object.defineProperty(exports, "hasDynamicAttributeName", {
7655
+ enumerable: true,
7656
+ get: function () { return core.hasDynamicAttributeName; }
7657
+ });
7658
+ Object.defineProperty(exports, "hasDynamicAttributeValue", {
7659
+ enumerable: true,
7660
+ get: function () { return core.hasDynamicAttributeValue; }
7661
+ });
7662
+ Object.defineProperty(exports, "hasStaticAttributeValue", {
7663
+ enumerable: true,
7664
+ get: function () { return core.hasStaticAttributeValue; }
7665
+ });
7666
+ Object.defineProperty(exports, "hasStaticAttributeValueContent", {
7667
+ enumerable: true,
7668
+ get: function () { return core.hasStaticAttributeValueContent; }
7669
+ });
7670
+ Object.defineProperty(exports, "isAttributeValueQuoted", {
7671
+ enumerable: true,
7672
+ get: function () { return core.isAttributeValueQuoted; }
7673
+ });
7212
7674
  exports.ABSTRACT_ARIA_ROLES = ABSTRACT_ARIA_ROLES;
7213
7675
  exports.ARIA_ATTRIBUTES = ARIA_ATTRIBUTES;
7676
+ exports.ActionViewNoSilentHelperRule = ActionViewNoSilentHelperRule;
7677
+ exports.ActionViewNoSilentRenderRule = ActionViewNoSilentRenderRule;
7214
7678
  exports.AttributeVisitorMixin = AttributeVisitorMixin;
7215
7679
  exports.BaseLexerRuleVisitor = BaseLexerRuleVisitor;
7216
7680
  exports.BaseRuleVisitor = BaseRuleVisitor;
@@ -7224,6 +7688,7 @@ exports.ERBCommentSyntax = ERBCommentSyntax;
7224
7688
  exports.ERBNoCaseNodeChildrenRule = ERBNoCaseNodeChildrenRule;
7225
7689
  exports.ERBNoConditionalOpenTagRule = ERBNoConditionalOpenTagRule;
7226
7690
  exports.ERBNoDuplicateBranchElementsRule = ERBNoDuplicateBranchElementsRule;
7691
+ exports.ERBNoEmptyControlFlowRule = ERBNoEmptyControlFlowRule;
7227
7692
  exports.ERBNoEmptyTagsRule = ERBNoEmptyTagsRule;
7228
7693
  exports.ERBNoExtraNewLineRule = ERBNoExtraNewLineRule;
7229
7694
  exports.ERBNoExtraWhitespaceRule = ERBNoExtraWhitespaceRule;
@@ -7234,6 +7699,7 @@ exports.ERBNoOutputControlFlowRule = ERBNoOutputControlFlowRule;
7234
7699
  exports.ERBNoOutputInAttributeNameRule = ERBNoOutputInAttributeNameRule;
7235
7700
  exports.ERBNoOutputInAttributePositionRule = ERBNoOutputInAttributePositionRule;
7236
7701
  exports.ERBNoRawOutputInAttributeValueRule = ERBNoRawOutputInAttributeValueRule;
7702
+ exports.ERBNoSilentStatementRule = ERBNoSilentStatementRule;
7237
7703
  exports.ERBNoSilentTagInAttributeNameRule = ERBNoSilentTagInAttributeNameRule;
7238
7704
  exports.ERBNoStatementInScriptRule = ERBNoStatementInScriptRule;
7239
7705
  exports.ERBNoThenInControlFlowRule = ERBNoThenInControlFlowRule;