@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.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
  /**
@@ -1189,13 +1199,6 @@ const HTML_VOID_ELEMENTS = new Set([
1189
1199
  "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta",
1190
1200
  "param", "source", "track", "wbr",
1191
1201
  ]);
1192
- const HTML_BOOLEAN_ATTRIBUTES = new Set([
1193
- "autofocus", "autoplay", "checked", "controls", "defer", "disabled", "hidden",
1194
- "loop", "multiple", "muted", "readonly", "required", "reversed", "selected",
1195
- "open", "default", "formnovalidate", "novalidate", "itemscope", "scoped",
1196
- "seamless", "allowfullscreen", "async", "compact", "declare", "nohref",
1197
- "noresize", "noshade", "nowrap", "sortable", "truespeed", "typemustmatch"
1198
- ]);
1199
1202
  const HEADING_TAGS = new Set(["h1", "h2", "h3", "h4", "h5", "h6"]);
1200
1203
  /**
1201
1204
  * SVG elements that use camelCase naming
@@ -1350,12 +1353,6 @@ function isBlockElement(tagName) {
1350
1353
  function isVoidElement(tagName) {
1351
1354
  return HTML_VOID_ELEMENTS.has(tagName.toLowerCase());
1352
1355
  }
1353
- /**
1354
- * Checks if an attribute is a boolean attribute
1355
- */
1356
- function isBooleanAttribute(attributeName) {
1357
- return HTML_BOOLEAN_ATTRIBUTES.has(attributeName.toLowerCase());
1358
- }
1359
1356
  /**
1360
1357
  * Attribute visitor that provides granular processing based on both
1361
1358
  * attribute name type (static/dynamic) and value type (static/dynamic)
@@ -1378,7 +1375,7 @@ class AttributeVisitorMixin extends BaseRuleVisitor {
1378
1375
  core.forEachAttribute(node, (attributeNode) => {
1379
1376
  const staticAttributeName = core.getAttributeName(attributeNode);
1380
1377
  const originalAttributeName = core.getAttributeName(attributeNode, false) || "";
1381
- const isDynamicName = core.hasDynamicAttributeNameOnAttribute(attributeNode);
1378
+ const isDynamicName = core.hasDynamicAttributeName(attributeNode);
1382
1379
  const staticAttributeValue = core.getStaticAttributeValue(attributeNode);
1383
1380
  const valueNodes = core.getAttributeValueNodes(attributeNode);
1384
1381
  const hasOutputERB = core.hasERBOutput(valueNodes);
@@ -1455,7 +1452,7 @@ class BaseLexerRuleVisitor {
1455
1452
  * Helper method to create an unbound lint offense (without severity).
1456
1453
  * The Linter will bind severity based on the rule's config.
1457
1454
  */
