@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.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import picomatch from 'picomatch';
|
|
2
|
-
import { getNodesBeforePosition, getNodesAfterPosition, filterNodes, ERBContentNode, isERBOutputNode, isERBIfNode, isERBUnlessNode, isERBElseNode, isHTMLTextNode, Visitor, isToken, isParseResult, forEachAttribute, getAttributeName,
|
|
2
|
+
import { getNodesBeforePosition, getNodesAfterPosition, filterNodes, ERBContentNode, isERBOutputNode, isERBIfNode, isERBUnlessNode, isERBElseNode, isHTMLTextNode, Visitor, isToken, isParseResult, forEachAttribute, getAttributeName, hasDynamicAttributeName, getStaticAttributeValue, getAttributeValueNodes, hasERBOutput, isEffectivelyStatic, getValidatableStaticContent, getAttributeValue, getCombinedAttributeNameString, Location, Position, isERBOpenTagNode, isERBNode, isWhitespaceNode, isCommentNode, isLiteralNode, findParentArray, isHTMLOpenTagNode, isERBCaseNode, isPureWhitespaceNode, isERBWhenNode, isHTMLElementNode, isEquivalentElement, removeNodeFromArray, createLiteral, replaceNodeWithBody, HTMLElementNode, PrismVisitor, splitLiteralsAtWhitespace, groupNodesByClass, filterERBContentNodes, getTagLocalName, getAttribute, isERBCommentNode, getAttributes, findAttributeByName, isNode, LiteralNode, didyoumean, hasAttributeValue, isRubyLiteralNode, filterHTMLAttributeNodes, filterLiteralNodes, getAttributeValueQuoteType, Token, hasAttribute, isHTMLAttributeValueNode, isERBContentNode, getStaticAttributeName, isERBControlFlowNode, HTMLCloseTagNode, getTagName, createWhitespaceNode, filterWhitespaceNodes, getStaticContentFromNodes, getOpenTag, isHTMLCloseTagNode, HTMLOpenTagNode, DEFAULT_PARSER_OPTIONS } from '@herb-tools/core';
|
|
3
|
+
export { findAttributeByName, getAttribute, getAttributeName, getAttributeValue, getAttributeValueNodes, getAttributeValueQuoteType, getAttributes, getCombinedAttributeNameString, getStaticAttributeValue, getStaticAttributeValueContent, getTagName, hasAttribute, hasAttributeValue, hasDynamicAttributeName, hasDynamicAttributeValue, hasStaticAttributeValue, hasStaticAttributeValueContent, isAttributeValueQuoted } from '@herb-tools/core';
|
|
3
4
|
|
|
4
5
|
class PrintContext {
|
|
5
6
|
output = "";
|
|
@@ -439,6 +440,12 @@ class IdentityPrinter extends Printer {
|
|
|
439
440
|
this.visit(node.end_node);
|
|
440
441
|
}
|
|
441
442
|
}
|
|
443
|
+
visitERBRenderNode(node) {
|
|
444
|
+
this.printERBNode(node);
|
|
445
|
+
}
|
|
446
|
+
visitRubyRenderLocalNode(_node) {
|
|
447
|
+
// extracted metadata, nothing to print
|
|
448
|
+
}
|
|
442
449
|
visitERBYieldNode(node) {
|
|
443
450
|
this.printERBNode(node);
|
|
444
451
|
}
|
|
@@ -989,7 +996,7 @@ class ParserRule {
|
|
|
989
996
|
get parserOptions() {
|
|
990
997
|
return DEFAULT_LINTER_PARSER_OPTIONS;
|
|
991
998
|
}
|
|
992
|
-
createOffense(message, location, autofixContext, severity) {
|
|
999
|
+
createOffense(message, location, autofixContext, severity, tags) {
|
|
993
1000
|
return {
|
|
994
1001
|
rule: this.ruleName,
|
|
995
1002
|
code: this.ruleName,
|
|
@@ -998,6 +1005,7 @@ class ParserRule {
|
|
|
998
1005
|
location,
|
|
999
1006
|
autofixContext,
|
|
1000
1007
|
severity,
|
|
1008
|
+
tags,
|
|
1001
1009
|
};
|
|
1002
1010
|
}
|
|
1003
1011
|
}
|
|
@@ -1017,7 +1025,7 @@ class LexerRule {
|
|
|
1017
1025
|
get defaultConfig() {
|
|
1018
1026
|
return DEFAULT_RULE_CONFIG;
|
|
1019
1027
|
}
|
|
1020
|
-
createOffense(message, location, autofixContext, severity) {
|
|
1028
|
+
createOffense(message, location, autofixContext, severity, tags) {
|
|
1021
1029
|
return {
|
|
1022
1030
|
rule: this.ruleName,
|
|
1023
1031
|
code: this.ruleName,
|
|
@@ -1026,6 +1034,7 @@ class LexerRule {
|
|
|
1026
1034
|
location,
|
|
1027
1035
|
autofixContext,
|
|
1028
1036
|
severity,
|
|
1037
|
+
tags,
|
|
1029
1038
|
};
|
|
1030
1039
|
}
|
|
1031
1040
|
}
|
|
@@ -1051,7 +1060,7 @@ class SourceRule {
|
|
|
1051
1060
|
get defaultConfig() {
|
|
1052
1061
|
return DEFAULT_RULE_CONFIG;
|
|
1053
1062
|
}
|
|
1054
|
-
createOffense(message, location, autofixContext, severity) {
|
|
1063
|
+
createOffense(message, location, autofixContext, severity, tags) {
|
|
1055
1064
|
return {
|
|
1056
1065
|
rule: this.ruleName,
|
|
1057
1066
|
code: this.ruleName,
|
|
@@ -1060,6 +1069,7 @@ class SourceRule {
|
|
|
1060
1069
|
location,
|
|
1061
1070
|
autofixContext,
|
|
1062
1071
|
severity,
|
|
1072
|
+
tags,
|
|
1063
1073
|
};
|
|
1064
1074
|
}
|
|
1065
1075
|
}
|
|
@@ -1085,7 +1095,7 @@ class BaseRuleVisitor extends Visitor {
|
|
|
1085
1095
|
* Helper method to create an unbound lint offense (without severity).
|
|
1086
1096
|
* The Linter will bind severity based on the rule's config.
|
|
1087
1097
|
*/
|
|
1088
|
-
createOffense(message, location, autofixContext, severity) {
|
|
1098
|
+
createOffense(message, location, autofixContext, severity, tags) {
|
|
1089
1099
|
return {
|
|
1090
1100
|
rule: this.ruleName,
|
|
1091
1101
|
code: this.ruleName,
|
|
@@ -1094,13 +1104,14 @@ class BaseRuleVisitor extends Visitor {
|
|
|
1094
1104
|
location,
|
|
1095
1105
|
autofixContext,
|
|
1096
1106
|
severity,
|
|
1107
|
+
tags,
|
|
1097
1108
|
};
|
|
1098
1109
|
}
|
|
1099
1110
|
/**
|
|
1100
1111
|
* Helper method to add an offense to the offenses array
|
|
1101
1112
|
*/
|
|
1102
|
-
addOffense(message, location, autofixContext, severity) {
|
|
1103
|
-
this.offenses.push(this.createOffense(message, location, autofixContext, severity));
|
|
1113
|
+
addOffense(message, location, autofixContext, severity, tags) {
|
|
1114
|
+
this.offenses.push(this.createOffense(message, location, autofixContext, severity, tags));
|
|
1104
1115
|
}
|
|
1105
1116
|
}
|
|
1106
1117
|
/**
|
|
@@ -1376,7 +1387,7 @@ class AttributeVisitorMixin extends BaseRuleVisitor {
|
|
|
1376
1387
|
forEachAttribute(node, (attributeNode) => {
|
|
1377
1388
|
const staticAttributeName = getAttributeName(attributeNode);
|
|
1378
1389
|
const originalAttributeName = getAttributeName(attributeNode, false) || "";
|
|
1379
|
-
const isDynamicName =
|
|
1390
|
+
const isDynamicName = hasDynamicAttributeName(attributeNode);
|
|
1380
1391
|
const staticAttributeValue = getStaticAttributeValue(attributeNode);
|
|
1381
1392
|
const valueNodes = getAttributeValueNodes(attributeNode);
|
|
1382
1393
|
const hasOutputERB = hasERBOutput(valueNodes);
|
|
@@ -1453,7 +1464,7 @@ class BaseLexerRuleVisitor {
|
|
|
1453
1464
|
* Helper method to create an unbound lint offense (without severity).
|
|
1454
1465
|
* The Linter will bind severity based on the rule's config.
|
|
1455
1466
|
*/
|
|
1456
|
-
createOffense(message, location, autofixContext, severity) {
|
|
1467
|
+
createOffense(message, location, autofixContext, severity, tags) {
|
|
1457
1468
|
return {
|
|
1458
1469
|
rule: this.ruleName,
|
|
1459
1470
|
code: this.ruleName,
|
|
@@ -1462,13 +1473,14 @@ class BaseLexerRuleVisitor {
|
|
|
1462
1473
|
location,
|
|
1463
1474
|
autofixContext,
|
|
1464
1475
|
severity,
|
|
1476
|
+
tags,
|
|
1465
1477
|
};
|
|
1466
1478
|
}
|
|
1467
1479
|
/**
|
|
1468
1480
|
* Helper method to add an offense to the offenses array
|
|
1469
1481
|
*/
|
|
1470
|
-
addOffense(message, location, autofixContext, severity) {
|
|
1471
|
-
this.offenses.push(this.createOffense(message, location, autofixContext, severity));
|
|
1482
|
+
addOffense(message, location, autofixContext, severity, tags) {
|
|
1483
|
+
this.offenses.push(this.createOffense(message, location, autofixContext, severity, tags));
|
|
1472
1484
|
}
|
|
1473
1485
|
/**
|
|
1474
1486
|
* Main entry point for lexer rule visitors
|
|
@@ -1509,7 +1521,7 @@ class BaseSourceRuleVisitor {
|
|
|
1509
1521
|
* Helper method to create an unbound lint offense (without severity).
|
|
1510
1522
|
* The Linter will bind severity based on the rule's config.
|
|
1511
1523
|
*/
|
|
1512
|
-
createOffense(message, location, autofixContext, severity) {
|
|
1524
|
+
createOffense(message, location, autofixContext, severity, tags) {
|
|
1513
1525
|
return {
|
|
1514
1526
|
rule: this.ruleName,
|
|
1515
1527
|
code: this.ruleName,
|
|
@@ -1518,13 +1530,14 @@ class BaseSourceRuleVisitor {
|
|
|
1518
1530
|
location,
|
|
1519
1531
|
autofixContext,
|
|
1520
1532
|
severity,
|
|
1533
|
+
tags,
|
|
1521
1534
|
};
|
|
1522
1535
|
}
|
|
1523
1536
|
/**
|
|
1524
1537
|
* Helper method to add an offense to the offenses array
|
|
1525
1538
|
*/
|
|
1526
|
-
addOffense(message, location, autofixContext, severity) {
|
|
1527
|
-
this.offenses.push(this.createOffense(message, location, autofixContext, severity));
|
|
1539
|
+
addOffense(message, location, autofixContext, severity, tags) {
|
|
1540
|
+
this.offenses.push(this.createOffense(message, location, autofixContext, severity, tags));
|
|
1528
1541
|
}
|
|
1529
1542
|
/**
|
|
1530
1543
|
* Main entry point for source rule visitors
|
|
@@ -1874,6 +1887,34 @@ class ActionViewNoSilentHelperRule extends ParserRule {
|
|
|
1874
1887
|
}
|
|
1875
1888
|
}
|
|
1876
1889
|
|
|
1890
|
+
class ActionViewNoSilentRenderVisitor extends BaseRuleVisitor {
|
|
1891
|
+
visitERBRenderNode(node) {
|
|
1892
|
+
if (!isERBOutputNode(node)) {
|
|
1893
|
+
this.addOffense(`Avoid using \`${node.tag_opening?.value} %>\` with \`render\`. Use \`<%= %>\` to ensure the rendered content is output.`, node.location);
|
|
1894
|
+
}
|
|
1895
|
+
this.visitChildNodes(node);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
class ActionViewNoSilentRenderRule extends ParserRule {
|
|
1899
|
+
static ruleName = "actionview-no-silent-render";
|
|
1900
|
+
get defaultConfig() {
|
|
1901
|
+
return {
|
|
1902
|
+
enabled: true,
|
|
1903
|
+
severity: "error"
|
|
1904
|
+
};
|
|
1905
|
+
}
|
|
1906
|
+
get parserOptions() {
|
|
1907
|
+
return {
|
|
1908
|
+
render_nodes: true,
|
|
1909
|
+
};
|
|
1910
|
+
}
|
|
1911
|
+
check(result, context) {
|
|
1912
|
+
const visitor = new ActionViewNoSilentRenderVisitor(this.ruleName, context);
|
|
1913
|
+
visitor.visit(result.value);
|
|
1914
|
+
return visitor.offenses;
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1877
1918
|
class ERBCommentSyntaxVisitor extends BaseRuleVisitor {
|
|
1878
1919
|
visitERBContentNode(node) {
|
|
1879
1920
|
const content = node.content?.value || "";
|
|
@@ -1938,7 +1979,9 @@ class ERBNoCaseNodeChildrenVisitor extends BaseRuleVisitor {
|
|
|
1938
1979
|
for (const child of node.children) {
|
|
1939
1980
|
if (!this.isAllowedContent(child)) {
|
|
1940
1981
|
const childCode = IdentityPrinter.print(child).trim();
|
|
1941
|
-
this.
|
|
1982
|
+
const offense = this.createOffense(`Do not place \`${childCode}\` between \`${caseCode}\` and \`${conditionCode}\`. Content here is not part of any branch and will not be rendered.`, child.location);
|
|
1983
|
+
offense.tags = ["unnecessary"];
|
|
1984
|
+
this.offenses.push(offense);
|
|
1942
1985
|
}
|
|
1943
1986
|
}
|
|
1944
1987
|
}
|
|
@@ -1968,6 +2011,193 @@ class ERBNoCaseNodeChildrenRule extends ParserRule {
|
|
|
1968
2011
|
}
|
|
1969
2012
|
}
|
|
1970
2013
|
|
|
2014
|
+
class ERBNoEmptyControlFlowVisitor extends BaseRuleVisitor {
|
|
2015
|
+
processedIfNodes = new Set();
|
|
2016
|
+
processedElseNodes = new Set();
|
|
2017
|
+
visitERBIfNode(node) {
|
|
2018
|
+
if (this.processedIfNodes.has(node)) {
|
|
2019
|
+
return;
|
|
2020
|
+
}
|
|
2021
|
+
this.markIfChainAsProcessed(node);
|
|
2022
|
+
this.markElseNodesInIfChain(node);
|
|
2023
|
+
const entireChainEmpty = this.isEntireIfChainEmpty(node);
|
|
2024
|
+
if (entireChainEmpty) {
|
|
2025
|
+
this.addEmptyBlockOffense(node, node.statements, "if");
|
|
2026
|
+
}
|
|
2027
|
+
else {
|
|
2028
|
+
this.checkIfChainParts(node);
|
|
2029
|
+
}
|
|
2030
|
+
this.visitChildNodes(node);
|
|
2031
|
+
}
|
|
2032
|
+
visitERBElseNode(node) {
|
|
2033
|
+
if (this.processedElseNodes.has(node)) {
|
|
2034
|
+
this.visitChildNodes(node);
|
|
2035
|
+
return;
|
|
2036
|
+
}
|
|
2037
|
+
this.addEmptyBlockOffense(node, node.statements, "else");
|
|
2038
|
+
this.visitChildNodes(node);
|
|
2039
|
+
}
|
|
2040
|
+
visitERBUnlessNode(node) {
|
|
2041
|
+
const unlessHasContent = this.statementsHaveContent(node.statements);
|
|
2042
|
+
const elseHasContent = node.else_clause && this.statementsHaveContent(node.else_clause.statements);
|
|
2043
|
+
if (node.else_clause) {
|
|
2044
|
+
this.processedElseNodes.add(node.else_clause);
|
|
2045
|
+
}
|
|
2046
|
+
const entireBlockEmpty = !unlessHasContent && !elseHasContent;
|
|
2047
|
+
if (entireBlockEmpty) {
|
|
2048
|
+
this.addEmptyBlockOffense(node, node.statements, "unless");
|
|
2049
|
+
}
|
|
2050
|
+
else {
|
|
2051
|
+
if (!unlessHasContent) {
|
|
2052
|
+
this.addEmptyBlockOffenseWithEnd(node, node.statements, "unless", node.else_clause);
|
|
2053
|
+
}
|
|
2054
|
+
if (node.else_clause && !elseHasContent) {
|
|
2055
|
+
this.addEmptyBlockOffense(node.else_clause, node.else_clause.statements, "else");
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
this.visitChildNodes(node);
|
|
2059
|
+
}
|
|
2060
|
+
visitERBForNode(node) {
|
|
2061
|
+
this.addEmptyBlockOffense(node, node.statements, "for");
|
|
2062
|
+
this.visitChildNodes(node);
|
|
2063
|
+
}
|
|
2064
|
+
visitERBWhileNode(node) {
|
|
2065
|
+
this.addEmptyBlockOffense(node, node.statements, "while");
|
|
2066
|
+
this.visitChildNodes(node);
|
|
2067
|
+
}
|
|
2068
|
+
visitERBUntilNode(node) {
|
|
2069
|
+
this.addEmptyBlockOffense(node, node.statements, "until");
|
|
2070
|
+
this.visitChildNodes(node);
|
|
2071
|
+
}
|
|
2072
|
+
visitERBWhenNode(node) {
|
|
2073
|
+
if (!node.then_keyword) {
|
|
2074
|
+
this.addEmptyBlockOffense(node, node.statements, "when");
|
|
2075
|
+
}
|
|
2076
|
+
this.visitChildNodes(node);
|
|
2077
|
+
}
|
|
2078
|
+
visitERBInNode(node) {
|
|
2079
|
+
if (!node.then_keyword) {
|
|
2080
|
+
this.addEmptyBlockOffense(node, node.statements, "in");
|
|
2081
|
+
}
|
|
2082
|
+
this.visitChildNodes(node);
|
|
2083
|
+
}
|
|
2084
|
+
visitERBBeginNode(node) {
|
|
2085
|
+
this.addEmptyBlockOffense(node, node.statements, "begin");
|
|
2086
|
+
this.visitChildNodes(node);
|
|
2087
|
+
}
|
|
2088
|
+
visitERBRescueNode(node) {
|
|
2089
|
+
this.addEmptyBlockOffense(node, node.statements, "rescue");
|
|
2090
|
+
this.visitChildNodes(node);
|
|
2091
|
+
}
|
|
2092
|
+
visitERBEnsureNode(node) {
|
|
2093
|
+
this.addEmptyBlockOffense(node, node.statements, "ensure");
|
|
2094
|
+
this.visitChildNodes(node);
|
|
2095
|
+
}
|
|
2096
|
+
visitERBBlockNode(node) {
|
|
2097
|
+
this.addEmptyBlockOffense(node, node.body, "do");
|
|
2098
|
+
this.visitChildNodes(node);
|
|
2099
|
+
}
|
|
2100
|
+
addEmptyBlockOffense(node, statements, blockType) {
|
|
2101
|
+
this.addEmptyBlockOffenseWithEnd(node, statements, blockType, null);
|
|
2102
|
+
}
|
|
2103
|
+
addEmptyBlockOffenseWithEnd(node, statements, blockType, subsequentNode) {
|
|
2104
|
+
if (this.statementsHaveContent(statements)) {
|
|
2105
|
+
return;
|
|
2106
|
+
}
|
|
2107
|
+
const startLocation = node.location.start;
|
|
2108
|
+
const endLocation = subsequentNode
|
|
2109
|
+
? subsequentNode.location.start
|
|
2110
|
+
: node.location.end;
|
|
2111
|
+
const location = Location.from(startLocation.line, startLocation.column, endLocation.line, endLocation.column);
|
|
2112
|
+
const offense = this.createOffense(`Empty ${blockType} block: this control flow statement has no content`, location);
|
|
2113
|
+
offense.tags = ["unnecessary"];
|
|
2114
|
+
this.offenses.push(offense);
|
|
2115
|
+
}
|
|
2116
|
+
statementsHaveContent(statements) {
|
|
2117
|
+
return statements.some(statement => {
|
|
2118
|
+
if (isHTMLTextNode(statement)) {
|
|
2119
|
+
return statement.content.trim() !== "";
|
|
2120
|
+
}
|
|
2121
|
+
return true;
|
|
2122
|
+
});
|
|
2123
|
+
}
|
|
2124
|
+
markIfChainAsProcessed(node) {
|
|
2125
|
+
this.processedIfNodes.add(node);
|
|
2126
|
+
this.traverseSubsequentNodes(node.subsequent, (current) => {
|
|
2127
|
+
if (isERBIfNode(current)) {
|
|
2128
|
+
this.processedIfNodes.add(current);
|
|
2129
|
+
}
|
|
2130
|
+
});
|
|
2131
|
+
}
|
|
2132
|
+
markElseNodesInIfChain(node) {
|
|
2133
|
+
this.traverseSubsequentNodes(node.subsequent, (current) => {
|
|
2134
|
+
if (isERBElseNode(current)) {
|
|
2135
|
+
this.processedElseNodes.add(current);
|
|
2136
|
+
}
|
|
2137
|
+
});
|
|
2138
|
+
}
|
|
2139
|
+
traverseSubsequentNodes(startNode, callback) {
|
|
2140
|
+
let current = startNode;
|
|
2141
|
+
while (current) {
|
|
2142
|
+
if (isERBIfNode(current)) {
|
|
2143
|
+
callback(current);
|
|
2144
|
+
current = current.subsequent;
|
|
2145
|
+
}
|
|
2146
|
+
else if (isERBElseNode(current)) {
|
|
2147
|
+
callback(current);
|
|
2148
|
+
break;
|
|
2149
|
+
}
|
|
2150
|
+
else {
|
|
2151
|
+
break;
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
checkIfChainParts(node) {
|
|
2156
|
+
if (!this.statementsHaveContent(node.statements)) {
|
|
2157
|
+
this.addEmptyBlockOffenseWithEnd(node, node.statements, "if", node.subsequent);
|
|
2158
|
+
}
|
|
2159
|
+
this.traverseSubsequentNodes(node.subsequent, (current) => {
|
|
2160
|
+
if (this.statementsHaveContent(current.statements)) {
|
|
2161
|
+
return;
|
|
2162
|
+
}
|
|
2163
|
+
const blockType = isERBIfNode(current) ? "elsif" : "else";
|
|
2164
|
+
const nextSubsequent = isERBIfNode(current) ? current.subsequent : null;
|
|
2165
|
+
if (nextSubsequent) {
|
|
2166
|
+
this.addEmptyBlockOffenseWithEnd(current, current.statements, blockType, nextSubsequent);
|
|
2167
|
+
}
|
|
2168
|
+
else {
|
|
2169
|
+
this.addEmptyBlockOffense(current, current.statements, blockType);
|
|
2170
|
+
}
|
|
2171
|
+
});
|
|
2172
|
+
}
|
|
2173
|
+
isEntireIfChainEmpty(node) {
|
|
2174
|
+
if (this.statementsHaveContent(node.statements)) {
|
|
2175
|
+
return false;
|
|
2176
|
+
}
|
|
2177
|
+
let hasContent = false;
|
|
2178
|
+
this.traverseSubsequentNodes(node.subsequent, (current) => {
|
|
2179
|
+
if (this.statementsHaveContent(current.statements)) {
|
|
2180
|
+
hasContent = true;
|
|
2181
|
+
}
|
|
2182
|
+
});
|
|
2183
|
+
return !hasContent;
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
class ERBNoEmptyControlFlowRule extends ParserRule {
|
|
2187
|
+
static ruleName = "erb-no-empty-control-flow";
|
|
2188
|
+
get defaultConfig() {
|
|
2189
|
+
return {
|
|
2190
|
+
enabled: true,
|
|
2191
|
+
severity: "hint"
|
|
2192
|
+
};
|
|
2193
|
+
}
|
|
2194
|
+
check(result, context) {
|
|
2195
|
+
const visitor = new ERBNoEmptyControlFlowVisitor(this.ruleName, context);
|
|
2196
|
+
visitor.visit(result.value);
|
|
2197
|
+
return visitor.offenses;
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
|
|
1971
2201
|
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
|
|
1972
2202
|
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
|
|
1973
2203
|
function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
|
|
@@ -2126,6 +2356,15 @@ class ERBNoConditionalOpenTagRule extends ParserRule {
|
|
|
2126
2356
|
function getSignificantNodes(statements) {
|
|
2127
2357
|
return statements.filter(node => !isPureWhitespaceNode(node));
|
|
2128
2358
|
}
|
|
2359
|
+
function trimWhitespaceNodes(nodes) {
|
|
2360
|
+
let start = 0;
|
|
2361
|
+
let end = nodes.length;
|
|
2362
|
+
while (start < end && isPureWhitespaceNode(nodes[start]))
|
|
2363
|
+
start++;
|
|
2364
|
+
while (end > start && isPureWhitespaceNode(nodes[end - 1]))
|
|
2365
|
+
end--;
|
|
2366
|
+
return nodes.slice(start, end);
|
|
2367
|
+
}
|
|
2129
2368
|
function allEquivalentElements(nodes) {
|
|
2130
2369
|
if (nodes.length < 2)
|
|
2131
2370
|
return false;
|
|
@@ -2243,9 +2482,19 @@ class ERBNoDuplicateBranchElementsVisitor extends BaseRuleVisitor {
|
|
|
2243
2482
|
if (isERBIfNode(node)) {
|
|
2244
2483
|
this.markSubsequentIfNodesAsProcessed(node);
|
|
2245
2484
|
}
|
|
2485
|
+
if (this.allBranchesIdentical(branches)) {
|
|
2486
|
+
this.addOffense("All branches of this conditional have identical content. The conditional can be removed.", node.location, { node: node, allIdentical: true }, "warning");
|
|
2487
|
+
return;
|
|
2488
|
+
}
|
|
2246
2489
|
const state = { isFirstOffense: true };
|
|
2247
2490
|
this.checkBranches(branches, node, state);
|
|
2248
2491
|
}
|
|
2492
|
+
allBranchesIdentical(branches) {
|
|
2493
|
+
if (branches.length < 2)
|
|
2494
|
+
return false;
|
|
2495
|
+
const first = branches[0].map(node => IdentityPrinter.print(node)).join("");
|
|
2496
|
+
return branches.slice(1).every(branch => branch.map(node => IdentityPrinter.print(node)).join("") === first);
|
|
2497
|
+
}
|
|
2249
2498
|
markSubsequentIfNodesAsProcessed(node) {
|
|
2250
2499
|
let current = node.subsequent;
|
|
2251
2500
|
while (current) {
|
|
@@ -2279,11 +2528,23 @@ class ERBNoDuplicateBranchElementsVisitor extends BaseRuleVisitor {
|
|
|
2279
2528
|
const bodiesMatch = elements.every(element => IdentityPrinter.print(element) === IdentityPrinter.print(elements[0]));
|
|
2280
2529
|
for (const element of elements) {
|
|
2281
2530
|
const printed = IdentityPrinter.print(element.open_tag);
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2531
|
+
if (bodiesMatch) {
|
|
2532
|
+
const autofixContext = state.isFirstOffense
|
|
2533
|
+
? { node: conditionalNode }
|
|
2534
|
+
: undefined;
|
|
2535
|
+
this.addOffense(`The \`${printed}\` element is duplicated across all branches of this conditional and can be moved outside.`, element.location, autofixContext);
|
|
2536
|
+
state.isFirstOffense = false;
|
|
2537
|
+
}
|
|
2538
|
+
else {
|
|
2539
|
+
const autofixContext = state.isFirstOffense
|
|
2540
|
+
? { node: conditionalNode }
|
|
2541
|
+
: undefined;
|
|
2542
|
+
const tagNameLocation = isHTMLOpenTagNode(element.open_tag) && element.open_tag.tag_name?.location
|
|
2543
|
+
? element.open_tag.tag_name.location
|
|
2544
|
+
: element?.open_tag?.location || element.location;
|
|
2545
|
+
this.addOffense(`The \`${printed}\` tag is repeated across all branches with different content. Consider extracting the shared tag outside the conditional.`, tagNameLocation, autofixContext, "hint");
|
|
2546
|
+
state.isFirstOffense = false;
|
|
2547
|
+
}
|
|
2287
2548
|
}
|
|
2288
2549
|
if (!bodiesMatch && bodies.every(body => body.length > 0)) {
|
|
2289
2550
|
this.checkBranches(bodies, conditionalNode, state);
|
|
@@ -2312,6 +2573,15 @@ class ERBNoDuplicateBranchElementsRule extends ParserRule {
|
|
|
2312
2573
|
const branches = collectBranches(conditionalNode);
|
|
2313
2574
|
if (!branches)
|
|
2314
2575
|
return null;
|
|
2576
|
+
if (offense.autofixContext.allIdentical) {
|
|
2577
|
+
const parentInfo = findParentArray(result.value, conditionalNode);
|
|
2578
|
+
if (!parentInfo)
|
|
2579
|
+
return null;
|
|
2580
|
+
const { array: parentArray, index: conditionalIndex } = parentInfo;
|
|
2581
|
+
const firstBranchContent = trimWhitespaceNodes(branches[0]);
|
|
2582
|
+
parentArray.splice(conditionalIndex, 1, ...firstBranchContent);
|
|
2583
|
+
return result;
|
|
2584
|
+
}
|
|
2315
2585
|
const significantBranches = branches.map(getSignificantNodes);
|
|
2316
2586
|
if (significantBranches.some(branch => branch.length === 0))
|
|
2317
2587
|
return null;
|
|
@@ -2325,23 +2595,51 @@ class ERBNoDuplicateBranchElementsRule extends ParserRule {
|
|
|
2325
2595
|
return null;
|
|
2326
2596
|
let { array: parentArray, index: conditionalIndex } = parentInfo;
|
|
2327
2597
|
let hasWrapped = false;
|
|
2598
|
+
let didMutate = false;
|
|
2599
|
+
let failedToHoistPrefix = false;
|
|
2600
|
+
let hoistedBefore = false;
|
|
2328
2601
|
const hoistElement = (elements, position) => {
|
|
2602
|
+
const actualPosition = (position === "before" && failedToHoistPrefix) ? "after" : position;
|
|
2329
2603
|
const bodiesMatch = elements.every(element => IdentityPrinter.print(element) === IdentityPrinter.print(elements[0]));
|
|
2330
2604
|
if (bodiesMatch) {
|
|
2605
|
+
if (actualPosition === "after") {
|
|
2606
|
+
const currentLengths = branches.map(b => getSignificantNodes(b).length);
|
|
2607
|
+
if (currentLengths.some(l => l !== currentLengths[0]))
|
|
2608
|
+
return;
|
|
2609
|
+
}
|
|
2610
|
+
if (actualPosition === "after" && position === "before") {
|
|
2611
|
+
const isAtEnd = branches.every((branch, index) => {
|
|
2612
|
+
const nodes = getSignificantNodes(branch);
|
|
2613
|
+
return nodes.length > 0 && nodes[nodes.length - 1] === elements[index];
|
|
2614
|
+
});
|
|
2615
|
+
if (!isAtEnd)
|
|
2616
|
+
return;
|
|
2617
|
+
}
|
|
2331
2618
|
for (let i = 0; i < branches.length; i++) {
|
|
2332
2619
|
removeNodeFromArray(branches[i], elements[i]);
|
|
2333
2620
|
}
|
|
2334
|
-
if (
|
|
2335
|
-
parentArray.splice(conditionalIndex, 0, elements[0]);
|
|
2336
|
-
conditionalIndex
|
|
2621
|
+
if (actualPosition === "before") {
|
|
2622
|
+
parentArray.splice(conditionalIndex, 0, elements[0], createLiteral("\n"));
|
|
2623
|
+
conditionalIndex += 2;
|
|
2624
|
+
hoistedBefore = true;
|
|
2337
2625
|
}
|
|
2338
2626
|
else {
|
|
2339
|
-
parentArray.splice(conditionalIndex + 1, 0, elements[0]);
|
|
2627
|
+
parentArray.splice(conditionalIndex + 1, 0, createLiteral("\n"), elements[0]);
|
|
2340
2628
|
}
|
|
2629
|
+
didMutate = true;
|
|
2341
2630
|
}
|
|
2342
2631
|
else {
|
|
2343
2632
|
if (hasWrapped)
|
|
2344
2633
|
return;
|
|
2634
|
+
const canWrap = branches.every((branch, index) => {
|
|
2635
|
+
const remaining = getSignificantNodes(branch);
|
|
2636
|
+
return remaining.length === 1 && remaining[0] === elements[index];
|
|
2637
|
+
});
|
|
2638
|
+
if (!canWrap) {
|
|
2639
|
+
if (position === "before")
|
|
2640
|
+
failedToHoistPrefix = true;
|
|
2641
|
+
return;
|
|
2642
|
+
}
|
|
2345
2643
|
for (let i = 0; i < branches.length; i++) {
|
|
2346
2644
|
replaceNodeWithBody(branches[i], elements[i]);
|
|
2347
2645
|
}
|
|
@@ -2350,6 +2648,7 @@ class ERBNoDuplicateBranchElementsRule extends ParserRule {
|
|
|
2350
2648
|
parentArray = wrapper.body;
|
|
2351
2649
|
conditionalIndex = 1;
|
|
2352
2650
|
hasWrapped = true;
|
|
2651
|
+
didMutate = true;
|
|
2353
2652
|
}
|
|
2354
2653
|
};
|
|
2355
2654
|
for (let index = 0; index < prefixCount; index++) {
|
|
@@ -2360,7 +2659,22 @@ class ERBNoDuplicateBranchElementsRule extends ParserRule {
|
|
|
2360
2659
|
const elements = significantBranches.map(branch => branch[branch.length - 1 - offset]);
|
|
2361
2660
|
hoistElement(elements, "after");
|
|
2362
2661
|
}
|
|
2363
|
-
|
|
2662
|
+
if (!hasWrapped && hoistedBefore) {
|
|
2663
|
+
const remaining = branches.map(branch => getSignificantNodes(branch));
|
|
2664
|
+
if (remaining.every(branch => branch.length === 1) && allEquivalentElements(remaining.map(b => b[0]))) {
|
|
2665
|
+
const elements = remaining.map(b => b[0]);
|
|
2666
|
+
const bodiesMatch = elements.every(el => IdentityPrinter.print(el) === IdentityPrinter.print(elements[0]));
|
|
2667
|
+
if (!bodiesMatch && elements.every(el => el.body.length > 0)) {
|
|
2668
|
+
for (let i = 0; i < branches.length; i++) {
|
|
2669
|
+
replaceNodeWithBody(branches[i], elements[i]);
|
|
2670
|
+
}
|
|
2671
|
+
const wrapper = createWrapper(elements[0], [createLiteral("\n"), conditionalNode, createLiteral("\n")]);
|
|
2672
|
+
parentArray[conditionalIndex] = wrapper;
|
|
2673
|
+
didMutate = true;
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
return didMutate ? result : null;
|
|
2364
2678
|
}
|
|
2365
2679
|
}
|
|
2366
2680
|
|
|
@@ -2886,6 +3200,47 @@ class ERBNoRawOutputInAttributeValueRule extends ParserRule {
|
|
|
2886
3200
|
}
|
|
2887
3201
|
}
|
|
2888
3202
|
|
|
3203
|
+
function isAssignmentNode(prismNode) {
|
|
3204
|
+
const type = prismNode?.constructor?.name;
|
|
3205
|
+
if (!type)
|
|
3206
|
+
return false;
|
|
3207
|
+
return type.endsWith("WriteNode");
|
|
3208
|
+
}
|
|
3209
|
+
class ERBNoSilentStatementVisitor extends BaseRuleVisitor {
|
|
3210
|
+
visitERBContentNode(node) {
|
|
3211
|
+
if (isERBOutputNode(node))
|
|
3212
|
+
return;
|
|
3213
|
+
const prismNode = node.prismNode;
|
|
3214
|
+
if (!prismNode)
|
|
3215
|
+
return;
|
|
3216
|
+
if (isAssignmentNode(prismNode))
|
|
3217
|
+
return;
|
|
3218
|
+
const content = node.content?.value?.trim();
|
|
3219
|
+
if (!content)
|
|
3220
|
+
return;
|
|
3221
|
+
this.addOffense(`Avoid using silent ERB tags for statements. Move \`${content}\` to a controller, helper, or presenter.`, node.location);
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
class ERBNoSilentStatementRule extends ParserRule {
|
|
3225
|
+
static ruleName = "erb-no-silent-statement";
|
|
3226
|
+
get defaultConfig() {
|
|
3227
|
+
return {
|
|
3228
|
+
enabled: false,
|
|
3229
|
+
severity: "warning"
|
|
3230
|
+
};
|
|
3231
|
+
}
|
|
3232
|
+
get parserOptions() {
|
|
3233
|
+
return {
|
|
3234
|
+
prism_nodes: true,
|
|
3235
|
+
};
|
|
3236
|
+
}
|
|
3237
|
+
check(result, context) {
|
|
3238
|
+
const visitor = new ERBNoSilentStatementVisitor(this.ruleName, context);
|
|
3239
|
+
visitor.visit(result.value);
|
|
3240
|
+
return visitor.offenses;
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
|
|
2889
3244
|
class ERBNoSilentTagInAttributeNameVisitor extends BaseRuleVisitor {
|
|
2890
3245
|
visitHTMLAttributeNameNode(node) {
|
|
2891
3246
|
const erbNodes = filterERBContentNodes(node.children);
|
|
@@ -3148,7 +3503,7 @@ class ERBNoTrailingWhitespaceRule extends ParserRule {
|
|
|
3148
3503
|
}
|
|
3149
3504
|
|
|
3150
3505
|
const JS_ATTRIBUTE_PATTERN = /^on/i;
|
|
3151
|
-
const SAFE_PATTERN
|
|
3506
|
+
const SAFE_PATTERN = /\.to_json\s*$|\bj\s*[\s(]|\bescape_javascript\s*[\s(]/;
|
|
3152
3507
|
class ERBNoUnsafeJSAttributeVisitor extends AttributeVisitorMixin {
|
|
3153
3508
|
checkStaticAttributeDynamicValue({ attributeName, valueNodes }) {
|
|
3154
3509
|
if (!JS_ATTRIBUTE_PATTERN.test(attributeName))
|
|
@@ -3159,7 +3514,7 @@ class ERBNoUnsafeJSAttributeVisitor extends AttributeVisitorMixin {
|
|
|
3159
3514
|
if (!isERBOutputNode(node))
|
|
3160
3515
|
continue;
|
|
3161
3516
|
const content = node.content?.value?.trim() || "";
|
|
3162
|
-
if (SAFE_PATTERN
|
|
3517
|
+
if (SAFE_PATTERN.test(content))
|
|
3163
3518
|
continue;
|
|
3164
3519
|
this.addOffense(`Unsafe ERB output in \`${attributeName}\` attribute. Use \`.to_json\`, \`j()\`, or \`escape_javascript()\` to safely encode values.`, node.location);
|
|
3165
3520
|
}
|
|
@@ -3240,7 +3595,27 @@ class ERBNoUnsafeRawRule extends ParserRule {
|
|
|
3240
3595
|
}
|
|
3241
3596
|
}
|
|
3242
3597
|
|
|
3243
|
-
const
|
|
3598
|
+
const SAFE_METHOD_NAMES = new Set([
|
|
3599
|
+
"to_json",
|
|
3600
|
+
"json_escape",
|
|
3601
|
+
]);
|
|
3602
|
+
const ESCAPE_JAVASCRIPT_METHOD_NAMES = new Set([
|
|
3603
|
+
"j",
|
|
3604
|
+
"escape_javascript",
|
|
3605
|
+
]);
|
|
3606
|
+
class SafeCallDetector extends PrismVisitor {
|
|
3607
|
+
hasSafeCall = false;
|
|
3608
|
+
hasEscapeJavascriptCall = false;
|
|
3609
|
+
visitCallNode(node) {
|
|
3610
|
+
if (SAFE_METHOD_NAMES.has(node.name)) {
|
|
3611
|
+
this.hasSafeCall = true;
|
|
3612
|
+
}
|
|
3613
|
+
if (ESCAPE_JAVASCRIPT_METHOD_NAMES.has(node.name)) {
|
|
3614
|
+
this.hasEscapeJavascriptCall = true;
|
|
3615
|
+
}
|
|
3616
|
+
this.visitChildNodes(node);
|
|
3617
|
+
}
|
|
3618
|
+
}
|
|
3244
3619
|
class ERBNoUnsafeScriptInterpolationVisitor extends BaseRuleVisitor {
|
|
3245
3620
|
visitHTMLElementNode(node) {
|
|
3246
3621
|
if (!isHTMLOpenTagNode(node.open_tag)) {
|
|
@@ -3269,9 +3644,17 @@ class ERBNoUnsafeScriptInterpolationVisitor extends BaseRuleVisitor {
|
|
|
3269
3644
|
continue;
|
|
3270
3645
|
if (!isERBOutputNode(child))
|
|
3271
3646
|
continue;
|
|
3272
|
-
const
|
|
3273
|
-
|
|
3647
|
+
const erbContent = child;
|
|
3648
|
+
const prismNode = erbContent.prismNode;
|
|
3649
|
+
const detector = new SafeCallDetector();
|
|
3650
|
+
if (prismNode)
|
|
3651
|
+
detector.visit(prismNode);
|
|
3652
|
+
if (detector.hasSafeCall)
|
|
3274
3653
|
continue;
|
|
3654
|
+
if (detector.hasEscapeJavascriptCall) {
|
|
3655
|
+
this.addOffense("Avoid `j()` / `escape_javascript()` in `<script>` tags. It is only safe inside quoted string literals. Use `.to_json` instead, which is safe in any position.", child.location);
|
|
3656
|
+
continue;
|
|
3657
|
+
}
|
|
3275
3658
|
this.addOffense("Unsafe ERB output in `<script>` tag. Use `.to_json` to safely serialize values into JavaScript.", child.location);
|
|
3276
3659
|
}
|
|
3277
3660
|
}
|
|
@@ -3284,6 +3667,11 @@ class ERBNoUnsafeScriptInterpolationRule extends ParserRule {
|
|
|
3284
3667
|
severity: "error"
|
|
3285
3668
|
};
|
|
3286
3669
|
}
|
|
3670
|
+
get parserOptions() {
|
|
3671
|
+
return {
|
|
3672
|
+
prism_nodes: true,
|
|
3673
|
+
};
|
|
3674
|
+
}
|
|
3287
3675
|
check(result, context) {
|
|
3288
3676
|
const visitor = new ERBNoUnsafeScriptInterpolationVisitor(this.ruleName, context);
|
|
3289
3677
|
visitor.visit(result.value);
|
|
@@ -4291,7 +4679,7 @@ class HerbDisableCommentValidRuleNameRule extends ParserRule {
|
|
|
4291
4679
|
}
|
|
4292
4680
|
}
|
|
4293
4681
|
|
|
4294
|
-
const ALLOWED_TYPES = ["text/javascript"];
|
|
4682
|
+
const ALLOWED_TYPES = ["text/javascript", "module", "importmap", "speculationrules"];
|
|
4295
4683
|
class AllowedScriptTypeVisitor extends BaseRuleVisitor {
|
|
4296
4684
|
visitHTMLOpenTagNode(node) {
|
|
4297
4685
|
if (getTagLocalName(node) === "script") {
|
|
@@ -4829,6 +5217,47 @@ class HTMLBodyOnlyElementsRule extends ParserRule {
|
|
|
4829
5217
|
}
|
|
4830
5218
|
}
|
|
4831
5219
|
|
|
5220
|
+
class BooleanAttributesNoValueVisitor extends AttributeVisitorMixin {
|
|
5221
|
+
checkStaticAttributeStaticValue({ originalAttributeName, attributeNode }) {
|
|
5222
|
+
this.checkAttribute(originalAttributeName, attributeNode);
|
|
5223
|
+
}
|
|
5224
|
+
checkStaticAttributeDynamicValue({ originalAttributeName, attributeNode }) {
|
|
5225
|
+
this.checkAttribute(originalAttributeName, attributeNode);
|
|
5226
|
+
}
|
|
5227
|
+
checkAttribute(attributeName, attributeNode) {
|
|
5228
|
+
if (!isBooleanAttribute(attributeName))
|
|
5229
|
+
return;
|
|
5230
|
+
if (!hasAttributeValue(attributeNode))
|
|
5231
|
+
return;
|
|
5232
|
+
this.addOffense(`Boolean attribute \`${IdentityPrinter.print(attributeNode.name)}\` should not have a value. Use \`${attributeName.toLowerCase()}\` instead of \`${IdentityPrinter.print(attributeNode)}\`.`, attributeNode.value.location, {
|
|
5233
|
+
node: attributeNode
|
|
5234
|
+
});
|
|
5235
|
+
}
|
|
5236
|
+
}
|
|
5237
|
+
class HTMLBooleanAttributesNoValueRule extends ParserRule {
|
|
5238
|
+
static autocorrectable = true;
|
|
5239
|
+
static ruleName = "html-boolean-attributes-no-value";
|
|
5240
|
+
get defaultConfig() {
|
|
5241
|
+
return {
|
|
5242
|
+
enabled: true,
|
|
5243
|
+
severity: "error"
|
|
5244
|
+
};
|
|
5245
|
+
}
|
|
5246
|
+
check(result, context) {
|
|
5247
|
+
const visitor = new BooleanAttributesNoValueVisitor(this.ruleName, context);
|
|
5248
|
+
visitor.visit(result.value);
|
|
5249
|
+
return visitor.offenses;
|
|
5250
|
+
}
|
|
5251
|
+
autofix(offense, result, _context) {
|
|
5252
|
+
if (!offense.autofixContext)
|
|
5253
|
+
return null;
|
|
5254
|
+
const { node } = offense.autofixContext;
|
|
5255
|
+
node.equals = null;
|
|
5256
|
+
node.value = null;
|
|
5257
|
+
return result;
|
|
5258
|
+
}
|
|
5259
|
+
}
|
|
5260
|
+
|
|
4832
5261
|
class DetailsHasSummaryVisitor extends BaseRuleVisitor {
|
|
4833
5262
|
visitHTMLElementNode(node) {
|
|
4834
5263
|
this.checkDetailsElement(node);
|
|
@@ -4878,47 +5307,6 @@ class HTMLDetailsHasSummaryRule extends ParserRule {
|
|
|
4878
5307
|
}
|
|
4879
5308
|
}
|
|
4880
5309
|
|
|
4881
|
-
class BooleanAttributesNoValueVisitor extends AttributeVisitorMixin {
|
|
4882
|
-
checkStaticAttributeStaticValue({ originalAttributeName, attributeNode }) {
|
|
4883
|
-
this.checkAttribute(originalAttributeName, attributeNode);
|
|
4884
|
-
}
|
|
4885
|
-
checkStaticAttributeDynamicValue({ originalAttributeName, attributeNode }) {
|
|
4886
|
-
this.checkAttribute(originalAttributeName, attributeNode);
|
|
4887
|
-
}
|
|
4888
|
-
checkAttribute(attributeName, attributeNode) {
|
|
4889
|
-
if (!isBooleanAttribute(attributeName))
|
|
4890
|
-
return;
|
|
4891
|
-
if (!hasAttributeValue(attributeNode))
|
|
4892
|
-
return;
|
|
4893
|
-
this.addOffense(`Boolean attribute \`${IdentityPrinter.print(attributeNode.name)}\` should not have a value. Use \`${attributeName.toLowerCase()}\` instead of \`${IdentityPrinter.print(attributeNode)}\`.`, attributeNode.value.location, {
|
|
4894
|
-
node: attributeNode
|
|
4895
|
-
});
|
|
4896
|
-
}
|
|
4897
|
-
}
|
|
4898
|
-
class HTMLBooleanAttributesNoValueRule extends ParserRule {
|
|
4899
|
-
static autocorrectable = true;
|
|
4900
|
-
static ruleName = "html-boolean-attributes-no-value";
|
|
4901
|
-
get defaultConfig() {
|
|
4902
|
-
return {
|
|
4903
|
-
enabled: true,
|
|
4904
|
-
severity: "error"
|
|
4905
|
-
};
|
|
4906
|
-
}
|
|
4907
|
-
check(result, context) {
|
|
4908
|
-
const visitor = new BooleanAttributesNoValueVisitor(this.ruleName, context);
|
|
4909
|
-
visitor.visit(result.value);
|
|
4910
|
-
return visitor.offenses;
|
|
4911
|
-
}
|
|
4912
|
-
autofix(offense, result, _context) {
|
|
4913
|
-
if (!offense.autofixContext)
|
|
4914
|
-
return null;
|
|
4915
|
-
const { node } = offense.autofixContext;
|
|
4916
|
-
node.equals = null;
|
|
4917
|
-
node.value = null;
|
|
4918
|
-
return result;
|
|
4919
|
-
}
|
|
4920
|
-
}
|
|
4921
|
-
|
|
4922
5310
|
class HeadOnlyElementsVisitor extends BaseRuleVisitor {
|
|
4923
5311
|
elementStack = [];
|
|
4924
5312
|
visitHTMLElementNode(node) {
|
|
@@ -6601,8 +6989,10 @@ class TurboPermanentRequireIdRule extends ParserRule {
|
|
|
6601
6989
|
|
|
6602
6990
|
const rules = [
|
|
6603
6991
|
ActionViewNoSilentHelperRule,
|
|
6992
|
+
ActionViewNoSilentRenderRule,
|
|
6604
6993
|
ERBCommentSyntax,
|
|
6605
6994
|
ERBNoCaseNodeChildrenRule,
|
|
6995
|
+
ERBNoEmptyControlFlowRule,
|
|
6606
6996
|
ERBNoConditionalHTMLElementRule,
|
|
6607
6997
|
ERBNoConditionalOpenTagRule,
|
|
6608
6998
|
ERBNoDuplicateBranchElementsRule,
|
|
@@ -6617,6 +7007,7 @@ const rules = [
|
|
|
6617
7007
|
ERBNoOutputInAttributeNameRule,
|
|
6618
7008
|
ERBNoOutputInAttributePositionRule,
|
|
6619
7009
|
ERBNoRawOutputInAttributeValueRule,
|
|
7010
|
+
ERBNoSilentStatementRule,
|
|
6620
7011
|
ERBNoSilentTagInAttributeNameRule,
|
|
6621
7012
|
ERBNoStatementInScriptRule,
|
|
6622
7013
|
ERBNoThenInControlFlowRule,
|
|
@@ -6648,8 +7039,8 @@ const rules = [
|
|
|
6648
7039
|
HTMLAttributeValuesRequireQuotesRule,
|
|
6649
7040
|
HTMLAvoidBothDisabledAndAriaDisabledRule,
|
|
6650
7041
|
HTMLBodyOnlyElementsRule,
|
|
6651
|
-
HTMLDetailsHasSummaryRule,
|
|
6652
7042
|
HTMLBooleanAttributesNoValueRule,
|
|
7043
|
+
HTMLDetailsHasSummaryRule,
|
|
6653
7044
|
HTMLHeadOnlyElementsRule,
|
|
6654
7045
|
HTMLIframeHasTitleRule,
|
|
6655
7046
|
HTMLImgRequireAltRule,
|
|
@@ -7207,5 +7598,5 @@ function ruleDocumentationUrl(ruleId) {
|
|
|
7207
7598
|
return `${DOCS_BASE_URL}/${ruleId}`;
|
|
7208
7599
|
}
|
|
7209
7600
|
|
|
7210
|
-
export { ABSTRACT_ARIA_ROLES, ARIA_ATTRIBUTES, AttributeVisitorMixin, BaseLexerRuleVisitor, BaseRuleVisitor, BaseSourceRuleVisitor, ControlFlowTrackingVisitor, ControlFlowType, DEFAULT_LINTER_PARSER_OPTIONS, DEFAULT_LINT_CONTEXT, DEFAULT_RULE_CONFIG, DOCUMENT_ONLY_TAG_NAMES, ERBCommentSyntax, ERBNoCaseNodeChildrenRule, ERBNoConditionalOpenTagRule, ERBNoDuplicateBranchElementsRule, ERBNoEmptyTagsRule, ERBNoExtraNewLineRule, ERBNoExtraWhitespaceRule, ERBNoInlineCaseConditionsRule, ERBNoInstanceVariablesInPartialsRule, ERBNoJavascriptTagHelperRule, ERBNoOutputControlFlowRule, ERBNoOutputInAttributeNameRule, ERBNoOutputInAttributePositionRule, ERBNoRawOutputInAttributeValueRule, ERBNoSilentTagInAttributeNameRule, ERBNoStatementInScriptRule, ERBNoThenInControlFlowRule, ERBNoTrailingWhitespaceRule, ERBNoUnsafeJSAttributeRule, ERBNoUnsafeRawRule, ERBNoUnsafeScriptInterpolationRule, ERBPreferImageTagHelperRule, ERBRequireTrailingNewlineRule, ERBRequireWhitespaceRule, ERBRightTrimRule, ERBStrictLocalsCommentSyntaxRule, ERBStrictLocalsRequiredRule, HEADING_TAGS, HEAD_AND_BODY_TAG_NAMES, HEAD_ONLY_TAG_NAMES, HTMLAllowedScriptTypeRule, HTMLAnchorRequireHrefRule, HTMLAriaLabelIsWellFormattedRule, HTMLAriaLevelMustBeValidRule, HTMLAriaRoleHeadingRequiresLevelRule, HTMLAriaRoleMustBeValidRule, HTMLAttributeDoubleQuotesRule, HTMLAttributeEqualsSpacingRule, HTMLAttributeValuesRequireQuotesRule, HTMLAvoidBothDisabledAndAriaDisabledRule, HTMLBodyOnlyElementsRule, HTMLBooleanAttributesNoValueRule, HTMLDetailsHasSummaryRule, HTMLHeadOnlyElementsRule, HTMLIframeHasTitleRule, HTMLImgRequireAltRule, HTMLInputRequireAutocompleteRule, HTMLNavigationHasLabelRule, HTMLNoAbstractRolesRule, HTMLNoAriaHiddenOnBodyRule, HTMLNoAriaHiddenOnFocusableRule, HTMLNoBlockInsideInlineRule, HTMLNoDuplicateAttributesRule, HTMLNoDuplicateIdsRule, HTMLNoDuplicateMetaNamesRule, HTMLNoEmptyAttributesRule, HTMLNoEmptyHeadingsRule, HTMLNoNestedLinksRule, HTMLNoPositiveTabIndexRule, HTMLNoSelfClosingRule, HTMLNoSpaceInTagRule, HTMLNoTitleAttributeRule, HTMLNoUnderscoresInAttributeNamesRule, HTMLRequireClosingTagsRule, HTMLTagNameLowercaseRule, HTML_BLOCK_ELEMENTS, HTML_BOOLEAN_ATTRIBUTES, HTML_INLINE_ELEMENTS, HTML_ONLY_TAG_NAMES, HTML_VOID_ELEMENTS, HerbDisableCommentBaseVisitor, HerbDisableCommentMalformedRule, HerbDisableCommentMissingRulesRule, HerbDisableCommentNoDuplicateRulesRule, HerbDisableCommentNoRedundantAllRule, HerbDisableCommentParsedVisitor, HerbDisableCommentUnnecessaryRule, HerbDisableCommentValidRuleNameRule, LexerRule, Linter, ParserRule, STRICT_LOCALS_PATTERN, SVGTagNameCapitalizationRule, SVG_CAMEL_CASE_ELEMENTS, SVG_LOWERCASE_TO_CAMELCASE, SourceRule, VALID_ARIA_ROLES, createEndOfFileLocation, findNodeAtPosition, findNodeByLocation, findParent, getBasename, hasBalancedParentheses, isBlockElement, isBodyOnlyTag, isBodyTag, isBooleanAttribute, isDocumentOnlyTag, isHeadAndBodyTag, isHeadOnlyTag, isHeadTag, isHtmlOnlyTag, isInlineElement, isPartialFile, isVoidElement, locationFromOffset, locationsEqual, positionFromOffset, ruleDocumentationUrl, rules, splitByTopLevelComma };
|
|
7601
|
+
export { ABSTRACT_ARIA_ROLES, ARIA_ATTRIBUTES, ActionViewNoSilentHelperRule, ActionViewNoSilentRenderRule, AttributeVisitorMixin, BaseLexerRuleVisitor, BaseRuleVisitor, BaseSourceRuleVisitor, ControlFlowTrackingVisitor, ControlFlowType, DEFAULT_LINTER_PARSER_OPTIONS, DEFAULT_LINT_CONTEXT, DEFAULT_RULE_CONFIG, DOCUMENT_ONLY_TAG_NAMES, ERBCommentSyntax, ERBNoCaseNodeChildrenRule, ERBNoConditionalOpenTagRule, ERBNoDuplicateBranchElementsRule, ERBNoEmptyControlFlowRule, ERBNoEmptyTagsRule, ERBNoExtraNewLineRule, ERBNoExtraWhitespaceRule, ERBNoInlineCaseConditionsRule, ERBNoInstanceVariablesInPartialsRule, ERBNoJavascriptTagHelperRule, ERBNoOutputControlFlowRule, ERBNoOutputInAttributeNameRule, ERBNoOutputInAttributePositionRule, ERBNoRawOutputInAttributeValueRule, ERBNoSilentStatementRule, ERBNoSilentTagInAttributeNameRule, ERBNoStatementInScriptRule, ERBNoThenInControlFlowRule, ERBNoTrailingWhitespaceRule, ERBNoUnsafeJSAttributeRule, ERBNoUnsafeRawRule, ERBNoUnsafeScriptInterpolationRule, ERBPreferImageTagHelperRule, ERBRequireTrailingNewlineRule, ERBRequireWhitespaceRule, ERBRightTrimRule, ERBStrictLocalsCommentSyntaxRule, ERBStrictLocalsRequiredRule, HEADING_TAGS, HEAD_AND_BODY_TAG_NAMES, HEAD_ONLY_TAG_NAMES, HTMLAllowedScriptTypeRule, HTMLAnchorRequireHrefRule, HTMLAriaLabelIsWellFormattedRule, HTMLAriaLevelMustBeValidRule, HTMLAriaRoleHeadingRequiresLevelRule, HTMLAriaRoleMustBeValidRule, HTMLAttributeDoubleQuotesRule, HTMLAttributeEqualsSpacingRule, HTMLAttributeValuesRequireQuotesRule, HTMLAvoidBothDisabledAndAriaDisabledRule, HTMLBodyOnlyElementsRule, HTMLBooleanAttributesNoValueRule, HTMLDetailsHasSummaryRule, HTMLHeadOnlyElementsRule, HTMLIframeHasTitleRule, HTMLImgRequireAltRule, HTMLInputRequireAutocompleteRule, HTMLNavigationHasLabelRule, HTMLNoAbstractRolesRule, HTMLNoAriaHiddenOnBodyRule, HTMLNoAriaHiddenOnFocusableRule, HTMLNoBlockInsideInlineRule, HTMLNoDuplicateAttributesRule, HTMLNoDuplicateIdsRule, HTMLNoDuplicateMetaNamesRule, HTMLNoEmptyAttributesRule, HTMLNoEmptyHeadingsRule, HTMLNoNestedLinksRule, HTMLNoPositiveTabIndexRule, HTMLNoSelfClosingRule, HTMLNoSpaceInTagRule, HTMLNoTitleAttributeRule, HTMLNoUnderscoresInAttributeNamesRule, HTMLRequireClosingTagsRule, HTMLTagNameLowercaseRule, HTML_BLOCK_ELEMENTS, HTML_BOOLEAN_ATTRIBUTES, HTML_INLINE_ELEMENTS, HTML_ONLY_TAG_NAMES, HTML_VOID_ELEMENTS, HerbDisableCommentBaseVisitor, HerbDisableCommentMalformedRule, HerbDisableCommentMissingRulesRule, HerbDisableCommentNoDuplicateRulesRule, HerbDisableCommentNoRedundantAllRule, HerbDisableCommentParsedVisitor, HerbDisableCommentUnnecessaryRule, HerbDisableCommentValidRuleNameRule, LexerRule, Linter, ParserRule, STRICT_LOCALS_PATTERN, SVGTagNameCapitalizationRule, SVG_CAMEL_CASE_ELEMENTS, SVG_LOWERCASE_TO_CAMELCASE, SourceRule, VALID_ARIA_ROLES, createEndOfFileLocation, findNodeAtPosition, findNodeByLocation, findParent, getBasename, hasBalancedParentheses, isBlockElement, isBodyOnlyTag, isBodyTag, isBooleanAttribute, isDocumentOnlyTag, isHeadAndBodyTag, isHeadOnlyTag, isHeadTag, isHtmlOnlyTag, isInlineElement, isPartialFile, isVoidElement, locationFromOffset, locationsEqual, positionFromOffset, ruleDocumentationUrl, rules, splitByTopLevelComma };
|
|
7211
7602
|
//# sourceMappingURL=index.js.map
|