@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.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, isBooleanAttribute, getStaticAttributeName, isERBControlFlowNode, HTMLCloseTagNode, getTagName, createWhitespaceNode, filterWhitespaceNodes, getStaticContentFromNodes, getOpenTag, isHTMLCloseTagNode, HTMLOpenTagNode, DEFAULT_PARSER_OPTIONS } from '@herb-tools/core';
|
|
3
|
+
export { HTML_BOOLEAN_ATTRIBUTES, findAttributeByName, getAttribute, getAttributeName, getAttributeValue, getAttributeValueNodes, getAttributeValueQuoteType, getAttributes, getCombinedAttributeNameString, getStaticAttributeValue, getStaticAttributeValueContent, getTagName, hasAttribute, hasAttributeValue, hasDynamicAttributeName, hasDynamicAttributeValue, hasStaticAttributeValue, hasStaticAttributeValueContent, isAttributeValueQuoted, isBooleanAttribute } 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
|
/**
|
|
@@ -1187,13 +1198,6 @@ const HTML_VOID_ELEMENTS = new Set([
|
|
|
1187
1198
|
"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta",
|
|
1188
1199
|
"param", "source", "track", "wbr",
|
|
1189
1200
|
]);
|
|
1190
|
-
const HTML_BOOLEAN_ATTRIBUTES = new Set([
|
|
1191
|
-
"autofocus", "autoplay", "checked", "controls", "defer", "disabled", "hidden",
|
|
1192
|
-
"loop", "multiple", "muted", "readonly", "required", "reversed", "selected",
|
|
1193
|
-
"open", "default", "formnovalidate", "novalidate", "itemscope", "scoped",
|
|
1194
|
-
"seamless", "allowfullscreen", "async", "compact", "declare", "nohref",
|
|
1195
|
-
"noresize", "noshade", "nowrap", "sortable", "truespeed", "typemustmatch"
|
|
1196
|
-
]);
|
|
1197
1201
|
const HEADING_TAGS = new Set(["h1", "h2", "h3", "h4", "h5", "h6"]);
|
|
1198
1202
|
/**
|
|
1199
1203
|
* SVG elements that use camelCase naming
|
|
@@ -1348,12 +1352,6 @@ function isBlockElement(tagName) {
|
|
|
1348
1352
|
function isVoidElement(tagName) {
|
|
1349
1353
|
return HTML_VOID_ELEMENTS.has(tagName.toLowerCase());
|
|
1350
1354
|
}
|
|
1351
|
-
/**
|
|
1352
|
-
* Checks if an attribute is a boolean attribute
|
|
1353
|
-
*/
|
|
1354
|
-
function isBooleanAttribute(attributeName) {
|
|
1355
|
-
return HTML_BOOLEAN_ATTRIBUTES.has(attributeName.toLowerCase());
|
|
1356
|
-
}
|
|
1357
1355
|
/**
|
|
1358
1356
|
* Attribute visitor that provides granular processing based on both
|
|
1359
1357
|
* attribute name type (static/dynamic) and value type (static/dynamic)
|
|
@@ -1376,7 +1374,7 @@ class AttributeVisitorMixin extends BaseRuleVisitor {
|
|
|
1376
1374
|
forEachAttribute(node, (attributeNode) => {
|
|
1377
1375
|
const staticAttributeName = getAttributeName(attributeNode);
|
|
1378
1376
|
const originalAttributeName = getAttributeName(attributeNode, false) || "";
|
|
1379
|
-
const isDynamicName =
|
|
1377
|
+
const isDynamicName = hasDynamicAttributeName(attributeNode);
|
|
1380
1378
|
const staticAttributeValue = getStaticAttributeValue(attributeNode);
|
|
1381
1379
|
const valueNodes = getAttributeValueNodes(attributeNode);
|
|
1382
1380
|
const hasOutputERB = hasERBOutput(valueNodes);
|
|
@@ -1453,7 +1451,7 @@ class BaseLexerRuleVisitor {
|
|
|
1453
1451
|
* Helper method to create an unbound lint offense (without severity).
|
|
1454
1452
|
* The Linter will bind severity based on the rule's config.
|
|
1455
1453
|
*/
|
|
1456
|
-
createOffense(message, location, autofixContext, severity) {
|
|
1454
|
+
createOffense(message, location, autofixContext, severity, tags) {
|
|
1457
1455
|
return {
|
|
1458
1456
|
rule: this.ruleName,
|
|
1459
1457
|
code: this.ruleName,
|
|
@@ -1462,13 +1460,14 @@ class BaseLexerRuleVisitor {
|
|
|
1462
1460
|
location,
|
|
1463
1461
|
autofixContext,
|
|
1464
1462
|
severity,
|
|
1463
|
+
tags,
|
|
1465
1464
|
};
|
|
1466
1465
|
}
|
|
1467
1466
|
/**
|
|
1468
1467
|
* Helper method to add an offense to the offenses array
|
|
1469
1468
|
*/
|
|
1470
|
-
addOffense(message, location, autofixContext, severity) {
|
|
1471
|
-
this.offenses.push(this.createOffense(message, location, autofixContext, severity));
|
|
1469
|
+
addOffense(message, location, autofixContext, severity, tags) {
|
|
1470
|
+
this.offenses.push(this.createOffense(message, location, autofixContext, severity, tags));
|
|
1472
1471
|
}
|
|
1473
1472
|
/**
|
|
1474
1473
|
* Main entry point for lexer rule visitors
|
|
@@ -1509,7 +1508,7 @@ class BaseSourceRuleVisitor {
|
|
|
1509
1508
|
* Helper method to create an unbound lint offense (without severity).
|
|
1510
1509
|
* The Linter will bind severity based on the rule's config.
|
|
1511
1510
|
*/
|
|
1512
|
-
createOffense(message, location, autofixContext, severity) {
|
|
1511
|
+
createOffense(message, location, autofixContext, severity, tags) {
|
|
1513
1512
|
return {
|
|
1514
1513
|
rule: this.ruleName,
|
|
1515
1514
|
code: this.ruleName,
|
|
@@ -1518,13 +1517,14 @@ class BaseSourceRuleVisitor {
|
|
|
1518
1517
|
location,
|
|
1519
1518
|
autofixContext,
|
|
1520
1519
|
severity,
|
|
1520
|
+
tags,
|
|
1521
1521
|
};
|
|
1522
1522
|
}
|
|
1523
1523
|
/**
|
|
1524
1524
|
* Helper method to add an offense to the offenses array
|
|
1525
1525
|
*/
|
|
1526
|
-
addOffense(message, location, autofixContext, severity) {
|
|
1527
|
-
this.offenses.push(this.createOffense(message, location, autofixContext, severity));
|
|
1526
|
+
addOffense(message, location, autofixContext, severity, tags) {
|
|
1527
|
+
this.offenses.push(this.createOffense(message, location, autofixContext, severity, tags));
|
|
1528
1528
|
}
|
|
1529
1529
|
/**
|
|
1530
1530
|
* Main entry point for source rule visitors
|
|
@@ -1874,6 +1874,34 @@ class ActionViewNoSilentHelperRule extends ParserRule {
|
|
|
1874
1874
|
}
|
|
1875
1875
|
}
|
|
1876
1876
|
|
|
1877
|
+
class ActionViewNoSilentRenderVisitor extends BaseRuleVisitor {
|
|
1878
|
+
visitERBRenderNode(node) {
|
|
1879
|
+
if (!isERBOutputNode(node)) {
|
|
1880
|
+
this.addOffense(`Avoid using \`${node.tag_opening?.value} %>\` with \`render\`. Use \`<%= %>\` to ensure the rendered content is output.`, node.location);
|
|
1881
|
+
}
|
|
1882
|
+
this.visitChildNodes(node);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
class ActionViewNoSilentRenderRule extends ParserRule {
|
|
1886
|
+
static ruleName = "actionview-no-silent-render";
|
|
1887
|
+
get defaultConfig() {
|
|
1888
|
+
return {
|
|
1889
|
+
enabled: true,
|
|
1890
|
+
severity: "error"
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1893
|
+
get parserOptions() {
|
|
1894
|
+
return {
|
|
1895
|
+
render_nodes: true,
|
|
1896
|
+
};
|
|
1897
|
+
}
|
|
1898
|
+
check(result, context) {
|
|
1899
|
+
const visitor = new ActionViewNoSilentRenderVisitor(this.ruleName, context);
|
|
1900
|
+
visitor.visit(result.value);
|
|
1901
|
+
return visitor.offenses;
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1877
1905
|
class ERBCommentSyntaxVisitor extends BaseRuleVisitor {
|
|
1878
1906
|
visitERBContentNode(node) {
|
|
1879
1907
|
const content = node.content?.value || "";
|
|
@@ -1938,7 +1966,9 @@ class ERBNoCaseNodeChildrenVisitor extends BaseRuleVisitor {
|
|
|
1938
1966
|
for (const child of node.children) {
|
|
1939
1967
|
if (!this.isAllowedContent(child)) {
|
|
1940
1968
|
const childCode = IdentityPrinter.print(child).trim();
|
|
1941
|
-
this.
|
|
1969
|
+
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);
|
|
1970
|
+
offense.tags = ["unnecessary"];
|
|
1971
|
+
this.offenses.push(offense);
|
|
1942
1972
|
}
|
|
1943
1973
|
}
|
|
1944
1974
|
}
|
|
@@ -1968,6 +1998,193 @@ class ERBNoCaseNodeChildrenRule extends ParserRule {
|
|
|
1968
1998
|
}
|
|
1969
1999
|
}
|
|
1970
2000
|
|
|
2001
|
+
class ERBNoEmptyControlFlowVisitor extends BaseRuleVisitor {
|
|
2002
|
+
processedIfNodes = new Set();
|
|
2003
|
+
processedElseNodes = new Set();
|
|
2004
|
+
visitERBIfNode(node) {
|
|
2005
|
+
if (this.processedIfNodes.has(node)) {
|
|
2006
|
+
return;
|
|
2007
|
+
}
|
|
2008
|
+
this.markIfChainAsProcessed(node);
|
|
2009
|
+
this.markElseNodesInIfChain(node);
|
|
2010
|
+
const entireChainEmpty = this.isEntireIfChainEmpty(node);
|
|
2011
|
+
if (entireChainEmpty) {
|
|
2012
|
+
this.addEmptyBlockOffense(node, node.statements, "if");
|
|
2013
|
+
}
|
|
2014
|
+
else {
|
|
2015
|
+
this.checkIfChainParts(node);
|
|
2016
|
+
}
|
|
2017
|
+
this.visitChildNodes(node);
|
|
2018
|
+
}
|
|
2019
|
+
visitERBElseNode(node) {
|
|
2020
|
+
if (this.processedElseNodes.has(node)) {
|
|
2021
|
+
this.visitChildNodes(node);
|
|
2022
|
+
return;
|
|
2023
|
+
}
|
|
2024
|
+
this.addEmptyBlockOffense(node, node.statements, "else");
|
|
2025
|
+
this.visitChildNodes(node);
|
|
2026
|
+
}
|
|
2027
|
+
visitERBUnlessNode(node) {
|
|
2028
|
+
const unlessHasContent = this.statementsHaveContent(node.statements);
|
|
2029
|
+
const elseHasContent = node.else_clause && this.statementsHaveContent(node.else_clause.statements);
|
|
2030
|
+
if (node.else_clause) {
|
|
2031
|
+
this.processedElseNodes.add(node.else_clause);
|
|
2032
|
+
}
|
|
2033
|
+
const entireBlockEmpty = !unlessHasContent && !elseHasContent;
|
|
2034
|
+
if (entireBlockEmpty) {
|
|
2035
|
+
this.addEmptyBlockOffense(node, node.statements, "unless");
|
|
2036
|
+
}
|
|
2037
|
+
else {
|
|
2038
|
+
if (!unlessHasContent) {
|
|
2039
|
+
this.addEmptyBlockOffenseWithEnd(node, node.statements, "unless", node.else_clause);
|
|
2040
|
+
}
|
|
2041
|
+
if (node.else_clause && !elseHasContent) {
|
|
2042
|
+
this.addEmptyBlockOffense(node.else_clause, node.else_clause.statements, "else");
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
this.visitChildNodes(node);
|
|
2046
|
+
}
|
|
2047
|
+
visitERBForNode(node) {
|
|
2048
|
+
this.addEmptyBlockOffense(node, node.statements, "for");
|
|
2049
|
+
this.visitChildNodes(node);
|
|
2050
|
+
}
|
|
2051
|
+
visitERBWhileNode(node) {
|
|
2052
|
+
this.addEmptyBlockOffense(node, node.statements, "while");
|
|
2053
|
+
this.visitChildNodes(node);
|
|
2054
|
+
}
|
|
2055
|
+
visitERBUntilNode(node) {
|
|
2056
|
+
this.addEmptyBlockOffense(node, node.statements, "until");
|
|
2057
|
+
this.visitChildNodes(node);
|
|
2058
|
+
}
|
|
2059
|
+
visitERBWhenNode(node) {
|
|
2060
|
+
if (!node.then_keyword) {
|
|
2061
|
+
this.addEmptyBlockOffense(node, node.statements, "when");
|
|
2062
|
+
}
|
|
2063
|
+
this.visitChildNodes(node);
|
|
2064
|
+
}
|
|
2065
|
+
visitERBInNode(node) {
|
|
2066
|
+
if (!node.then_keyword) {
|
|
2067
|
+
this.addEmptyBlockOffense(node, node.statements, "in");
|
|
2068
|
+
}
|
|
2069
|
+
this.visitChildNodes(node);
|
|
2070
|
+
}
|
|
2071
|
+
visitERBBeginNode(node) {
|
|
2072
|
+
this.addEmptyBlockOffense(node, node.statements, "begin");
|
|
2073
|
+
this.visitChildNodes(node);
|
|
2074
|
+
}
|
|
2075
|
+
visitERBRescueNode(node) {
|
|
2076
|
+
this.addEmptyBlockOffense(node, node.statements, "rescue");
|
|
2077
|
+
this.visitChildNodes(node);
|
|
2078
|
+
}
|
|
2079
|
+
visitERBEnsureNode(node) {
|
|
2080
|
+
this.addEmptyBlockOffense(node, node.statements, "ensure");
|
|
2081
|
+
this.visitChildNodes(node);
|
|
2082
|
+
}
|
|
2083
|
+
visitERBBlockNode(node) {
|
|
2084
|
+
this.addEmptyBlockOffense(node, node.body, "do");
|
|
2085
|
+
this.visitChildNodes(node);
|
|
2086
|
+
}
|
|
2087
|
+
addEmptyBlockOffense(node, statements, blockType) {
|
|
2088
|
+
this.addEmptyBlockOffenseWithEnd(node, statements, blockType, null);
|
|
2089
|
+
}
|
|
2090
|
+
addEmptyBlockOffenseWithEnd(node, statements, blockType, subsequentNode) {
|
|
2091
|
+
if (this.statementsHaveContent(statements)) {
|
|
2092
|
+
return;
|
|
2093
|
+
}
|
|
2094
|
+
const startLocation = node.location.start;
|
|
2095
|
+
const endLocation = subsequentNode
|
|
2096
|
+
? subsequentNode.location.start
|
|
2097
|
+
: node.location.end;
|
|
2098
|
+
const location = Location.from(startLocation.line, startLocation.column, endLocation.line, endLocation.column);
|
|
2099
|
+
const offense = this.createOffense(`Empty ${blockType} block: this control flow statement has no content`, location);
|
|
2100
|
+
offense.tags = ["unnecessary"];
|
|
2101
|
+
this.offenses.push(offense);
|
|
2102
|
+
}
|
|
2103
|
+
statementsHaveContent(statements) {
|
|
2104
|
+
return statements.some(statement => {
|
|
2105
|
+
if (isHTMLTextNode(statement)) {
|
|
2106
|
+
return statement.content.trim() !== "";
|
|
2107
|
+
}
|
|
2108
|
+
return true;
|
|
2109
|
+
});
|
|
2110
|
+
}
|
|
2111
|
+
markIfChainAsProcessed(node) {
|
|
2112
|
+
this.processedIfNodes.add(node);
|
|
2113
|
+
this.traverseSubsequentNodes(node.subsequent, (current) => {
|
|
2114
|
+
if (isERBIfNode(current)) {
|
|
2115
|
+
this.processedIfNodes.add(current);
|
|
2116
|
+
}
|
|
2117
|
+
});
|
|
2118
|
+
}
|
|
2119
|
+
markElseNodesInIfChain(node) {
|
|
2120
|
+
this.traverseSubsequentNodes(node.subsequent, (current) => {
|
|
2121
|
+
if (isERBElseNode(current)) {
|
|
2122
|
+
this.processedElseNodes.add(current);
|
|
2123
|
+
}
|
|
2124
|
+
});
|
|
2125
|
+
}
|
|
2126
|
+
traverseSubsequentNodes(startNode, callback) {
|
|
2127
|
+
let current = startNode;
|
|
2128
|
+
while (current) {
|
|
2129
|
+
if (isERBIfNode(current)) {
|
|
2130
|
+
callback(current);
|
|
2131
|
+
current = current.subsequent;
|
|
2132
|
+
}
|
|
2133
|
+
else if (isERBElseNode(current)) {
|
|
2134
|
+
callback(current);
|
|
2135
|
+
break;
|
|
2136
|
+
}
|
|
2137
|
+
else {
|
|
2138
|
+
break;
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
}
|
|
2142
|
+
checkIfChainParts(node) {
|
|
2143
|
+
if (!this.statementsHaveContent(node.statements)) {
|
|
2144
|
+
this.addEmptyBlockOffenseWithEnd(node, node.statements, "if", node.subsequent);
|
|
2145
|
+
}
|
|
2146
|
+
this.traverseSubsequentNodes(node.subsequent, (current) => {
|
|
2147
|
+
if (this.statementsHaveContent(current.statements)) {
|
|
2148
|
+
return;
|
|
2149
|
+
}
|
|
2150
|
+
const blockType = isERBIfNode(current) ? "elsif" : "else";
|
|
2151
|
+
const nextSubsequent = isERBIfNode(current) ? current.subsequent : null;
|
|
2152
|
+
if (nextSubsequent) {
|
|
2153
|
+
this.addEmptyBlockOffenseWithEnd(current, current.statements, blockType, nextSubsequent);
|
|
2154
|
+
}
|
|
2155
|
+
else {
|
|
2156
|
+
this.addEmptyBlockOffense(current, current.statements, blockType);
|
|
2157
|
+
}
|
|
2158
|
+
});
|
|
2159
|
+
}
|
|
2160
|
+
isEntireIfChainEmpty(node) {
|
|
2161
|
+
if (this.statementsHaveContent(node.statements)) {
|
|
2162
|
+
return false;
|
|
2163
|
+
}
|
|
2164
|
+
let hasContent = false;
|
|
2165
|
+
this.traverseSubsequentNodes(node.subsequent, (current) => {
|
|
2166
|
+
if (this.statementsHaveContent(current.statements)) {
|
|
2167
|
+
hasContent = true;
|
|
2168
|
+
}
|
|
2169
|
+
});
|
|
2170
|
+
return !hasContent;
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
class ERBNoEmptyControlFlowRule extends ParserRule {
|
|
2174
|
+
static ruleName = "erb-no-empty-control-flow";
|
|
2175
|
+
get defaultConfig() {
|
|
2176
|
+
return {
|
|
2177
|
+
enabled: true,
|
|
2178
|
+
severity: "hint"
|
|
2179
|
+
};
|
|
2180
|
+
}
|
|
2181
|
+
check(result, context) {
|
|
2182
|
+
const visitor = new ERBNoEmptyControlFlowVisitor(this.ruleName, context);
|
|
2183
|
+
visitor.visit(result.value);
|
|
2184
|
+
return visitor.offenses;
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
|
|
1971
2188
|
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
2189
|
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
2190
|
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 +2343,15 @@ class ERBNoConditionalOpenTagRule extends ParserRule {
|
|
|
2126
2343
|
function getSignificantNodes(statements) {
|
|
2127
2344
|
return statements.filter(node => !isPureWhitespaceNode(node));
|
|
2128
2345
|
}
|
|
2346
|
+
function trimWhitespaceNodes(nodes) {
|
|
2347
|
+
let start = 0;
|
|
2348
|
+
let end = nodes.length;
|
|
2349
|
+
while (start < end && isPureWhitespaceNode(nodes[start]))
|
|
2350
|
+
start++;
|
|
2351
|
+
while (end > start && isPureWhitespaceNode(nodes[end - 1]))
|
|
2352
|
+
end--;
|
|
2353
|
+
return nodes.slice(start, end);
|
|
2354
|
+
}
|
|
2129
2355
|
function allEquivalentElements(nodes) {
|
|
2130
2356
|
if (nodes.length < 2)
|
|
2131
2357
|
return false;
|
|
@@ -2243,9 +2469,19 @@ class ERBNoDuplicateBranchElementsVisitor extends BaseRuleVisitor {
|
|
|
2243
2469
|
if (isERBIfNode(node)) {
|
|
2244
2470
|
this.markSubsequentIfNodesAsProcessed(node);
|
|
2245
2471
|
}
|
|
2472
|
+
if (this.allBranchesIdentical(branches)) {
|
|
2473
|
+
this.addOffense("All branches of this conditional have identical content. The conditional can be removed.", node.location, { node: node, allIdentical: true }, "warning");
|
|
2474
|
+
return;
|
|
2475
|
+
}
|
|
2246
2476
|
const state = { isFirstOffense: true };
|
|
2247
2477
|
this.checkBranches(branches, node, state);
|
|
2248
2478
|
}
|
|
2479
|
+
allBranchesIdentical(branches) {
|
|
2480
|
+
if (branches.length < 2)
|
|
2481
|
+
return false;
|
|
2482
|
+
const first = branches[0].map(node => IdentityPrinter.print(node)).join("");
|
|
2483
|
+
return branches.slice(1).every(branch => branch.map(node => IdentityPrinter.print(node)).join("") === first);
|
|
2484
|
+
}
|
|
2249
2485
|
markSubsequentIfNodesAsProcessed(node) {
|
|
2250
2486
|
let current = node.subsequent;
|
|
2251
2487
|
while (current) {
|
|
@@ -2279,11 +2515,23 @@ class ERBNoDuplicateBranchElementsVisitor extends BaseRuleVisitor {
|
|
|
2279
2515
|
const bodiesMatch = elements.every(element => IdentityPrinter.print(element) === IdentityPrinter.print(elements[0]));
|
|
2280
2516
|
for (const element of elements) {
|
|
2281
2517
|
const printed = IdentityPrinter.print(element.open_tag);
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2518
|
+
if (bodiesMatch) {
|
|
2519
|
+
const autofixContext = state.isFirstOffense
|
|
2520
|
+
? { node: conditionalNode }
|
|
2521
|
+
: undefined;
|
|
2522
|
+
this.addOffense(`The \`${printed}\` element is duplicated across all branches of this conditional and can be moved outside.`, element.location, autofixContext);
|
|
2523
|
+
state.isFirstOffense = false;
|
|
2524
|
+
}
|
|
2525
|
+
else {
|
|
2526
|
+
const autofixContext = state.isFirstOffense
|
|
2527
|
+
? { node: conditionalNode }
|
|
2528
|
+
: undefined;
|
|
2529
|
+
const tagNameLocation = isHTMLOpenTagNode(element.open_tag) && element.open_tag.tag_name?.location
|
|
2530
|
+
? element.open_tag.tag_name.location
|
|
2531
|
+
: element?.open_tag?.location || element.location;
|
|
2532
|
+
this.addOffense(`The \`${printed}\` tag is repeated across all branches with different content. Consider extracting the shared tag outside the conditional.`, tagNameLocation, autofixContext, "hint");
|
|
2533
|
+
state.isFirstOffense = false;
|
|
2534
|
+
}
|
|
2287
2535
|
}
|
|
2288
2536
|
if (!bodiesMatch && bodies.every(body => body.length > 0)) {
|
|
2289
2537
|
this.checkBranches(bodies, conditionalNode, state);
|
|
@@ -2312,6 +2560,15 @@ class ERBNoDuplicateBranchElementsRule extends ParserRule {
|
|
|
2312
2560
|
const branches = collectBranches(conditionalNode);
|
|
2313
2561
|
if (!branches)
|
|
2314
2562
|
return null;
|
|
2563
|
+
if (offense.autofixContext.allIdentical) {
|
|
2564
|
+
const parentInfo = findParentArray(result.value, conditionalNode);
|
|
2565
|
+
if (!parentInfo)
|
|
2566
|
+
return null;
|
|
2567
|
+
const { array: parentArray, index: conditionalIndex } = parentInfo;
|
|
2568
|
+
const firstBranchContent = trimWhitespaceNodes(branches[0]);
|
|
2569
|
+
parentArray.splice(conditionalIndex, 1, ...firstBranchContent);
|
|
2570
|
+
return result;
|
|
2571
|
+
}
|
|
2315
2572
|
const significantBranches = branches.map(getSignificantNodes);
|
|
2316
2573
|
if (significantBranches.some(branch => branch.length === 0))
|
|
2317
2574
|
return null;
|
|
@@ -2325,23 +2582,51 @@ class ERBNoDuplicateBranchElementsRule extends ParserRule {
|
|
|
2325
2582
|
return null;
|
|
2326
2583
|
let { array: parentArray, index: conditionalIndex } = parentInfo;
|
|
2327
2584
|
let hasWrapped = false;
|
|
2585
|
+
let didMutate = false;
|
|
2586
|
+
let failedToHoistPrefix = false;
|
|
2587
|
+
let hoistedBefore = false;
|
|
2328
2588
|
const hoistElement = (elements, position) => {
|
|
2589
|
+
const actualPosition = (position === "before" && failedToHoistPrefix) ? "after" : position;
|
|
2329
2590
|
const bodiesMatch = elements.every(element => IdentityPrinter.print(element) === IdentityPrinter.print(elements[0]));
|
|
2330
2591
|
if (bodiesMatch) {
|
|
2592
|
+
if (actualPosition === "after") {
|
|
2593
|
+
const currentLengths = branches.map(b => getSignificantNodes(b).length);
|
|
2594
|
+
if (currentLengths.some(l => l !== currentLengths[0]))
|
|
2595
|
+
return;
|
|
2596
|
+
}
|
|
2597
|
+
if (actualPosition === "after" && position === "before") {
|
|
2598
|
+
const isAtEnd = branches.every((branch, index) => {
|
|
2599
|
+
const nodes = getSignificantNodes(branch);
|
|
2600
|
+
return nodes.length > 0 && nodes[nodes.length - 1] === elements[index];
|
|
2601
|
+
});
|
|
2602
|
+
if (!isAtEnd)
|
|
2603
|
+
return;
|
|
2604
|
+
}
|
|
2331
2605
|
for (let i = 0; i < branches.length; i++) {
|
|
2332
2606
|
removeNodeFromArray(branches[i], elements[i]);
|
|
2333
2607
|
}
|
|
2334
|
-
if (
|
|
2335
|
-
parentArray.splice(conditionalIndex, 0, elements[0]);
|
|
2336
|
-
conditionalIndex
|
|
2608
|
+
if (actualPosition === "before") {
|
|
2609
|
+
parentArray.splice(conditionalIndex, 0, elements[0], createLiteral("\n"));
|
|
2610
|
+
conditionalIndex += 2;
|
|
2611
|
+
hoistedBefore = true;
|
|
2337
2612
|
}
|
|
2338
2613
|
else {
|
|
2339
|
-
parentArray.splice(conditionalIndex + 1, 0, elements[0]);
|
|
2614
|
+
parentArray.splice(conditionalIndex + 1, 0, createLiteral("\n"), elements[0]);
|
|
2340
2615
|
}
|
|
2616
|
+
didMutate = true;
|
|
2341
2617
|
}
|
|
2342
2618
|
else {
|
|
2343
2619
|
if (hasWrapped)
|
|
2344
2620
|
return;
|
|
2621
|
+
const canWrap = branches.every((branch, index) => {
|
|
2622
|
+
const remaining = getSignificantNodes(branch);
|
|
2623
|
+
return remaining.length === 1 && remaining[0] === elements[index];
|
|
2624
|
+
});
|
|
2625
|
+
if (!canWrap) {
|
|
2626
|
+
if (position === "before")
|
|
2627
|
+
failedToHoistPrefix = true;
|
|
2628
|
+
return;
|
|
2629
|
+
}
|
|
2345
2630
|
for (let i = 0; i < branches.length; i++) {
|
|
2346
2631
|
replaceNodeWithBody(branches[i], elements[i]);
|
|
2347
2632
|
}
|
|
@@ -2350,6 +2635,7 @@ class ERBNoDuplicateBranchElementsRule extends ParserRule {
|
|
|
2350
2635
|
parentArray = wrapper.body;
|
|
2351
2636
|
conditionalIndex = 1;
|
|
2352
2637
|
hasWrapped = true;
|
|
2638
|
+
didMutate = true;
|
|
2353
2639
|
}
|
|
2354
2640
|
};
|
|
2355
2641
|
for (let index = 0; index < prefixCount; index++) {
|
|
@@ -2360,7 +2646,22 @@ class ERBNoDuplicateBranchElementsRule extends ParserRule {
|
|
|
2360
2646
|
const elements = significantBranches.map(branch => branch[branch.length - 1 - offset]);
|
|
2361
2647
|
hoistElement(elements, "after");
|
|
2362
2648
|
}
|
|
2363
|
-
|
|
2649
|
+
if (!hasWrapped && hoistedBefore) {
|
|
2650
|
+
const remaining = branches.map(branch => getSignificantNodes(branch));
|
|
2651
|
+
if (remaining.every(branch => branch.length === 1) && allEquivalentElements(remaining.map(b => b[0]))) {
|
|
2652
|
+
const elements = remaining.map(b => b[0]);
|
|
2653
|
+
const bodiesMatch = elements.every(el => IdentityPrinter.print(el) === IdentityPrinter.print(elements[0]));
|
|
2654
|
+
if (!bodiesMatch && elements.every(el => el.body.length > 0)) {
|
|
2655
|
+
for (let i = 0; i < branches.length; i++) {
|
|
2656
|
+
replaceNodeWithBody(branches[i], elements[i]);
|
|
2657
|
+
}
|
|
2658
|
+
const wrapper = createWrapper(elements[0], [createLiteral("\n"), conditionalNode, createLiteral("\n")]);
|
|
2659
|
+
parentArray[conditionalIndex] = wrapper;
|
|
2660
|
+
didMutate = true;
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
return didMutate ? result : null;
|
|
2364
2665
|
}
|
|
2365
2666
|
}
|
|
2366
2667
|
|
|
@@ -2886,6 +3187,47 @@ class ERBNoRawOutputInAttributeValueRule extends ParserRule {
|
|
|
2886
3187
|
}
|
|
2887
3188
|
}
|
|
2888
3189
|
|
|
3190
|
+
function isAssignmentNode(prismNode) {
|
|
3191
|
+
const type = prismNode?.constructor?.name;
|
|
3192
|
+
if (!type)
|
|
3193
|
+
return false;
|
|
3194
|
+
return type.endsWith("WriteNode");
|
|
3195
|
+
}
|
|
3196
|
+
class ERBNoSilentStatementVisitor extends BaseRuleVisitor {
|
|
3197
|
+
visitERBContentNode(node) {
|
|
3198
|
+
if (isERBOutputNode(node))
|
|
3199
|
+
return;
|
|
3200
|
+
const prismNode = node.prismNode;
|
|
3201
|
+
if (!prismNode)
|
|
3202
|
+
return;
|
|
3203
|
+
if (isAssignmentNode(prismNode))
|
|
3204
|
+
return;
|
|
3205
|
+
const content = node.content?.value?.trim();
|
|
3206
|
+
if (!content)
|
|
3207
|
+
return;
|
|
3208
|
+
this.addOffense(`Avoid using silent ERB tags for statements. Move \`${content}\` to a controller, helper, or presenter.`, node.location);
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
class ERBNoSilentStatementRule extends ParserRule {
|
|
3212
|
+
static ruleName = "erb-no-silent-statement";
|
|
3213
|
+
get defaultConfig() {
|
|
3214
|
+
return {
|
|
3215
|
+
enabled: false,
|
|
3216
|
+
severity: "warning"
|
|
3217
|
+
};
|
|
3218
|
+
}
|
|
3219
|
+
get parserOptions() {
|
|
3220
|
+
return {
|
|
3221
|
+
prism_nodes: true,
|
|
3222
|
+
};
|
|
3223
|
+
}
|
|
3224
|
+
check(result, context) {
|
|
3225
|
+
const visitor = new ERBNoSilentStatementVisitor(this.ruleName, context);
|
|
3226
|
+
visitor.visit(result.value);
|
|
3227
|
+
return visitor.offenses;
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
|
|
2889
3231
|
class ERBNoSilentTagInAttributeNameVisitor extends BaseRuleVisitor {
|
|
2890
3232
|
visitHTMLAttributeNameNode(node) {
|
|
2891
3233
|
const erbNodes = filterERBContentNodes(node.children);
|
|
@@ -3148,7 +3490,7 @@ class ERBNoTrailingWhitespaceRule extends ParserRule {
|
|
|
3148
3490
|
}
|
|
3149
3491
|
|
|
3150
3492
|
const JS_ATTRIBUTE_PATTERN = /^on/i;
|
|
3151
|
-
const SAFE_PATTERN
|
|
3493
|
+
const SAFE_PATTERN = /\.to_json\s*$|\bj\s*[\s(]|\bescape_javascript\s*[\s(]/;
|
|
3152
3494
|
class ERBNoUnsafeJSAttributeVisitor extends AttributeVisitorMixin {
|
|
3153
3495
|
checkStaticAttributeDynamicValue({ attributeName, valueNodes }) {
|
|
3154
3496
|
if (!JS_ATTRIBUTE_PATTERN.test(attributeName))
|
|
@@ -3159,7 +3501,7 @@ class ERBNoUnsafeJSAttributeVisitor extends AttributeVisitorMixin {
|
|
|
3159
3501
|
if (!isERBOutputNode(node))
|
|
3160
3502
|
continue;
|
|
3161
3503
|
const content = node.content?.value?.trim() || "";
|
|
3162
|
-
if (SAFE_PATTERN
|
|
3504
|
+
if (SAFE_PATTERN.test(content))
|
|
3163
3505
|
continue;
|
|
3164
3506
|
this.addOffense(`Unsafe ERB output in \`${attributeName}\` attribute. Use \`.to_json\`, \`j()\`, or \`escape_javascript()\` to safely encode values.`, node.location);
|
|
3165
3507
|
}
|
|
@@ -3240,7 +3582,27 @@ class ERBNoUnsafeRawRule extends ParserRule {
|
|
|
3240
3582
|
}
|
|
3241
3583
|
}
|
|
3242
3584
|
|
|
3243
|
-
const
|
|
3585
|
+
const SAFE_METHOD_NAMES = new Set([
|
|
3586
|
+
"to_json",
|
|
3587
|
+
"json_escape",
|
|
3588
|
+
]);
|
|
3589
|
+
const ESCAPE_JAVASCRIPT_METHOD_NAMES = new Set([
|
|
3590
|
+
"j",
|
|
3591
|
+
"escape_javascript",
|
|
3592
|
+
]);
|
|
3593
|
+
class SafeCallDetector extends PrismVisitor {
|
|
3594
|
+
hasSafeCall = false;
|
|
3595
|
+
hasEscapeJavascriptCall = false;
|
|
3596
|
+
visitCallNode(node) {
|
|
3597
|
+
if (SAFE_METHOD_NAMES.has(node.name)) {
|
|
3598
|
+
this.hasSafeCall = true;
|
|
3599
|
+
}
|
|
3600
|
+
if (ESCAPE_JAVASCRIPT_METHOD_NAMES.has(node.name)) {
|
|
3601
|
+
this.hasEscapeJavascriptCall = true;
|
|
3602
|
+
}
|
|
3603
|
+
this.visitChildNodes(node);
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3244
3606
|
class ERBNoUnsafeScriptInterpolationVisitor extends BaseRuleVisitor {
|
|
3245
3607
|
visitHTMLElementNode(node) {
|
|
3246
3608
|
if (!isHTMLOpenTagNode(node.open_tag)) {
|
|
@@ -3269,9 +3631,17 @@ class ERBNoUnsafeScriptInterpolationVisitor extends BaseRuleVisitor {
|
|
|
3269
3631
|
continue;
|
|
3270
3632
|
if (!isERBOutputNode(child))
|
|
3271
3633
|
continue;
|
|
3272
|
-
const
|
|
3273
|
-
|
|
3634
|
+
const erbContent = child;
|
|
3635
|
+
const prismNode = erbContent.prismNode;
|
|
3636
|
+
const detector = new SafeCallDetector();
|
|
3637
|
+
if (prismNode)
|
|
3638
|
+
detector.visit(prismNode);
|
|
3639
|
+
if (detector.hasSafeCall)
|
|
3640
|
+
continue;
|
|
3641
|
+
if (detector.hasEscapeJavascriptCall) {
|
|
3642
|
+
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);
|
|
3274
3643
|
continue;
|
|
3644
|
+
}
|
|
3275
3645
|
this.addOffense("Unsafe ERB output in `<script>` tag. Use `.to_json` to safely serialize values into JavaScript.", child.location);
|
|
3276
3646
|
}
|
|
3277
3647
|
}
|
|
@@ -3284,6 +3654,11 @@ class ERBNoUnsafeScriptInterpolationRule extends ParserRule {
|
|
|
3284
3654
|
severity: "error"
|
|
3285
3655
|
};
|
|
3286
3656
|
}
|
|
3657
|
+
get parserOptions() {
|
|
3658
|
+
return {
|
|
3659
|
+
prism_nodes: true,
|
|
3660
|
+
};
|
|
3661
|
+
}
|
|
3287
3662
|
check(result, context) {
|
|
3288
3663
|
const visitor = new ERBNoUnsafeScriptInterpolationVisitor(this.ruleName, context);
|
|
3289
3664
|
visitor.visit(result.value);
|
|
@@ -4291,7 +4666,7 @@ class HerbDisableCommentValidRuleNameRule extends ParserRule {
|
|
|
4291
4666
|
}
|
|
4292
4667
|
}
|
|
4293
4668
|
|
|
4294
|
-
const ALLOWED_TYPES = ["text/javascript"];
|
|
4669
|
+
const ALLOWED_TYPES = ["text/javascript", "module", "importmap", "speculationrules"];
|
|
4295
4670
|
class AllowedScriptTypeVisitor extends BaseRuleVisitor {
|
|
4296
4671
|
visitHTMLOpenTagNode(node) {
|
|
4297
4672
|
if (getTagLocalName(node) === "script") {
|
|
@@ -4829,6 +5204,47 @@ class HTMLBodyOnlyElementsRule extends ParserRule {
|
|
|
4829
5204
|
}
|
|
4830
5205
|
}
|
|
4831
5206
|
|
|
5207
|
+
class BooleanAttributesNoValueVisitor extends AttributeVisitorMixin {
|
|
5208
|
+
checkStaticAttributeStaticValue({ originalAttributeName, attributeNode }) {
|
|
5209
|
+
this.checkAttribute(originalAttributeName, attributeNode);
|
|
5210
|
+
}
|
|
5211
|
+
checkStaticAttributeDynamicValue({ originalAttributeName, attributeNode }) {
|
|
5212
|
+
this.checkAttribute(originalAttributeName, attributeNode);
|
|
5213
|
+
}
|
|
5214
|
+
checkAttribute(attributeName, attributeNode) {
|
|
5215
|
+
if (!isBooleanAttribute(attributeName))
|
|
5216
|
+
return;
|
|
5217
|
+
if (!hasAttributeValue(attributeNode))
|
|
5218
|
+
return;
|
|
5219
|
+
this.addOffense(`Boolean attribute \`${IdentityPrinter.print(attributeNode.name)}\` should not have a value. Use \`${attributeName.toLowerCase()}\` instead of \`${IdentityPrinter.print(attributeNode)}\`.`, attributeNode.value.location, {
|
|
5220
|
+
node: attributeNode
|
|
5221
|
+
});
|
|
5222
|
+
}
|
|
5223
|
+
}
|
|
5224
|
+
class HTMLBooleanAttributesNoValueRule extends ParserRule {
|
|
5225
|
+
static autocorrectable = true;
|
|
5226
|
+
static ruleName = "html-boolean-attributes-no-value";
|
|
5227
|
+
get defaultConfig() {
|
|
5228
|
+
return {
|
|
5229
|
+
enabled: true,
|
|
5230
|
+
severity: "error"
|
|
5231
|
+
};
|
|
5232
|
+
}
|
|
5233
|
+
check(result, context) {
|
|
5234
|
+
const visitor = new BooleanAttributesNoValueVisitor(this.ruleName, context);
|
|
5235
|
+
visitor.visit(result.value);
|
|
5236
|
+
return visitor.offenses;
|
|
5237
|
+
}
|
|
5238
|
+
autofix(offense, result, _context) {
|
|
5239
|
+
if (!offense.autofixContext)
|
|
5240
|
+
return null;
|
|
5241
|
+
const { node } = offense.autofixContext;
|
|
5242
|
+
node.equals = null;
|
|
5243
|
+
node.value = null;
|
|
5244
|
+
return result;
|
|
5245
|
+
}
|
|
5246
|
+
}
|
|
5247
|
+
|
|
4832
5248
|
class DetailsHasSummaryVisitor extends BaseRuleVisitor {
|
|
4833
5249
|
visitHTMLElementNode(node) {
|
|
4834
5250
|
this.checkDetailsElement(node);
|
|
@@ -4878,47 +5294,6 @@ class HTMLDetailsHasSummaryRule extends ParserRule {
|
|
|
4878
5294
|
}
|
|
4879
5295
|
}
|
|
4880
5296
|
|
|
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
5297
|
class HeadOnlyElementsVisitor extends BaseRuleVisitor {
|
|
4923
5298
|
elementStack = [];
|
|
4924
5299
|
visitHTMLElementNode(node) {
|
|
@@ -6601,8 +6976,10 @@ class TurboPermanentRequireIdRule extends ParserRule {
|
|
|
6601
6976
|
|
|
6602
6977
|
const rules = [
|
|
6603
6978
|
ActionViewNoSilentHelperRule,
|
|
6979
|
+
ActionViewNoSilentRenderRule,
|
|
6604
6980
|
ERBCommentSyntax,
|
|
6605
6981
|
ERBNoCaseNodeChildrenRule,
|
|
6982
|
+
ERBNoEmptyControlFlowRule,
|
|
6606
6983
|
ERBNoConditionalHTMLElementRule,
|
|
6607
6984
|
ERBNoConditionalOpenTagRule,
|
|
6608
6985
|
ERBNoDuplicateBranchElementsRule,
|
|
@@ -6617,6 +6994,7 @@ const rules = [
|
|
|
6617
6994
|
ERBNoOutputInAttributeNameRule,
|
|
6618
6995
|
ERBNoOutputInAttributePositionRule,
|
|
6619
6996
|
ERBNoRawOutputInAttributeValueRule,
|
|
6997
|
+
ERBNoSilentStatementRule,
|
|
6620
6998
|
ERBNoSilentTagInAttributeNameRule,
|
|
6621
6999
|
ERBNoStatementInScriptRule,
|
|
6622
7000
|
ERBNoThenInControlFlowRule,
|
|
@@ -6648,8 +7026,8 @@ const rules = [
|
|
|
6648
7026
|
HTMLAttributeValuesRequireQuotesRule,
|
|
6649
7027
|
HTMLAvoidBothDisabledAndAriaDisabledRule,
|
|
6650
7028
|
HTMLBodyOnlyElementsRule,
|
|
6651
|
-
HTMLDetailsHasSummaryRule,
|
|
6652
7029
|
HTMLBooleanAttributesNoValueRule,
|
|
7030
|
+
HTMLDetailsHasSummaryRule,
|
|
6653
7031
|
HTMLHeadOnlyElementsRule,
|
|
6654
7032
|
HTMLIframeHasTitleRule,
|
|
6655
7033
|
HTMLImgRequireAltRule,
|
|
@@ -7207,5 +7585,5 @@ function ruleDocumentationUrl(ruleId) {
|
|
|
7207
7585
|
return `${DOCS_BASE_URL}/${ruleId}`;
|
|
7208
7586
|
}
|
|
7209
7587
|
|
|
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,
|
|
7588
|
+
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_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, isDocumentOnlyTag, isHeadAndBodyTag, isHeadOnlyTag, isHeadTag, isHtmlOnlyTag, isInlineElement, isPartialFile, isVoidElement, locationFromOffset, locationsEqual, positionFromOffset, ruleDocumentationUrl, rules, splitByTopLevelComma };
|
|
7211
7589
|
//# sourceMappingURL=index.js.map
|