1458
- createOffense(message, location, autofixContext, severity) {
1455
+ createOffense(message, location, autofixContext, severity, tags) {
1459
1456
  return {
1460
1457
  rule: this.ruleName,
1461
1458
  code: this.ruleName,
@@ -1464,13 +1461,14 @@ class BaseLexerRuleVisitor {
1464
1461
  location,
1465
1462
  autofixContext,
1466
1463
  severity,
1464
+ tags,
1467
1465
  };
1468
1466
  }
1469
1467
  /**
1470
1468
  * Helper method to add an offense to the offenses array
1471
1469
  */
1472
- addOffense(message, location, autofixContext, severity) {
1473
- this.offenses.push(this.createOffense(message, location, autofixContext, severity));
1470
+ addOffense(message, location, autofixContext, severity, tags) {
1471
+ this.offenses.push(this.createOffense(message, location, autofixContext, severity, tags));
1474
1472
  }
1475
1473
  /**
1476
1474
  * Main entry point for lexer rule visitors
@@ -1511,7 +1509,7 @@ class BaseSourceRuleVisitor {
1511
1509
  * Helper method to create an unbound lint offense (without severity).
1512
1510
  * The Linter will bind severity based on the rule's config.
1513
1511
  */
1514
- createOffense(message, location, autofixContext, severity) {
1512
+ createOffense(message, location, autofixContext, severity, tags) {
1515
1513
  return {
1516
1514
  rule: this.ruleName,
1517
1515
  code: this.ruleName,
@@ -1520,13 +1518,14 @@ class BaseSourceRuleVisitor {
1520
1518
  location,
1521
1519
  autofixContext,
1522
1520
  severity,
1521
+ tags,
1523
1522
  };
1524
1523
  }
1525
1524
  /**
1526
1525
  * Helper method to add an offense to the offenses array
1527
1526
  */
1528
- addOffense(message, location, autofixContext, severity) {
1529
- this.offenses.push(this.createOffense(message, location, autofixContext, severity));
1527
+ addOffense(message, location, autofixContext, severity, tags) {
1528
+ this.offenses.push(this.createOffense(message, location, autofixContext, severity, tags));
1530
1529
  }
1531
1530
  /**
1532
1531
  * Main entry point for source rule visitors
@@ -1876,6 +1875,34 @@ class ActionViewNoSilentHelperRule extends ParserRule {
1876
1875
  }
1877
1876
  }
1878
1877
 
1878
+ class ActionViewNoSilentRenderVisitor extends BaseRuleVisitor {
1879
+ visitERBRenderNode(node) {
1880
+ if (!core.isERBOutputNode(node)) {
1881
+ this.addOffense(`Avoid using \`${node.tag_opening?.value} %>\` with \`render\`. Use \`<%= %>\` to ensure the rendered content is output.`, node.location);
1882
+ }
1883
+ this.visitChildNodes(node);
1884
+ }
1885
+ }
1886
+ class ActionViewNoSilentRenderRule extends ParserRule {
1887
+ static ruleName = "actionview-no-silent-render";
1888
+ get defaultConfig() {
1889
+ return {
1890
+ enabled: true,
1891
+ severity: "error"
1892
+ };
1893
+ }
1894
+ get parserOptions() {
1895
+ return {
1896
+ render_nodes: true,
1897
+ };
1898
+ }
1899
+ check(result, context) {
1900
+ const visitor = new ActionViewNoSilentRenderVisitor(this.ruleName, context);
1901
+ visitor.visit(result.value);
1902
+ return visitor.offenses;
1903
+ }
1904
+ }
1905
+
1879
1906
  class ERBCommentSyntaxVisitor extends BaseRuleVisitor {
1880
1907
  visitERBContentNode(node) {
1881
1908
  const content = node.content?.value || "";
@@ -1940,7 +1967,9 @@ class ERBNoCaseNodeChildrenVisitor extends BaseRuleVisitor {
1940
1967
  for (const child of node.children) {
1941
1968
  if (!this.isAllowedContent(child)) {
1942
1969
  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);
1970
+ 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);
1971
+ offense.tags = ["unnecessary"];
1972
+ this.offenses.push(offense);
1944
1973
  }
1945
1974
  }
1946
1975
  }
@@ -1970,6 +1999,193 @@ class ERBNoCaseNodeChildrenRule extends ParserRule {
1970
1999
  }
1971
2000
  }
1972
2001
 
2002
+ class ERBNoEmptyControlFlowVisitor extends BaseRuleVisitor {
2003
+ processedIfNodes = new Set();
2004
+ processedElseNodes = new Set();
2005
+ visitERBIfNode(node) {
2006
+ if (this.processedIfNodes.has(node)) {
2007
+ return;
2008
+ }
2009
+ this.markIfChainAsProcessed(node);
2010
+ this.markElseNodesInIfChain(node);
2011
+ const entireChainEmpty = this.isEntireIfChainEmpty(node);
2012
+ if (entireChainEmpty) {
2013
+ this.addEmptyBlockOffense(node, node.statements, "if");
2014
+ }
2015
+ else {
2016
+ this.checkIfChainParts(node);
2017
+ }
2018
+ this.visitChildNodes(node);
2019
+ }
2020
+ visitERBElseNode(node) {
2021
+ if (this.processedElseNodes.has(node)) {
2022
+ this.visitChildNodes(node);
2023
+ return;
2024
+ }
2025
+ this.addEmptyBlockOffense(node, node.statements, "else");
2026
+ this.visitChildNodes(node);
2027
+ }
2028
+ visitERBUnlessNode(node) {
2029
+ const unlessHasContent = this.statementsHaveContent(node.statements);
2030
+ const elseHasContent = node.else_clause && this.statementsHaveContent(node.else_clause.statements);
2031
+ if (node.else_clause) {
2032
+ this.processedElseNodes.add(node.else_clause);
2033
+ }
2034
+ const entireBlockEmpty = !unlessHasContent && !elseHasContent;
2035
+ if (entireBlockEmpty) {
2036
+ this.addEmptyBlockOffense(node, node.statements, "unless");
2037
+ }
2038
+ else {
2039
+ if (!unlessHasContent) {
2040
+ this.addEmptyBlockOffenseWithEnd(node, node.statements, "unless", node.else_clause);
2041
+ }
2042
+ if (node.else_clause && !elseHasContent) {
2043
+ this.addEmptyBlockOffense(node.else_clause, node.else_clause.statements, "else");
2044
+ }
2045
+ }
2046
+ this.visitChildNodes(node);
2047
+ }
2048
+ visitERBForNode(node) {
2049
+ this.addEmptyBlockOffense(node, node.statements, "for");
2050
+ this.visitChildNodes(node);
2051
+ }
2052
+ visitERBWhileNode(node) {
2053
+ this.addEmptyBlockOffense(node, node.statements, "while");
2054
+ this.visitChildNodes(node);
2055
+ }
2056
+ visitERBUntilNode(node) {
2057
+ this.addEmptyBlockOffense(node, node.statements, "until");
2058
+ this.visitChildNodes(node);
2059
+ }
2060
+ visitERBWhenNode(node) {
2061
+ if (!node.then_keyword) {
2062
+ this.addEmptyBlockOffense(node, node.statements, "when");
2063
+ }
2064
+ this.visitChildNodes(node);
2065
+ }
2066
+ visitERBInNode(node) {
2067
+ if (!node.then_keyword) {
2068
+ this.addEmptyBlockOffense(node, node.statements, "in");
2069
+ }
2070
+ this.visitChildNodes(node);
2071
+ }
2072
+ visitERBBeginNode(node) {
2073
+ this.addEmptyBlockOffense(node, node.statements, "begin");
2074
+ this.visitChildNodes(node);
2075
+ }
2076
+ visitERBRescueNode(node) {
2077
+ this.addEmptyBlockOffense(node, node.statements, "rescue");
2078
+ this.visitChildNodes(node);
2079
+ }
2080
+ visitERBEnsureNode(node) {
2081
+ this.addEmptyBlockOffense(node, node.statements, "ensure");
2082
+ this.visitChildNodes(node);
2083
+ }
2084
+ visitERBBlockNode(node) {
2085
+ this.addEmptyBlockOffense(node, node.body, "do");
2086
+ this.visitChildNodes(node);
2087
+ }
2088
+ addEmptyBlockOffense(node, statements, blockType) {
2089
+ this.addEmptyBlockOffenseWithEnd(node, statements, blockType, null);
2090
+ }
2091
+ addEmptyBlockOffenseWithEnd(node, statements, blockType, subsequentNode) {
2092
+ if (this.statementsHaveContent(statements)) {
2093
+ return;
2094
+ }
2095
+ const startLocation = node.location.start;
2096
+ const endLocation = subsequentNode
2097
+ ? subsequentNode.location.start
2098
+ : node.location.end;
2099
+ const location = core.Location.from(startLocation.line, startLocation.column, endLocation.line, endLocation.column);
2100
+ const offense = this.createOffense(`Empty ${blockType} block: this control flow statement has no content`, location);
2101
+ offense.tags = ["unnecessary"];
2102
+ this.offenses.push(offense);
2103
+ }
2104
+ statementsHaveContent(statements) {
2105
+ return statements.some(statement => {
2106
+ if (core.isHTMLTextNode(statement)) {
2107
+ return statement.content.trim() !== "";
2108
+ }
2109
+ return true;
2110
+ });
2111
+ }
2112
+ markIfChainAsProcessed(node) {
2113
+ this.processedIfNodes.add(node);
2114
+ this.traverseSubsequentNodes(node.subsequent, (current) => {
2115
+ if (core.isERBIfNode(current)) {
2116
+ this.processedIfNodes.add(current);
2117
+ }
2118
+ });
2119
+ }
2120
+ markElseNodesInIfChain(node) {
2121
+ this.traverseSubsequentNodes(node.subsequent, (current) => {
2122
+ if (core.isERBElseNode(current)) {
2123
+ this.processedElseNodes.add(current);
2124
+ }
2125
+ });
2126
+ }
2127
+ traverseSubsequentNodes(startNode, callback) {
2128
+ let current = startNode;
2129
+ while (current) {
2130
+ if (core.isERBIfNode(current)) {
2131
+ callback(current);
2132
+ current = current.subsequent;
2133
+ }
2134
+ else if (core.isERBElseNode(current)) {
2135
+ callback(current);
2136
+ break;
2137
+ }
2138
+ else {
2139
+ break;
2140
+ }
2141
+ }
2142
+ }
2143
+ checkIfChainParts(node) {
2144
+ if (!this.statementsHaveContent(node.statements)) {
2145
+ this.addEmptyBlockOffenseWithEnd(node, node.statements, "if", node.subsequent);
2146
+ }
2147
+ this.traverseSubsequentNodes(node.subsequent, (current) => {
2148
+ if (this.statementsHaveContent(current.statements)) {
2149
+ return;
2150
+ }
2151
+ const blockType = core.isERBIfNode(current) ? "elsif" : "else";
2152
+ const nextSubsequent = core.isERBIfNode(current) ? current.subsequent : null;
2153
+ if (nextSubsequent) {
2154
+ this.addEmptyBlockOffenseWithEnd(current, current.statements, blockType, nextSubsequent);
2155
+ }
2156
+ else {
2157
+ this.addEmptyBlockOffense(current, current.statements, blockType);
2158
+ }
2159
+ });
2160
+ }
2161
+ isEntireIfChainEmpty(node) {
2162
+ if (this.statementsHaveContent(node.statements)) {
2163
+ return false;
2164
+ }
2165
+ let hasContent = false;
2166
+ this.traverseSubsequentNodes(node.subsequent, (current) => {
2167
+ if (this.statementsHaveContent(current.statements)) {
2168
+ hasContent = true;
2169
+ }
2170
+ });
2171
+ return !hasContent;
2172
+ }
2173
+ }
2174
+ class ERBNoEmptyControlFlowRule extends ParserRule {
2175
+ static ruleName = "erb-no-empty-control-flow";
2176
+ get defaultConfig() {
2177
+ return {
2178
+ enabled: true,
2179
+ severity: "hint"
2180
+ };
2181
+ }
2182
+ check(result, context) {
2183
+ const visitor = new ERBNoEmptyControlFlowVisitor(this.ruleName, context);
2184
+ visitor.visit(result.value);
2185
+ return visitor.offenses;
2186
+ }
2187
+ }
2188
+
1973
2189
  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
2190
  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
2191
  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 +2344,15 @@ class ERBNoConditionalOpenTagRule extends ParserRule {
2128
2344
  function getSignificantNodes(statements) {
2129
2345
  return statements.filter(node => !core.isPureWhitespaceNode(node));
2130
2346
  }
2347
+ function trimWhitespaceNodes(nodes) {
2348
+ let start = 0;
2349
+ let end = nodes.length;
2350
+ while (start < end && core.isPureWhitespaceNode(nodes[start]))
2351
+ start++;
2352
+ while (end > start && core.isPureWhitespaceNode(nodes[end - 1]))
2353
+ end--;
2354
+ return nodes.slice(start, end);
2355
+ }
2131
2356
  function allEquivalentElements(nodes) {
2132
2357
  if (nodes.length < 2)
2133
2358
  return false;
@@ -2245,9 +2470,19 @@ class ERBNoDuplicateBranchElementsVisitor extends BaseRuleVisitor {
2245
2470
  if (core.isERBIfNode(node)) {
2246
2471
  this.markSubsequentIfNodesAsProcessed(node);
2247
2472
  }
2473
+ if (this.allBranchesIdentical(branches)) {
2474
+ this.addOffense("All branches of this conditional have identical content. The conditional can be removed.", node.location, { node: node, allIdentical: true }, "warning");
2475
+ return;
2476
+ }
2248
2477
  const state = { isFirstOffense: true };
2249
2478
  this.checkBranches(branches, node, state);
2250
2479
  }
2480
+ allBranchesIdentical(branches) {
2481
+ if (branches.length < 2)
2482
+ return false;
2483
+ const first = branches[0].map(node => IdentityPrinter.print(node)).join("");
2484
+ return branches.slice(1).every(branch => branch.map(node => IdentityPrinter.print(node)).join("") === first);
2485
+ }
2251
2486
  markSubsequentIfNodesAsProcessed(node) {
2252
2487
  let current = node.subsequent;
2253
2488
  while (current) {
@@ -2281,11 +2516,23 @@ class ERBNoDuplicateBranchElementsVisitor extends BaseRuleVisitor {
2281
2516
  const bodiesMatch = elements.every(element => IdentityPrinter.print(element) === IdentityPrinter.print(elements[0]));
2282
2517
  for (const element of elements) {
2283
2518
  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;
2519
+ if (bodiesMatch) {
2520
+ const autofixContext = state.isFirstOffense
2521
+ ? { node: conditionalNode }
2522
+ : undefined;
2523
+ this.addOffense(`The \`${printed}\` element is duplicated across all branches of this conditional and can be moved outside.`, element.location, autofixContext);
2524
+ state.isFirstOffense = false;
2525
+ }
2526
+ else {
2527
+ const autofixContext = state.isFirstOffense
2528
+ ? { node: conditionalNode }
2529
+ : undefined;
2530
+ const tagNameLocation = core.isHTMLOpenTagNode(element.open_tag) && element.open_tag.tag_name?.location
2531
+ ? element.open_tag.tag_name.location
2532
+ : element?.open_tag?.location || element.location;
2533
+ this.addOffense(`The \`${printed}\` tag is repeated across all branches with different content. Consider extracting the shared tag outside the conditional.`, tagNameLocation, autofixContext, "hint");
2534
+ state.isFirstOffense = false;
2535
+ }
2289
2536
  }
2290
2537
  if (!bodiesMatch && bodies.every(body => body.length > 0)) {
2291
2538
  this.checkBranches(bodies, conditionalNode, state);
@@ -2314,6 +2561,15 @@ class ERBNoDuplicateBranchElementsRule extends ParserRule {
2314
2561
  const branches = collectBranches(conditionalNode);
2315
2562
  if (!branches)
2316
2563
  return null;
2564
+ if (offense.autofixContext.allIdentical) {
2565
+ const parentInfo = core.findParentArray(result.value, conditionalNode);
2566
+ if (!parentInfo)
2567
+ return null;
2568
+ const { array: parentArray, index: conditionalIndex } = parentInfo;
2569
+ const firstBranchContent = trimWhitespaceNodes(branches[0]);
2570
+ parentArray.splice(conditionalIndex, 1, ...firstBranchContent);
2571
+ return result;
2572
+ }
2317
2573
  const significantBranches = branches.map(getSignificantNodes);
2318
2574
  if (significantBranches.some(branch => branch.length === 0))
2319
2575
  return null;
@@ -2327,23 +2583,51 @@ class ERBNoDuplicateBranchElementsRule extends ParserRule {
2327
2583
  return null;
2328
2584
  let { array: parentArray, index: conditionalIndex } = parentInfo;
2329
2585
  let hasWrapped = false;
2586
+ let didMutate = false;
2587
+ let failedToHoistPrefix = false;
2588
+ let hoistedBefore = false;
2330
2589
  const hoistElement = (elements, position) => {
2590
+ const actualPosition = (position === "before" && failedToHoistPrefix) ? "after" : position;
2331
2591
  const bodiesMatch = elements.every(element => IdentityPrinter.print(element) === IdentityPrinter.print(elements[0]));
2332
2592
  if (bodiesMatch) {
2593
+ if (actualPosition === "after") {
2594
+ const currentLengths = branches.map(b => getSignificantNodes(b).length);
2595
+ if (currentLengths.some(l => l !== currentLengths[0]))
2596
+ return;
2597
+ }
2598
+ if (actualPosition === "after" && position === "before") {
2599
+ const isAtEnd = branches.every((branch, index) => {
2600
+ const nodes = getSignificantNodes(branch);
2601
+ return nodes.length > 0 && nodes[nodes.length - 1] === elements[index];
2602
+ });
2603
+ if (!isAtEnd)
2604
+ return;
2605
+ }
2333
2606
  for (let i = 0; i < branches.length; i++) {
2334
2607
  core.removeNodeFromArray(branches[i], elements[i]);
2335
2608
  }
2336
- if (position === "before") {
2337
- parentArray.splice(conditionalIndex, 0, elements[0]);
2338
- conditionalIndex++;
2609
+ if (actualPosition === "before") {
2610
+ parentArray.splice(conditionalIndex, 0, elements[0], core.createLiteral("\n"));
2611
+ conditionalIndex += 2;
2612
+ hoistedBefore = true;
2339
2613
  }
2340
2614
  else {
2341
- parentArray.splice(conditionalIndex + 1, 0, elements[0]);
2615
+ parentArray.splice(conditionalIndex + 1, 0, core.createLiteral("\n"), elements[0]);
2342
2616
  }
2617
+ didMutate = true;
2343
2618
  }
2344
2619
  else {
2345
2620
  if (hasWrapped)
2346
2621
  return;
2622
+ const canWrap = branches.every((branch, index) => {
2623
+ const remaining = getSignificantNodes(branch);
2624
+ return remaining.length === 1 && remaining[0] === elements[index];
2625
+ });
2626
+ if (!canWrap) {
2627
+ if (position === "before")
2628
+ failedToHoistPrefix = true;
2629
+ return;
2630
+ }
2347
2631
  for (let i = 0; i < branches.length; i++) {
2348
2632
  core.replaceNodeWithBody(branches[i], elements[i]);
2349
2633
  }
@@ -2352,6 +2636,7 @@ class ERBNoDuplicateBranchElementsRule extends ParserRule {
2352
2636
  parentArray = wrapper.body;
2353
2637
  conditionalIndex = 1;
2354
2638
  hasWrapped = true;
2639
+ didMutate = true;
2355
2640
  }
2356
2641
  };
2357
2642
  for (let index = 0; index < prefixCount; index++) {
@@ -2362,7 +2647,22 @@ class ERBNoDuplicateBranchElementsRule extends ParserRule {
2362
2647
  const elements = significantBranches.map(branch => branch[branch.length - 1 - offset]);
2363
2648
  hoistElement(elements, "after");
2364
2649
  }
2365
- return result;
2650
+ if (!hasWrapped && hoistedBefore) {
2651
+ const remaining = branches.map(branch => getSignificantNodes(branch));
2652
+ if (remaining.every(branch => branch.length === 1) && allEquivalentElements(remaining.map(b => b[0]))) {
2653
+ const elements = remaining.map(b => b[0]);
2654
+ const bodiesMatch = elements.every(el => IdentityPrinter.print(el) === IdentityPrinter.print(elements[0]));
2655
+ if (!bodiesMatch && elements.every(el => el.body.length > 0)) {
2656
+ for (let i = 0; i < branches.length; i++) {
2657
+ core.replaceNodeWithBody(branches[i], elements[i]);
2658
+ }
2659
+ const wrapper = createWrapper(elements[0], [core.createLiteral("\n"), conditionalNode, core.createLiteral("\n")]);
2660
+ parentArray[conditionalIndex] = wrapper;
2661
+ didMutate = true;
2662
+ }
2663
+ }
2664
+ }
2665
+ return didMutate ? result : null;
2366
2666
  }
2367
2667
  }
2368
2668
 
@@ -2888,6 +3188,47 @@ class ERBNoRawOutputInAttributeValueRule extends ParserRule {
2888
3188
  }
2889
3189
  }
2890
3190
 
3191
+ function isAssignmentNode(prismNode) {
3192
+ const type = prismNode?.constructor?.name;
3193
+ if (!type)
3194
+ return false;
3195
+ return type.endsWith("WriteNode");
3196
+ }
3197
+ class ERBNoSilentStatementVisitor extends BaseRuleVisitor {
3198
+ visitERBContentNode(node) {
3199
+ if (core.isERBOutputNode(node))
3200
+ return;
3201
+ const prismNode = node.prismNode;
3202
+ if (!prismNode)
3203
+ return;
3204
+ if (isAssignmentNode(prismNode))
3205
+ return;
3206
+ const content = node.content?.value?.trim();
3207
+ if (!content)
3208
+ return;
3209
+ this.addOffense(`Avoid using silent ERB tags for statements. Move \`${content}\` to a controller, helper, or presenter.`, node.location);
3210
+ }
3211
+ }
3212
+ class ERBNoSilentStatementRule extends ParserRule {
3213
+ static ruleName = "erb-no-silent-statement";
3214
+ get defaultConfig() {
3215
+ return {
3216
+ enabled: false,
3217
+ severity: "warning"
3218
+ };
3219
+ }
3220
+ get parserOptions() {
3221
+ return {
3222
+ prism_nodes: true,
3223
+ };
3224
+ }
3225
+ check(result, context) {
3226
+ const visitor = new ERBNoSilentStatementVisitor(this.ruleName, context);
3227
+ visitor.visit(result.value);
3228
+ return visitor.offenses;
3229
+ }
3230
+ }
3231
+
2891
3232
  class ERBNoSilentTagInAttributeNameVisitor extends BaseRuleVisitor {
2892
3233
  visitHTMLAttributeNameNode(node) {
2893
3234
  const erbNodes = core.filterERBContentNodes(node.children);
@@ -3150,7 +3491,7 @@ class ERBNoTrailingWhitespaceRule extends ParserRule {
3150
3491
  }
3151
3492
 
3152
3493
  const JS_ATTRIBUTE_PATTERN = /^on/i;
3153
- const SAFE_PATTERN$1 = /\.to_json\s*$|\bj\s*[\s(]|\bescape_javascript\s*[\s(]/;
3494
+ const SAFE_PATTERN = /\.to_json\s*$|\bj\s*[\s(]|\bescape_javascript\s*[\s(]/;
3154
3495
  class ERBNoUnsafeJSAttributeVisitor extends AttributeVisitorMixin {
3155
3496
  checkStaticAttributeDynamicValue({ attributeName, valueNodes }) {
3156
3497
  if (!JS_ATTRIBUTE_PATTERN.test(attributeName))
@@ -3161,7 +3502,7 @@ class ERBNoUnsafeJSAttributeVisitor extends AttributeVisitorMixin {
3161
3502
  if (!core.isERBOutputNode(node))
3162
3503
  continue;
3163
3504
  const content = node.content?.value?.trim() || "";
3164
- if (SAFE_PATTERN$1.test(content))
3505
+ if (SAFE_PATTERN.test(content))
3165
3506
  continue;
3166
3507
  this.addOffense(`Unsafe ERB output in \`${attributeName}\` attribute. Use \`.to_json\`, \`j()\`, or \`escape_javascript()\` to safely encode values.`, node.location);
3167
3508
  }
@@ -3242,7 +3583,27 @@ class ERBNoUnsafeRawRule extends ParserRule {
3242
3583
  }
3243
3584
  }
3244
3585
 
3245
- const SAFE_PATTERN = /\.to_json\b/;
3586
+ const SAFE_METHOD_NAMES = new Set([
3587
+ "to_json",
3588
+ "json_escape",
3589
+ ]);
3590
+ const ESCAPE_JAVASCRIPT_METHOD_NAMES = new Set([
3591
+ "j",
3592
+ "escape_javascript",
3593
+ ]);
3594
+ class SafeCallDetector extends core.PrismVisitor {
3595
+ hasSafeCall = false;
3596
+ hasEscapeJavascriptCall = false;
3597
+ visitCallNode(node) {
3598
+ if (SAFE_METHOD_NAMES.has(node.name)) {
3599
+ this.hasSafeCall = true;
3600
+ }
3601
+ if (ESCAPE_JAVASCRIPT_METHOD_NAMES.has(node.name)) {
3602
+ this.hasEscapeJavascriptCall = true;
3603
+ }
3604
+ this.visitChildNodes(node);
3605
+ }
3606
+ }
3246
3607
  class ERBNoUnsafeScriptInterpolationVisitor extends BaseRuleVisitor {
3247
3608
  visitHTMLElementNode(node) {
3248
3609
  if (!core.isHTMLOpenTagNode(node.open_tag)) {
@@ -3271,9 +3632,17 @@ class ERBNoUnsafeScriptInterpolationVisitor extends BaseRuleVisitor {
3271
3632
  continue;
3272
3633
  if (!core.isERBOutputNode(child))
3273
3634
  continue;
3274
- const content = child.content?.value?.trim() || "";
3275
- if (SAFE_PATTERN.test(content))
3635
+ const erbContent = child;
3636
+ const prismNode = erbContent.prismNode;
3637
+ const detector = new SafeCallDetector();
3638
+ if (prismNode)
3639
+ detector.visit(prismNode);
3640
+ if (detector.hasSafeCall)
3276
3641
  continue;
3642
+ if (detector.hasEscapeJavascriptCall) {
3643
+ 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);
3644
+ continue;
3645
+ }
3277
3646
  this.addOffense("Unsafe ERB output in `<script>` tag. Use `.to_json` to safely serialize values into JavaScript.", child.location);
3278
3647
  }
3279
3648
  }
@@ -3286,6 +3655,11 @@ class ERBNoUnsafeScriptInterpolationRule extends ParserRule {
3286
3655
  severity: "error"
3287
3656
  };
3288
3657
  }
3658
+ get parserOptions() {
3659
+ return {
3660
+ prism_nodes: true,
3661
+ };
3662
+ }
3289
3663
  check(result, context) {
3290
3664
  const visitor = new ERBNoUnsafeScriptInterpolationVisitor(this.ruleName, context);
3291
3665
  visitor.visit(result.value);
@@ -4293,7 +4667,7 @@ class HerbDisableCommentValidRuleNameRule extends ParserRule {
4293
4667
  }
4294
4668
  }
4295
4669
 
4296
- const ALLOWED_TYPES = ["text/javascript"];
4670
+ const ALLOWED_TYPES = ["text/javascript", "module", "importmap", "speculationrules"];
4297
4671
  class AllowedScriptTypeVisitor extends BaseRuleVisitor {
4298
4672
  visitHTMLOpenTagNode(node) {
4299
4673
  if (core.getTagLocalName(node) === "script") {
@@ -4831,6 +5205,47 @@ class HTMLBodyOnlyElementsRule extends ParserRule {
4831
5205
  }
4832
5206
  }
4833
5207
 
5208
+ class BooleanAttributesNoValueVisitor extends AttributeVisitorMixin {
5209
+ checkStaticAttributeStaticValue({ originalAttributeName, attributeNode }) {
5210
+ this.checkAttribute(originalAttributeName, attributeNode);
5211
+ }
5212
+ checkStaticAttributeDynamicValue({ originalAttributeName, attributeNode }) {
5213
+ this.checkAttribute(originalAttributeName, attributeNode);
5214
+ }
5215
+ checkAttribute(attributeName, attributeNode) {
5216
+ if (!core.isBooleanAttribute(attributeName))
5217
+ return;
5218
+ if (!core.hasAttributeValue(attributeNode))
5219
+ return;
5220
+ this.addOffense(`Boolean attribute \`${IdentityPrinter.print(attributeNode.name)}\` should not have a value. Use \`${attributeName.toLowerCase()}\` instead of \`${IdentityPrinter.print(attributeNode)}\`.`, attributeNode.value.location, {
5221
+ node: attributeNode
5222
+ });
5223
+ }
5224
+ }
5225
+ class HTMLBooleanAttributesNoValueRule extends ParserRule {
5226
+ static autocorrectable = true;
5227
+ static ruleName = "html-boolean-attributes-no-value";
5228
+ get defaultConfig() {
5229
+ return {
5230
+ enabled: true,
5231
+ severity: "error"
5232
+ };
5233
+ }
5234
+ check(result, context) {
5235
+ const visitor = new BooleanAttributesNoValueVisitor(this.ruleName, context);
5236
+ visitor.visit(result.value);
5237
+ return visitor.offenses;
5238
+ }
5239
+ autofix(offense, result, _context) {
5240
+ if (!offense.autofixContext)
5241
+ return null;
5242
+ const { node } = offense.autofixContext;
5243
+ node.equals = null;
5244
+ node.value = null;
5245
+ return result;
5246
+ }
5247
+ }
5248
+
4834
5249
  class DetailsHasSummaryVisitor extends BaseRuleVisitor {
4835
5250
  visitHTMLElementNode(node) {
4836
5251
  this.checkDetailsElement(node);
@@ -4880,47 +5295,6 @@ class HTMLDetailsHasSummaryRule extends ParserRule {
4880
5295
  }
4881
5296
  }
4882
5297
 
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
5298
  class HeadOnlyElementsVisitor extends BaseRuleVisitor {
4925
5299
  elementStack = [];
4926
5300
  visitHTMLElementNode(node) {
@@ -6603,8 +6977,10 @@ class TurboPermanentRequireIdRule extends ParserRule {
6603
6977
 
6604
6978
  const rules = [
6605
6979
  ActionViewNoSilentHelperRule,
6980
+ ActionViewNoSilentRenderRule,
6606
6981
  ERBCommentSyntax,
6607
6982
  ERBNoCaseNodeChildrenRule,
6983
+ ERBNoEmptyControlFlowRule,
6608
6984
  ERBNoConditionalHTMLElementRule,
6609
6985
  ERBNoConditionalOpenTagRule,
6610
6986
  ERBNoDuplicateBranchElementsRule,
@@ -6619,6 +6995,7 @@ const rules = [
6619
6995
  ERBNoOutputInAttributeNameRule,
6620
6996
  ERBNoOutputInAttributePositionRule,
6621
6997
  ERBNoRawOutputInAttributeValueRule,
6998
+ ERBNoSilentStatementRule,
6622
6999
  ERBNoSilentTagInAttributeNameRule,
6623
7000
  ERBNoStatementInScriptRule,
6624
7001
  ERBNoThenInControlFlowRule,
@@ -6650,8 +7027,8 @@ const rules = [
6650
7027
  HTMLAttributeValuesRequireQuotesRule,
6651
7028
  HTMLAvoidBothDisabledAndAriaDisabledRule,
6652
7029
  HTMLBodyOnlyElementsRule,
6653
- HTMLDetailsHasSummaryRule,
6654
7030
  HTMLBooleanAttributesNoValueRule,
7031
+ HTMLDetailsHasSummaryRule,
6655
7032
  HTMLHeadOnlyElementsRule,
6656
7033
  HTMLIframeHasTitleRule,
6657
7034
  HTMLImgRequireAltRule,
@@ -7209,8 +7586,90 @@ function ruleDocumentationUrl(ruleId) {
7209
7586
  return `${DOCS_BASE_URL}/${ruleId}`;
7210
7587
  }
7211
7588
 
7589
+ Object.defineProperty(exports, "HTML_BOOLEAN_ATTRIBUTES", {
7590
+ enumerable: true,
7591
+ get: function () { return core.HTML_BOOLEAN_ATTRIBUTES; }
7592
+ });
7593
+ Object.defineProperty(exports, "findAttributeByName", {
7594
+ enumerable: true,
7595
+ get: function () { return core.findAttributeByName; }
7596
+ });
7597
+ Object.defineProperty(exports, "getAttribute", {
7598
+ enumerable: true,
7599
+ get: function () { return core.getAttribute; }
7600
+ });
7601
+ Object.defineProperty(exports, "getAttributeName", {
7602
+ enumerable: true,
7603
+ get: function () { return core.getAttributeName; }
7604
+ });
7605
+ Object.defineProperty(exports, "getAttributeValue", {
7606
+ enumerable: true,
7607
+ get: function () { return core.getAttributeValue; }
7608
+ });
7609
+ Object.defineProperty(exports, "getAttributeValueNodes", {
7610
+ enumerable: true,
7611
+ get: function () { return core.getAttributeValueNodes; }
7612
+ });
7613
+ Object.defineProperty(exports, "getAttributeValueQuoteType", {
7614
+ enumerable: true,
7615
+ get: function () { return core.getAttributeValueQuoteType; }
7616
+ });
7617
+ Object.defineProperty(exports, "getAttributes", {
7618
+ enumerable: true,
7619
+ get: function () { return core.getAttributes; }
7620
+ });
7621
+ Object.defineProperty(exports, "getCombinedAttributeNameString", {
7622
+ enumerable: true,
7623
+ get: function () { return core.getCombinedAttributeNameString; }
7624
+ });
7625
+ Object.defineProperty(exports, "getStaticAttributeValue", {
7626
+ enumerable: true,
7627
+ get: function () { return core.getStaticAttributeValue; }
7628
+ });
7629
+ Object.defineProperty(exports, "getStaticAttributeValueContent", {
7630
+ enumerable: true,
7631
+ get: function () { return core.getStaticAttributeValueContent; }
7632
+ });
7633
+ Object.defineProperty(exports, "getTagName", {
7634
+ enumerable: true,
7635
+ get: function () { return core.getTagName; }
7636
+ });
7637
+ Object.defineProperty(exports, "hasAttribute", {
7638
+ enumerable: true,
7639
+ get: function () { return core.hasAttribute; }
7640
+ });
7641
+ Object.defineProperty(exports, "hasAttributeValue", {
7642
+ enumerable: true,
7643
+ get: function () { return core.hasAttributeValue; }
7644
+ });
7645
+ Object.defineProperty(exports, "hasDynamicAttributeName", {
7646
+ enumerable: true,
7647
+ get: function () { return core.hasDynamicAttributeName; }
7648
+ });
7649
+ Object.defineProperty(exports, "hasDynamicAttributeValue", {
7650
+ enumerable: true,
7651
+ get: function () { return core.hasDynamicAttributeValue; }
7652
+ });
7653
+ Object.defineProperty(exports, "hasStaticAttributeValue", {
7654
+ enumerable: true,
7655
+ get: function () { return core.hasStaticAttributeValue; }
7656
+ });
7657
+ Object.defineProperty(exports, "hasStaticAttributeValueContent", {
7658
+ enumerable: true,
7659
+ get: function () { return core.hasStaticAttributeValueContent; }
7660
+ });
7661
+ Object.defineProperty(exports, "isAttributeValueQuoted", {
7662
+ enumerable: true,
7663
+ get: function () { return core.isAttributeValueQuoted; }
7664
+ });
7665
+ Object.defineProperty(exports, "isBooleanAttribute", {
7666
+ enumerable: true,
7667
+ get: function () { return core.isBooleanAttribute; }
7668
+ });
7212
7669
  exports.ABSTRACT_ARIA_ROLES = ABSTRACT_ARIA_ROLES;
7213
7670
  exports.ARIA_ATTRIBUTES = ARIA_ATTRIBUTES;
7671
+ exports.ActionViewNoSilentHelperRule = ActionViewNoSilentHelperRule;
7672
+ exports.ActionViewNoSilentRenderRule = ActionViewNoSilentRenderRule;
7214
7673
  exports.AttributeVisitorMixin = AttributeVisitorMixin;
7215
7674
  exports.BaseLexerRuleVisitor = BaseLexerRuleVisitor;
7216
7675
  exports.BaseRuleVisitor = BaseRuleVisitor;
@@ -7224,6 +7683,7 @@ exports.ERBCommentSyntax = ERBCommentSyntax;
7224
7683
  exports.ERBNoCaseNodeChildrenRule = ERBNoCaseNodeChildrenRule;
7225
7684
  exports.ERBNoConditionalOpenTagRule = ERBNoConditionalOpenTagRule;
7226
7685
  exports.ERBNoDuplicateBranchElementsRule = ERBNoDuplicateBranchElementsRule;
7686
+ exports.ERBNoEmptyControlFlowRule = ERBNoEmptyControlFlowRule;
7227
7687
  exports.ERBNoEmptyTagsRule = ERBNoEmptyTagsRule;
7228
7688
  exports.ERBNoExtraNewLineRule = ERBNoExtraNewLineRule;
7229
7689
  exports.ERBNoExtraWhitespaceRule = ERBNoExtraWhitespaceRule;
@@ -7234,6 +7694,7 @@ exports.ERBNoOutputControlFlowRule = ERBNoOutputControlFlowRule;
7234
7694
  exports.ERBNoOutputInAttributeNameRule = ERBNoOutputInAttributeNameRule;
7235
7695
  exports.ERBNoOutputInAttributePositionRule = ERBNoOutputInAttributePositionRule;
7236
7696
  exports.ERBNoRawOutputInAttributeValueRule = ERBNoRawOutputInAttributeValueRule;
7697
+ exports.ERBNoSilentStatementRule = ERBNoSilentStatementRule;
7237
7698
  exports.ERBNoSilentTagInAttributeNameRule = ERBNoSilentTagInAttributeNameRule;
7238
7699
  exports.ERBNoStatementInScriptRule = ERBNoStatementInScriptRule;
7239
7700
  exports.ERBNoThenInControlFlowRule = ERBNoThenInControlFlowRule;
@@ -7286,7 +7747,6 @@ exports.HTMLNoUnderscoresInAttributeNamesRule = HTMLNoUnderscoresInAttributeName
7286
7747
  exports.HTMLRequireClosingTagsRule = HTMLRequireClosingTagsRule;
7287
7748
  exports.HTMLTagNameLowercaseRule = HTMLTagNameLowercaseRule;
7288
7749
  exports.HTML_BLOCK_ELEMENTS = HTML_BLOCK_ELEMENTS;
7289
- exports.HTML_BOOLEAN_ATTRIBUTES = HTML_BOOLEAN_ATTRIBUTES;
7290
7750
  exports.HTML_INLINE_ELEMENTS = HTML_INLINE_ELEMENTS;
7291
7751
  exports.HTML_ONLY_TAG_NAMES = HTML_ONLY_TAG_NAMES;
7292
7752
  exports.HTML_VOID_ELEMENTS = HTML_VOID_ELEMENTS;
@@ -7316,7 +7776,6 @@ exports.hasBalancedParentheses = hasBalancedParentheses;
7316
7776
  exports.isBlockElement = isBlockElement;
7317
7777
  exports.isBodyOnlyTag = isBodyOnlyTag;
7318
7778
  exports.isBodyTag = isBodyTag;
7319
- exports.isBooleanAttribute = isBooleanAttribute;
7320
7779
  exports.isDocumentOnlyTag = isDocumentOnlyTag;
7321
7780
  exports.isHeadAndBodyTag = isHeadAndBodyTag;
7322
7781
  exports.isHeadOnlyTag = isHeadOnlyTag;