@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.
- package/README.md +2 -2
- package/dist/herb-lint.js +1512 -85
- package/dist/herb-lint.js.map +1 -1
- package/dist/index.cjs +538 -72
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +465 -74
- package/dist/index.js.map +1 -1
- package/dist/lint-worker.js +1510 -83
- package/dist/lint-worker.js.map +1 -1
- package/dist/loader.cjs +1065 -81
- package/dist/loader.cjs.map +1 -1
- package/dist/loader.js +1044 -82
- package/dist/loader.js.map +1 -1
- package/dist/rules/actionview-no-silent-render.js +31 -0
- package/dist/rules/actionview-no-silent-render.js.map +1 -0
- package/dist/rules/erb-no-case-node-children.js +3 -1
- package/dist/rules/erb-no-case-node-children.js.map +1 -1
- package/dist/rules/erb-no-duplicate-branch-elements.js +95 -11
- package/dist/rules/erb-no-duplicate-branch-elements.js.map +1 -1
- package/dist/rules/erb-no-empty-control-flow.js +190 -0
- package/dist/rules/erb-no-empty-control-flow.js.map +1 -0
- package/dist/rules/erb-no-silent-statement.js +44 -0
- package/dist/rules/erb-no-silent-statement.js.map +1 -0
- package/dist/rules/erb-no-unsafe-script-interpolation.js +37 -3
- package/dist/rules/erb-no-unsafe-script-interpolation.js.map +1 -1
- package/dist/rules/html-allowed-script-type.js +1 -1
- package/dist/rules/html-allowed-script-type.js.map +1 -1
- package/dist/rules/index.js +20 -16
- package/dist/rules/index.js.map +1 -1
- package/dist/rules/rule-utils.js +13 -10
- package/dist/rules/rule-utils.js.map +1 -1
- package/dist/rules.js +8 -2
- package/dist/rules.js.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/rules/actionview-no-silent-render.d.ts +9 -0
- package/dist/types/rules/erb-no-duplicate-branch-elements.d.ts +1 -0
- package/dist/types/rules/erb-no-empty-control-flow.d.ts +8 -0
- package/dist/types/rules/erb-no-silent-statement.d.ts +9 -0
- package/dist/types/rules/erb-no-unsafe-script-interpolation.d.ts +2 -1
- package/dist/types/rules/index.d.ts +20 -16
- package/dist/types/rules/rule-utils.d.ts +7 -6
- package/dist/types/types.d.ts +4 -3
- package/dist/types.js +6 -3
- package/dist/types.js.map +1 -1
- package/docs/rules/README.md +3 -0
- package/docs/rules/actionview-no-silent-render.md +47 -0
- package/docs/rules/erb-no-empty-control-flow.md +83 -0
- package/docs/rules/erb-no-silent-statement.md +53 -0
- package/docs/rules/erb-no-unsafe-script-interpolation.md +70 -3
- package/package.json +8 -8
- package/src/index.ts +21 -0
- package/src/rules/actionview-no-silent-render.ts +44 -0
- package/src/rules/erb-no-case-node-children.ts +3 -1
- package/src/rules/erb-no-duplicate-branch-elements.ts +130 -14
- package/src/rules/erb-no-empty-control-flow.ts +255 -0
- package/src/rules/erb-no-silent-statement.ts +58 -0
- package/src/rules/erb-no-unsafe-script-interpolation.ts +51 -5
- package/src/rules/html-allowed-script-type.ts +1 -1
- package/src/rules/index.ts +21 -16
- package/src/rules/rule-utils.ts +14 -10
- package/src/rules.ts +8 -2
- 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.
|
|
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.
|
|
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
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3275
|
-
|
|
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;
|