@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.
- package/README.md +2 -2
- package/dist/herb-lint.js +1525 -98
- package/dist/herb-lint.js.map +1 -1
- package/dist/index.cjs +546 -87
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +465 -87
- package/dist/index.js.map +1 -1
- package/dist/lint-worker.js +1523 -96
- package/dist/lint-worker.js.map +1 -1
- package/dist/loader.cjs +1078 -94
- package/dist/loader.cjs.map +1 -1
- package/dist/loader.js +1057 -95
- 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 +14 -23
- 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 +8 -11
- 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 +15 -24
- 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
|
/**
|
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3275
|
-
|
|
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;
|