oxlint-plugin-react-doctor 0.7.5 → 0.7.6-dev.0150fc9
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/dist/index.js +1744 -817
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
|
+
import { KEYS } from "eslint-visitor-keys";
|
|
1
2
|
import * as path from "node:path";
|
|
2
3
|
import * as fs from "node:fs";
|
|
3
4
|
import { readFileSync } from "node:fs";
|
|
4
|
-
import { parseSync } from "oxc-parser";
|
|
5
|
+
import { parseSync, visitorKeys } from "oxc-parser";
|
|
5
6
|
import { analyze } from "eslint-scope";
|
|
6
|
-
import * as eslintVisitorKeys from "eslint-visitor-keys";
|
|
7
|
-
//#region src/plugin/utils/has-type-property.ts
|
|
8
|
-
const hasTypeProperty = (value) => Boolean(value && typeof value === "object" && "type" in value);
|
|
9
|
-
//#endregion
|
|
10
7
|
//#region src/plugin/utils/is-node-of-type.ts
|
|
11
|
-
const isNodeOfType = (node, type) =>
|
|
8
|
+
const isNodeOfType = (node, type) => node !== null && typeof node === "object" && "type" in node && node.type === type;
|
|
12
9
|
//#endregion
|
|
13
10
|
//#region src/plugin/utils/non-react-jsx-dialect.ts
|
|
14
11
|
const NON_REACT_JSX_DIALECT_PACKAGES = new Set([
|
|
@@ -400,6 +397,7 @@ const isProductionFilePath = (relativePath, sourceFilePattern) => {
|
|
|
400
397
|
//#endregion
|
|
401
398
|
//#region src/plugin/rules/security-scan/utils/is-production-source-path.ts
|
|
402
399
|
const isProductionSourcePath = (relativePath) => {
|
|
400
|
+
if (/\.d\.[cm]?[jt]s$/i.test(relativePath)) return false;
|
|
403
401
|
return isProductionFilePath(relativePath, SOURCE_FILE_PATTERN);
|
|
404
402
|
};
|
|
405
403
|
//#endregion
|
|
@@ -565,7 +563,8 @@ const REACT_RUNTIME_MODULE_SOURCES = new Set([
|
|
|
565
563
|
"react",
|
|
566
564
|
"react-dom",
|
|
567
565
|
"preact/compat",
|
|
568
|
-
"preact/hooks"
|
|
566
|
+
"preact/hooks",
|
|
567
|
+
"@wordpress/element"
|
|
569
568
|
]);
|
|
570
569
|
const REACT_ECOSYSTEM_PACKAGE_NAMES = new Set([
|
|
571
570
|
"next",
|
|
@@ -802,24 +801,55 @@ const isHookCall$2 = (node, hookName) => {
|
|
|
802
801
|
};
|
|
803
802
|
//#endregion
|
|
804
803
|
//#region src/plugin/utils/is-ast-node.ts
|
|
805
|
-
const isAstNode = (value) =>
|
|
806
|
-
|
|
807
|
-
|
|
804
|
+
const isAstNode = (value) => value !== null && typeof value === "object" && "type" in value && typeof value.type === "string";
|
|
805
|
+
//#endregion
|
|
806
|
+
//#region src/plugin/utils/runtime-visitor-keys.ts
|
|
807
|
+
const RUNTIME_VISITOR_KEYS = {
|
|
808
|
+
...KEYS,
|
|
809
|
+
ArrayPattern: ["decorators", ...KEYS.ArrayPattern],
|
|
810
|
+
AssignmentPattern: ["decorators", ...KEYS.AssignmentPattern],
|
|
811
|
+
ClassDeclaration: ["decorators", ...KEYS.ClassDeclaration],
|
|
812
|
+
ClassExpression: ["decorators", ...KEYS.ClassExpression],
|
|
813
|
+
Identifier: ["decorators", ...KEYS.Identifier],
|
|
814
|
+
MethodDefinition: ["decorators", ...KEYS.MethodDefinition],
|
|
815
|
+
ObjectPattern: ["decorators", ...KEYS.ObjectPattern],
|
|
816
|
+
PropertyDefinition: ["decorators", ...KEYS.PropertyDefinition],
|
|
817
|
+
RestElement: ["decorators", ...KEYS.RestElement]
|
|
808
818
|
};
|
|
809
819
|
//#endregion
|
|
810
820
|
//#region src/plugin/utils/walk-ast.ts
|
|
811
|
-
const
|
|
812
|
-
if (!node || typeof node !== "object") return;
|
|
813
|
-
if (visitor(node) === false) return;
|
|
821
|
+
const forEachChildNode = (node, visit) => {
|
|
814
822
|
const nodeRecord = node;
|
|
815
|
-
|
|
816
|
-
|
|
823
|
+
const childKeys = RUNTIME_VISITOR_KEYS[node.type];
|
|
824
|
+
if (childKeys !== void 0) {
|
|
825
|
+
for (let keyIndex = 0; keyIndex < childKeys.length; keyIndex += 1) {
|
|
826
|
+
const child = nodeRecord[childKeys[keyIndex]];
|
|
827
|
+
if (Array.isArray(child)) for (let itemIndex = 0; itemIndex < child.length; itemIndex += 1) {
|
|
828
|
+
const item = child[itemIndex];
|
|
829
|
+
if (isAstNode(item)) visit(item);
|
|
830
|
+
}
|
|
831
|
+
else if (isAstNode(child)) visit(child);
|
|
832
|
+
}
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
for (const key in nodeRecord) {
|
|
836
|
+
if (key === "parent" || !Object.hasOwn(nodeRecord, key)) continue;
|
|
817
837
|
const child = nodeRecord[key];
|
|
818
|
-
if (Array.isArray(child)) {
|
|
819
|
-
|
|
820
|
-
|
|
838
|
+
if (Array.isArray(child)) for (let itemIndex = 0; itemIndex < child.length; itemIndex += 1) {
|
|
839
|
+
const item = child[itemIndex];
|
|
840
|
+
if (isAstNode(item)) visit(item);
|
|
841
|
+
}
|
|
842
|
+
else if (isAstNode(child)) visit(child);
|
|
821
843
|
}
|
|
822
844
|
};
|
|
845
|
+
const walkAst = (node, visitor) => {
|
|
846
|
+
if (!node || typeof node !== "object") return;
|
|
847
|
+
const visitNode = (current) => {
|
|
848
|
+
if (visitor(current) === false) return;
|
|
849
|
+
forEachChildNode(current, visitNode);
|
|
850
|
+
};
|
|
851
|
+
visitNode(node);
|
|
852
|
+
};
|
|
823
853
|
//#endregion
|
|
824
854
|
//#region src/plugin/rules/state-and-effects/activity-wraps-effect-heavy-subtree.ts
|
|
825
855
|
const ACTIVITY_IMPORTED_NAMES = new Set(["Activity", "unstable_Activity"]);
|
|
@@ -1262,7 +1292,7 @@ const REGEX_PRECEDING_KEYWORDS = new Set([
|
|
|
1262
1292
|
"await",
|
|
1263
1293
|
"throw"
|
|
1264
1294
|
]);
|
|
1265
|
-
const isRegexLiteralStart = (characters, slashIndex) => {
|
|
1295
|
+
const isRegexLiteralStart = (content, characters, slashIndex) => {
|
|
1266
1296
|
let cursor = slashIndex - 1;
|
|
1267
1297
|
while (cursor >= 0 && WHITESPACE_PATTERN.test(characters[cursor])) cursor -= 1;
|
|
1268
1298
|
if (cursor < 0) return true;
|
|
@@ -1272,7 +1302,7 @@ const isRegexLiteralStart = (characters, slashIndex) => {
|
|
|
1272
1302
|
let wordStartIndex = cursor;
|
|
1273
1303
|
while (wordStartIndex > 0 && IDENTIFIER_CHARACTER_PATTERN.test(characters[wordStartIndex - 1])) wordStartIndex -= 1;
|
|
1274
1304
|
if (wordStartIndex > 0 && characters[wordStartIndex - 1] === ".") return false;
|
|
1275
|
-
return REGEX_PRECEDING_KEYWORDS.has(
|
|
1305
|
+
return REGEX_PRECEDING_KEYWORDS.has(content.slice(wordStartIndex, cursor + 1));
|
|
1276
1306
|
}
|
|
1277
1307
|
if (previousCharacter === "<") return false;
|
|
1278
1308
|
if (previousCharacter === ">") return characterBefore === "=";
|
|
@@ -1313,13 +1343,15 @@ const quotedLiteralHasWhitespace = (content, openQuoteIndex, delimiter) => {
|
|
|
1313
1343
|
return false;
|
|
1314
1344
|
};
|
|
1315
1345
|
const blankNonCodePreservingPositions = (content, blankStringContents) => {
|
|
1316
|
-
|
|
1346
|
+
let characters = null;
|
|
1317
1347
|
let stringDelimiter = null;
|
|
1318
1348
|
let isBlankingString = false;
|
|
1319
1349
|
const templateExpressionDepths = [];
|
|
1320
1350
|
let index = 0;
|
|
1321
1351
|
const blankUnlessNewline = (offset) => {
|
|
1322
|
-
if (offset
|
|
1352
|
+
if (offset >= content.length || content[offset] === "\n") return;
|
|
1353
|
+
characters ??= content.split("");
|
|
1354
|
+
characters[offset] = " ";
|
|
1323
1355
|
};
|
|
1324
1356
|
while (index < content.length) {
|
|
1325
1357
|
const character = content[index];
|
|
@@ -1366,6 +1398,7 @@ const blankNonCodePreservingPositions = (content, blankStringContents) => {
|
|
|
1366
1398
|
continue;
|
|
1367
1399
|
}
|
|
1368
1400
|
if (character === "/" && nextCharacter === "/") {
|
|
1401
|
+
characters ??= content.split("");
|
|
1369
1402
|
while (index < content.length && content[index] !== "\n") {
|
|
1370
1403
|
characters[index] = " ";
|
|
1371
1404
|
index += 1;
|
|
@@ -1373,6 +1406,7 @@ const blankNonCodePreservingPositions = (content, blankStringContents) => {
|
|
|
1373
1406
|
continue;
|
|
1374
1407
|
}
|
|
1375
1408
|
if (character === "/" && nextCharacter === "*") {
|
|
1409
|
+
characters ??= content.split("");
|
|
1376
1410
|
while (index < content.length) {
|
|
1377
1411
|
if (content[index] === "*" && content[index + 1] === "/") {
|
|
1378
1412
|
characters[index] = " ";
|
|
@@ -1386,7 +1420,7 @@ const blankNonCodePreservingPositions = (content, blankStringContents) => {
|
|
|
1386
1420
|
continue;
|
|
1387
1421
|
}
|
|
1388
1422
|
if (character === "/") {
|
|
1389
|
-
const regexEndIndex = isRegexLiteralStart(characters, index) ? findRegexLiteralEnd(content, index) : null;
|
|
1423
|
+
const regexEndIndex = isRegexLiteralStart(content, characters ?? content, index) ? findRegexLiteralEnd(content, index) : null;
|
|
1390
1424
|
if (regexEndIndex !== null) {
|
|
1391
1425
|
if (blankStringContents) for (let interiorIndex = index + 1; interiorIndex < regexEndIndex - 1; interiorIndex += 1) blankUnlessNewline(interiorIndex);
|
|
1392
1426
|
index = regexEndIndex;
|
|
@@ -1404,9 +1438,9 @@ const blankNonCodePreservingPositions = (content, blankStringContents) => {
|
|
|
1404
1438
|
}
|
|
1405
1439
|
index += 1;
|
|
1406
1440
|
}
|
|
1407
|
-
return characters
|
|
1441
|
+
return characters?.join("") ?? content;
|
|
1408
1442
|
};
|
|
1409
|
-
const stripCommentsPreservingPositions = (content) => blankNonCodePreservingPositions(content, false);
|
|
1443
|
+
const stripCommentsPreservingPositions = (content) => content.includes("//") || content.includes("/*") ? blankNonCodePreservingPositions(content, false) : content;
|
|
1410
1444
|
const stripCommentsAndStringLiteralsPreservingPositions = (content) => blankNonCodePreservingPositions(content, true);
|
|
1411
1445
|
//#endregion
|
|
1412
1446
|
//#region src/plugin/rules/security-scan/utils/scan-by-pattern.ts
|
|
@@ -1425,10 +1459,15 @@ const scanByPattern = ({ shouldScan, pattern, requireAll, suppressWhen, ignoreSt
|
|
|
1425
1459
|
if (!shouldScan(file)) return [];
|
|
1426
1460
|
const content = getScannableContent(file, ignoreStringLiterals);
|
|
1427
1461
|
if (requireAll !== void 0 && !requireAll.every((gate) => gate.test(content))) return [];
|
|
1428
|
-
|
|
1429
|
-
if (
|
|
1462
|
+
let matchIndex = -1;
|
|
1463
|
+
if (pattern instanceof RegExp) matchIndex = content.search(pattern);
|
|
1464
|
+
else for (let patternIndex = 0; patternIndex < pattern.length; patternIndex += 1) {
|
|
1465
|
+
matchIndex = content.search(pattern[patternIndex]);
|
|
1466
|
+
if (matchIndex >= 0) break;
|
|
1467
|
+
}
|
|
1468
|
+
if (matchIndex < 0) return [];
|
|
1430
1469
|
if (suppressWhen !== void 0 && suppressWhen.test(content)) return [];
|
|
1431
|
-
const { line, column } =
|
|
1470
|
+
const { line, column } = getLocationAtIndex(content, matchIndex);
|
|
1432
1471
|
return [{
|
|
1433
1472
|
message,
|
|
1434
1473
|
line,
|
|
@@ -1438,6 +1477,7 @@ const scanByPattern = ({ shouldScan, pattern, requireAll, suppressWhen, ignoreSt
|
|
|
1438
1477
|
//#endregion
|
|
1439
1478
|
//#region src/plugin/rules/security-scan/agent-tool-capability-risk.ts
|
|
1440
1479
|
const AGENT_TOOL_DEFINITION_PATTERN = /\b(?:tool\s*\(\s*\{|createTool\s*\(|defineTool\s*\(|new\s+(?:DynamicTool|StructuredTool)\s*\()/;
|
|
1480
|
+
const AGENT_TOOL_DEFINITION_PREFILTER_PATTERN = /\b(?:tool|createTool|defineTool|DynamicTool|StructuredTool)\b/;
|
|
1441
1481
|
const AGENT_TOOL_CONTEXT_PATH_PATTERN = /(?:^|\/)(?:agents?|tools?|mcp)(?:\/|$)|(?:agent|tool|mcp)[^/]*\.[cm]?[jt]sx?$/i;
|
|
1442
1482
|
const agentToolCapabilityRisk = defineRule({
|
|
1443
1483
|
id: "agent-tool-capability-risk",
|
|
@@ -1445,7 +1485,7 @@ const agentToolCapabilityRisk = defineRule({
|
|
|
1445
1485
|
severity: "warn",
|
|
1446
1486
|
recommendation: "Treat tool inputs as prompt-injection controlled. Validate arguments, scope permissions per call, and avoid exposing shell/file/network primitives directly to agents.",
|
|
1447
1487
|
scan: scanByPattern({
|
|
1448
|
-
shouldScan: (file) => isProductionSourcePath(file.relativePath) && AGENT_TOOL_CONTEXT_PATH_PATTERN.test(file.relativePath),
|
|
1488
|
+
shouldScan: (file) => isProductionSourcePath(file.relativePath) && AGENT_TOOL_CONTEXT_PATH_PATTERN.test(file.relativePath) && AGENT_TOOL_DEFINITION_PREFILTER_PATTERN.test(file.content) && AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN.test(file.content),
|
|
1449
1489
|
pattern: AGENT_TOOL_DEFINITION_PATTERN,
|
|
1450
1490
|
requireAll: [AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN],
|
|
1451
1491
|
ignoreStringLiterals: true,
|
|
@@ -1478,6 +1518,37 @@ const hasJsxPropIgnoreCase = (attributes, targetProp) => {
|
|
|
1478
1518
|
}
|
|
1479
1519
|
};
|
|
1480
1520
|
//#endregion
|
|
1521
|
+
//#region src/plugin/utils/is-uppercase-name.ts
|
|
1522
|
+
const isUppercaseName = (name) => UPPERCASE_PATTERN.test(name);
|
|
1523
|
+
//#endregion
|
|
1524
|
+
//#region src/plugin/utils/resolve-jsx-element-type.ts
|
|
1525
|
+
const resolveConstantStringBinding = (name) => {
|
|
1526
|
+
const binding = findVariableInitializer(name, name.name);
|
|
1527
|
+
if (!binding?.initializer) return null;
|
|
1528
|
+
const declarator = binding.bindingIdentifier.parent;
|
|
1529
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator")) return null;
|
|
1530
|
+
if (declarator.id !== binding.bindingIdentifier) return null;
|
|
1531
|
+
const declaration = declarator.parent;
|
|
1532
|
+
if (!declaration || !isNodeOfType(declaration, "VariableDeclaration")) return null;
|
|
1533
|
+
if (declaration.kind !== "const") return null;
|
|
1534
|
+
const initializer = stripParenExpression(binding.initializer);
|
|
1535
|
+
return isNodeOfType(initializer, "Literal") && typeof initializer.value === "string" ? initializer.value : null;
|
|
1536
|
+
};
|
|
1537
|
+
const flattenJsxName$2 = (name) => {
|
|
1538
|
+
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
1539
|
+
if (isNodeOfType(name, "JSXMemberExpression")) return `${flattenJsxName$2(name.object)}.${name.property.name}`;
|
|
1540
|
+
if (isNodeOfType(name, "JSXNamespacedName")) return `${name.namespace.name}:${name.name.name}`;
|
|
1541
|
+
return "";
|
|
1542
|
+
};
|
|
1543
|
+
const resolveJsxElementType = (openingElement) => {
|
|
1544
|
+
const name = openingElement.name;
|
|
1545
|
+
if (isNodeOfType(name, "JSXIdentifier")) {
|
|
1546
|
+
if (!isUppercaseName(name.name)) return name.name;
|
|
1547
|
+
return resolveConstantStringBinding(name) ?? name.name;
|
|
1548
|
+
}
|
|
1549
|
+
return flattenJsxName$2(name);
|
|
1550
|
+
};
|
|
1551
|
+
//#endregion
|
|
1481
1552
|
//#region src/plugin/utils/get-element-type.ts
|
|
1482
1553
|
const EMPTY_JSX_A11Y_SETTINGS = Object.freeze({});
|
|
1483
1554
|
const jsxA11ySettingsCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -1490,14 +1561,8 @@ const readJsxA11ySettings = (settings) => {
|
|
|
1490
1561
|
jsxA11ySettingsCache.set(settings, a11ySettings);
|
|
1491
1562
|
return a11ySettings;
|
|
1492
1563
|
};
|
|
1493
|
-
const flattenJsxName$2 = (name) => {
|
|
1494
|
-
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
1495
|
-
if (isNodeOfType(name, "JSXMemberExpression")) return `${flattenJsxName$2(name.object)}.${name.property.name}`;
|
|
1496
|
-
if (isNodeOfType(name, "JSXNamespacedName")) return `${name.namespace.name}:${name.name.name}`;
|
|
1497
|
-
return "";
|
|
1498
|
-
};
|
|
1499
1564
|
const computeElementType = (openingElement, a11ySettings) => {
|
|
1500
|
-
const baseName =
|
|
1565
|
+
const baseName = resolveJsxElementType(openingElement);
|
|
1501
1566
|
if (a11ySettings.polymorphicPropName) {
|
|
1502
1567
|
const polymorphicAttribute = hasJsxPropIgnoreCase(openingElement.attributes, a11ySettings.polymorphicPropName);
|
|
1503
1568
|
if (polymorphicAttribute) {
|
|
@@ -2332,8 +2397,9 @@ const anchorIsValid = defineRule({
|
|
|
2332
2397
|
const isTestlikeFile = isTestlikeFilename(context.filename);
|
|
2333
2398
|
return { JSXOpeningElement(node) {
|
|
2334
2399
|
if (isTestlikeFile) return;
|
|
2335
|
-
|
|
2336
|
-
if (
|
|
2400
|
+
const tag = getElementType(node, context.settings);
|
|
2401
|
+
if (!fileHasJsxA11ySettings && tag !== "a") return;
|
|
2402
|
+
if (tag !== "a") return;
|
|
2337
2403
|
let hrefAttribute;
|
|
2338
2404
|
for (const attributeName of settings.hrefAttributeNames) {
|
|
2339
2405
|
hrefAttribute = hasJsxPropIgnoreCase(node.attributes, attributeName);
|
|
@@ -5139,8 +5205,10 @@ const STORAGE_GLOBALS = new Set([
|
|
|
5139
5205
|
const SENSITIVE_KEY_PATTERN = /token|jwt|secret|password|passwd|credential|api[-_]?key|bearer|private[-_]?key/i;
|
|
5140
5206
|
const NON_AUTH_TOKEN_PATTERN = /csrf|xsrf|device|fcm|apns|push|design|tokeniz|syntax|css|theme|color/i;
|
|
5141
5207
|
const STRONG_AUTH_KEY_PATTERN = /jwt|secret|password|passwd|credential|private[-_]?key|api[-_]?key|bearer|access[-_]?token|refresh[-_]?token|auth[-_]?token|id[-_]?token|session/i;
|
|
5208
|
+
const PRODUCT_API_KEY_RECORDS_PATTERN = /(?:^|[._:-])(?:created|saved|integration|mailing)[-_]?api[-_]?keys$/i;
|
|
5142
5209
|
const PRODUCT_API_KEY_COLLECTION_PATTERN = /[._:-](?:created|generated|saved)[-_]?api[-_]?keys$/i;
|
|
5143
5210
|
const isAuthCredentialKey = (key) => {
|
|
5211
|
+
if (PRODUCT_API_KEY_RECORDS_PATTERN.test(key)) return false;
|
|
5144
5212
|
if (!SENSITIVE_KEY_PATTERN.test(key)) return false;
|
|
5145
5213
|
if (PRODUCT_API_KEY_COLLECTION_PATTERN.test(key)) return false;
|
|
5146
5214
|
if (NON_AUTH_TOKEN_PATTERN.test(key) && !STRONG_AUTH_KEY_PATTERN.test(key)) return false;
|
|
@@ -5686,7 +5754,7 @@ const buttonHasType = defineRule({
|
|
|
5686
5754
|
return {
|
|
5687
5755
|
JSXOpeningElement(node) {
|
|
5688
5756
|
if (isTestlikeFile) return;
|
|
5689
|
-
if (
|
|
5757
|
+
if (resolveJsxElementType(node) !== "button") return;
|
|
5690
5758
|
const typeAttr = hasJsxPropIgnoreCase(node.attributes, "type");
|
|
5691
5759
|
if (!typeAttr) {
|
|
5692
5760
|
if (hasJsxSpreadAttribute$1(node.attributes)) {
|
|
@@ -5876,7 +5944,7 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
|
|
|
5876
5944
|
};
|
|
5877
5945
|
return {
|
|
5878
5946
|
JSXOpeningElement(node) {
|
|
5879
|
-
if (
|
|
5947
|
+
if (resolveJsxElementType(node) !== "input") return;
|
|
5880
5948
|
reportFromPresence(collectFromJsxAttributes(node.attributes));
|
|
5881
5949
|
},
|
|
5882
5950
|
CallExpression(node) {
|
|
@@ -5892,6 +5960,21 @@ const checkedRequiresOnchangeOrReadonly = defineRule({
|
|
|
5892
5960
|
}
|
|
5893
5961
|
});
|
|
5894
5962
|
//#endregion
|
|
5963
|
+
//#region src/plugin/utils/get-static-property-key-name.ts
|
|
5964
|
+
const getStaticPropertyKeyName = (node, options = {}) => {
|
|
5965
|
+
if (!isNodeOfType(node, "Property")) return null;
|
|
5966
|
+
if (node.computed) {
|
|
5967
|
+
if (options.allowComputedString && isNodeOfType(node.key, "Literal") && typeof node.key.value === "string") return node.key.value;
|
|
5968
|
+
return null;
|
|
5969
|
+
}
|
|
5970
|
+
if (isNodeOfType(node.key, "Identifier")) return node.key.name;
|
|
5971
|
+
if (isNodeOfType(node.key, "Literal")) {
|
|
5972
|
+
if (typeof node.key.value === "string") return node.key.value;
|
|
5973
|
+
if (options.stringifyNonStringLiterals) return String(node.key.value);
|
|
5974
|
+
}
|
|
5975
|
+
return null;
|
|
5976
|
+
};
|
|
5977
|
+
//#endregion
|
|
5895
5978
|
//#region src/plugin/utils/is-presentation-role.ts
|
|
5896
5979
|
const isPresentationRole = (openingElement) => {
|
|
5897
5980
|
const roleAttribute = hasJsxPropIgnoreCase(openingElement.attributes, "role");
|
|
@@ -5936,6 +6019,21 @@ const isPureEventBlockerHandler = (attribute) => {
|
|
|
5936
6019
|
return false;
|
|
5937
6020
|
};
|
|
5938
6021
|
//#endregion
|
|
6022
|
+
//#region src/plugin/utils/resolve-const-identifier-alias.ts
|
|
6023
|
+
const resolveConstIdentifierAlias = (identifier, scopes) => {
|
|
6024
|
+
if (!isNodeOfType(identifier, "Identifier") && !isNodeOfType(identifier, "JSXIdentifier")) return null;
|
|
6025
|
+
const visitedSymbolIds = /* @__PURE__ */ new Set();
|
|
6026
|
+
let symbol = scopes.symbolFor(identifier);
|
|
6027
|
+
while (symbol?.kind === "const") {
|
|
6028
|
+
if (visitedSymbolIds.has(symbol.id) || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return null;
|
|
6029
|
+
visitedSymbolIds.add(symbol.id);
|
|
6030
|
+
const initializer = stripParenExpression(symbol.initializer);
|
|
6031
|
+
if (!isNodeOfType(initializer, "Identifier")) return symbol;
|
|
6032
|
+
symbol = scopes.symbolFor(initializer);
|
|
6033
|
+
}
|
|
6034
|
+
return symbol;
|
|
6035
|
+
};
|
|
6036
|
+
//#endregion
|
|
5939
6037
|
//#region src/plugin/rules/a11y/click-events-have-key-events.ts
|
|
5940
6038
|
const MESSAGE$57 = "Keyboard users can't trigger this click handler because there's no keyboard one, so add `onKeyUp`, `onKeyDown`, or `onKeyPress`.";
|
|
5941
6039
|
const KEY_HANDLERS = [
|
|
@@ -5946,6 +6044,63 @@ const KEY_HANDLERS = [
|
|
|
5946
6044
|
"onKeyDownCapture",
|
|
5947
6045
|
"onKeyPressCapture"
|
|
5948
6046
|
];
|
|
6047
|
+
const CLICK_HANDLERS = ["onClick", "onClickCapture"];
|
|
6048
|
+
const TRANSPARENT_SPREAD_EVENT_NAMES = new Set([...CLICK_HANDLERS, ...KEY_HANDLERS].map((eventName) => eventName.toLowerCase()));
|
|
6049
|
+
const CONSERVATIVE_SPREAD_PROP_NAMES = new Set([
|
|
6050
|
+
"aria-hidden",
|
|
6051
|
+
"onmouseenter",
|
|
6052
|
+
"onmouseover",
|
|
6053
|
+
"role"
|
|
6054
|
+
]);
|
|
6055
|
+
const resolveSpreadObjectExpression = (expression, scopes) => {
|
|
6056
|
+
const innerExpression = stripParenExpression(expression);
|
|
6057
|
+
if (isNodeOfType(innerExpression, "ObjectExpression")) return innerExpression;
|
|
6058
|
+
if (!isNodeOfType(innerExpression, "Identifier")) return null;
|
|
6059
|
+
const symbol = resolveConstIdentifierAlias(innerExpression, scopes);
|
|
6060
|
+
if (symbol?.kind !== "const" || !symbol.initializer) return null;
|
|
6061
|
+
const initializer = stripParenExpression(symbol.initializer);
|
|
6062
|
+
return isNodeOfType(initializer, "ObjectExpression") ? initializer : null;
|
|
6063
|
+
};
|
|
6064
|
+
const collectTransparentSpreadEventNames = (expression, scopes, eventValues, visitedObjectExpressions) => {
|
|
6065
|
+
const objectExpression = resolveSpreadObjectExpression(expression, scopes);
|
|
6066
|
+
if (!objectExpression || visitedObjectExpressions.has(objectExpression)) return false;
|
|
6067
|
+
visitedObjectExpressions.add(objectExpression);
|
|
6068
|
+
let isTransparent = true;
|
|
6069
|
+
for (const property of objectExpression.properties) {
|
|
6070
|
+
if (isNodeOfType(property, "SpreadElement")) {
|
|
6071
|
+
if (!collectTransparentSpreadEventNames(property.argument, scopes, eventValues, visitedObjectExpressions)) {
|
|
6072
|
+
isTransparent = false;
|
|
6073
|
+
break;
|
|
6074
|
+
}
|
|
6075
|
+
continue;
|
|
6076
|
+
}
|
|
6077
|
+
if (!isNodeOfType(property, "Property")) {
|
|
6078
|
+
isTransparent = false;
|
|
6079
|
+
break;
|
|
6080
|
+
}
|
|
6081
|
+
const propertyName = getStaticPropertyKeyName(property, { allowComputedString: true });
|
|
6082
|
+
if (!propertyName) {
|
|
6083
|
+
isTransparent = false;
|
|
6084
|
+
break;
|
|
6085
|
+
}
|
|
6086
|
+
const normalizedPropertyName = propertyName.toLowerCase();
|
|
6087
|
+
if (CONSERVATIVE_SPREAD_PROP_NAMES.has(normalizedPropertyName)) {
|
|
6088
|
+
isTransparent = false;
|
|
6089
|
+
break;
|
|
6090
|
+
}
|
|
6091
|
+
if (TRANSPARENT_SPREAD_EVENT_NAMES.has(normalizedPropertyName)) eventValues.set(normalizedPropertyName, property.value);
|
|
6092
|
+
}
|
|
6093
|
+
visitedObjectExpressions.delete(objectExpression);
|
|
6094
|
+
return isTransparent;
|
|
6095
|
+
};
|
|
6096
|
+
const getTransparentSpreadEventValues = (attributes, scopes) => {
|
|
6097
|
+
const eventValues = /* @__PURE__ */ new Map();
|
|
6098
|
+
for (const attribute of attributes) {
|
|
6099
|
+
if (!isNodeOfType(attribute, "JSXSpreadAttribute")) continue;
|
|
6100
|
+
if (!collectTransparentSpreadEventNames(attribute.argument, scopes, eventValues, /* @__PURE__ */ new Set())) return null;
|
|
6101
|
+
}
|
|
6102
|
+
return eventValues;
|
|
6103
|
+
};
|
|
5949
6104
|
const FOCUSLESS_CONTAINER_TAGS = new Set([
|
|
5950
6105
|
"tr",
|
|
5951
6106
|
"td",
|
|
@@ -5993,7 +6148,10 @@ const isFocusForwardingFunctionBody = (body) => {
|
|
|
5993
6148
|
};
|
|
5994
6149
|
const resolveHandlerFunction = (attribute) => {
|
|
5995
6150
|
if (!attribute.value || !isNodeOfType(attribute.value, "JSXExpressionContainer")) return null;
|
|
5996
|
-
|
|
6151
|
+
return resolveHandlerFunctionExpression(attribute.value.expression);
|
|
6152
|
+
};
|
|
6153
|
+
const resolveHandlerFunctionExpression = (handlerExpression) => {
|
|
6154
|
+
let expression = handlerExpression;
|
|
5997
6155
|
if (isNodeOfType(expression, "Identifier")) {
|
|
5998
6156
|
const binding = findVariableInitializer(expression, expression.name);
|
|
5999
6157
|
if (!binding?.initializer) return null;
|
|
@@ -6092,18 +6250,22 @@ const clickEventsHaveKeyEvents = defineRule({
|
|
|
6092
6250
|
if (!HTML_TAGS.has(tag)) return;
|
|
6093
6251
|
if (tag === "label") return;
|
|
6094
6252
|
if (!FOCUSLESS_CONTAINER_TAGS.has(tag) && isInteractiveElement(tag, node)) return;
|
|
6253
|
+
const spreadEventValues = getTransparentSpreadEventValues(node.attributes, context.scopes);
|
|
6254
|
+
if (!spreadEventValues) return;
|
|
6095
6255
|
const onClick = hasJsxPropIgnoreCase(node.attributes, "onClick") ?? hasJsxPropIgnoreCase(node.attributes, "onClickCapture");
|
|
6096
|
-
|
|
6097
|
-
if (
|
|
6098
|
-
if (
|
|
6099
|
-
if (
|
|
6256
|
+
const spreadOnClickExpression = CLICK_HANDLERS.map((name) => spreadEventValues.get(name.toLowerCase())).find((expression) => expression !== void 0);
|
|
6257
|
+
if (!onClick && !spreadOnClickExpression) return;
|
|
6258
|
+
if (onClick && isPureEventBlockerHandler(onClick)) return;
|
|
6259
|
+
if (onClick && isFocusForwardingHandler(onClick)) return;
|
|
6260
|
+
const spreadHandlerFunction = spreadOnClickExpression ? resolveHandlerFunctionExpression(spreadOnClickExpression) : null;
|
|
6261
|
+
if (spreadHandlerFunction && (isFocusForwardingFunctionBody(spreadHandlerFunction.body ?? null) || containsBackdropDismissComparison(spreadHandlerFunction.body ?? null))) return;
|
|
6100
6262
|
if (hasCompositeItemRole(node)) return;
|
|
6101
6263
|
if (isHoverSelectionListItem(tag, node)) return;
|
|
6102
|
-
if (isBackdropDismissHandler(onClick)) return;
|
|
6264
|
+
if (onClick && isBackdropDismissHandler(onClick)) return;
|
|
6103
6265
|
if (containsKeyboardActivatableDescendant(node.parent)) return;
|
|
6104
6266
|
if (isHiddenFromScreenReader(node, context.settings)) return;
|
|
6105
6267
|
if (isPresentationRole(node)) return;
|
|
6106
|
-
if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler))) return;
|
|
6268
|
+
if (KEY_HANDLERS.some((handler) => hasJsxPropIgnoreCase(node.attributes, handler) || spreadEventValues.has(handler.toLowerCase()))) return;
|
|
6107
6269
|
context.report({
|
|
6108
6270
|
node: node.name,
|
|
6109
6271
|
message: MESSAGE$57
|
|
@@ -6134,6 +6296,131 @@ const isGlobalMethodCall = (node, objectName, methodName) => {
|
|
|
6134
6296
|
return isNodeOfType(receiver, "Identifier") && receiver.name === objectName && isNodeOfType(callee.property, "Identifier") && callee.property.name === methodName;
|
|
6135
6297
|
};
|
|
6136
6298
|
//#endregion
|
|
6299
|
+
//#region src/plugin/utils/collect-safely-validated-local-storage-keys.ts
|
|
6300
|
+
const isLocalStorageCall = (node, methodName) => {
|
|
6301
|
+
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
6302
|
+
const callee = stripParenExpression(node.callee);
|
|
6303
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
6304
|
+
const receiver = stripParenExpression(callee.object);
|
|
6305
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === "localStorage" && isNodeOfType(callee.property, "Identifier") && callee.property.name === methodName;
|
|
6306
|
+
};
|
|
6307
|
+
const catchReturnsFallback = (tryStatement) => {
|
|
6308
|
+
const handler = tryStatement.handler;
|
|
6309
|
+
if (!handler) return false;
|
|
6310
|
+
let hasReturn = false;
|
|
6311
|
+
let hasThrow = false;
|
|
6312
|
+
walkAst(handler.body, (child) => {
|
|
6313
|
+
if (isFunctionLike$1(child)) return false;
|
|
6314
|
+
if (isNodeOfType(child, "ReturnStatement")) hasReturn = true;
|
|
6315
|
+
if (isNodeOfType(child, "ThrowStatement")) hasThrow = true;
|
|
6316
|
+
});
|
|
6317
|
+
return hasReturn && !hasThrow;
|
|
6318
|
+
};
|
|
6319
|
+
const resolveValidatorFunction = (callee, scopes) => {
|
|
6320
|
+
const unwrappedCallee = stripParenExpression(callee);
|
|
6321
|
+
if (!isNodeOfType(unwrappedCallee, "Identifier")) return null;
|
|
6322
|
+
const symbol = scopes.symbolFor(unwrappedCallee);
|
|
6323
|
+
if (!symbol) return null;
|
|
6324
|
+
const candidate = symbol.kind === "function" ? symbol.declarationNode : symbol.initializer;
|
|
6325
|
+
return isFunctionLike$1(candidate) ? candidate : null;
|
|
6326
|
+
};
|
|
6327
|
+
const validatorChecksPayloadProperties = (validatorFunction, scopes) => {
|
|
6328
|
+
if (!isFunctionLike$1(validatorFunction)) return false;
|
|
6329
|
+
const firstParameter = validatorFunction.params?.[0];
|
|
6330
|
+
if (!firstParameter || !isNodeOfType(firstParameter, "Identifier")) return false;
|
|
6331
|
+
const parameterSymbol = scopes.symbolFor(firstParameter);
|
|
6332
|
+
if (!parameterSymbol) return false;
|
|
6333
|
+
const payloadSymbolIds = new Set([parameterSymbol.id]);
|
|
6334
|
+
let hasPropertyTypeCheck = false;
|
|
6335
|
+
walkAst(validatorFunction.body, (child) => {
|
|
6336
|
+
if (isFunctionLike$1(child)) return false;
|
|
6337
|
+
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier") && child.init) {
|
|
6338
|
+
const initializer = stripParenExpression(child.init);
|
|
6339
|
+
if (isNodeOfType(initializer, "Identifier")) {
|
|
6340
|
+
const initializerSymbol = scopes.symbolFor(initializer);
|
|
6341
|
+
if (initializerSymbol && payloadSymbolIds.has(initializerSymbol.id)) {
|
|
6342
|
+
const aliasSymbol = scopes.symbolFor(child.id);
|
|
6343
|
+
if (aliasSymbol) payloadSymbolIds.add(aliasSymbol.id);
|
|
6344
|
+
}
|
|
6345
|
+
}
|
|
6346
|
+
}
|
|
6347
|
+
if (!isNodeOfType(child, "UnaryExpression") || child.operator !== "typeof") return;
|
|
6348
|
+
const checkedValue = stripParenExpression(child.argument);
|
|
6349
|
+
if (!isNodeOfType(checkedValue, "MemberExpression")) return;
|
|
6350
|
+
const receiver = stripParenExpression(checkedValue.object);
|
|
6351
|
+
if (!isNodeOfType(receiver, "Identifier")) return;
|
|
6352
|
+
const receiverSymbol = scopes.symbolFor(receiver);
|
|
6353
|
+
if (receiverSymbol && payloadSymbolIds.has(receiverSymbol.id)) hasPropertyTypeCheck = true;
|
|
6354
|
+
});
|
|
6355
|
+
return hasPropertyTypeCheck;
|
|
6356
|
+
};
|
|
6357
|
+
const incrementCount = (counts, keyValue) => {
|
|
6358
|
+
counts.set(keyValue, (counts.get(keyValue) ?? 0) + 1);
|
|
6359
|
+
};
|
|
6360
|
+
const isReturnedExpression = (expression) => {
|
|
6361
|
+
const parent = expression.parent;
|
|
6362
|
+
return Boolean(parent && isNodeOfType(parent, "ReturnStatement") && parent.argument === expression);
|
|
6363
|
+
};
|
|
6364
|
+
const collectSafelyValidatedLocalStorageKeys = ({ programNode, scopes, resolveKey }) => {
|
|
6365
|
+
const readCountsByKey = /* @__PURE__ */ new Map();
|
|
6366
|
+
const safeReadCountsByKey = /* @__PURE__ */ new Map();
|
|
6367
|
+
const safeReadSymbolIds = /* @__PURE__ */ new Set();
|
|
6368
|
+
walkAst(programNode, (child) => {
|
|
6369
|
+
if (!isNodeOfType(child, "CallExpression") || !isLocalStorageCall(child, "getItem")) return;
|
|
6370
|
+
const keyArgument = child.arguments?.[0];
|
|
6371
|
+
if (!keyArgument) return;
|
|
6372
|
+
const keyValue = resolveKey(keyArgument);
|
|
6373
|
+
if (keyValue !== null) incrementCount(readCountsByKey, keyValue);
|
|
6374
|
+
});
|
|
6375
|
+
walkAst(programNode, (child) => {
|
|
6376
|
+
if (!isNodeOfType(child, "TryStatement") || !catchReturnsFallback(child)) return;
|
|
6377
|
+
const rawValueKeys = /* @__PURE__ */ new Map();
|
|
6378
|
+
const parsedValueSources = /* @__PURE__ */ new Map();
|
|
6379
|
+
walkAst(child.block, (tryChild) => {
|
|
6380
|
+
if (isFunctionLike$1(tryChild)) return false;
|
|
6381
|
+
if (isNodeOfType(tryChild, "VariableDeclarator") && isNodeOfType(tryChild.id, "Identifier") && tryChild.init) {
|
|
6382
|
+
const initializer = stripParenExpression(tryChild.init);
|
|
6383
|
+
if (isNodeOfType(initializer, "CallExpression") && isLocalStorageCall(initializer, "getItem")) {
|
|
6384
|
+
const keyArgument = initializer.arguments?.[0];
|
|
6385
|
+
const bindingSymbol = scopes.symbolFor(tryChild.id);
|
|
6386
|
+
if (keyArgument && bindingSymbol) {
|
|
6387
|
+
const keyValue = resolveKey(keyArgument);
|
|
6388
|
+
if (keyValue !== null) rawValueKeys.set(bindingSymbol.id, keyValue);
|
|
6389
|
+
}
|
|
6390
|
+
}
|
|
6391
|
+
if (isNodeOfType(initializer, "CallExpression") && isGlobalMethodCall(initializer, "JSON", "parse")) {
|
|
6392
|
+
const rawValueArgument = initializer.arguments?.[0];
|
|
6393
|
+
const parsedValueSymbol = scopes.symbolFor(tryChild.id);
|
|
6394
|
+
if (rawValueArgument && isNodeOfType(rawValueArgument, "Identifier") && parsedValueSymbol) {
|
|
6395
|
+
const rawValueSymbol = scopes.symbolFor(rawValueArgument);
|
|
6396
|
+
if (rawValueSymbol && rawValueKeys.has(rawValueSymbol.id)) parsedValueSources.set(parsedValueSymbol.id, rawValueSymbol.id);
|
|
6397
|
+
}
|
|
6398
|
+
}
|
|
6399
|
+
}
|
|
6400
|
+
if (!isNodeOfType(tryChild, "ConditionalExpression") || !isReturnedExpression(tryChild)) return;
|
|
6401
|
+
const test = stripParenExpression(tryChild.test);
|
|
6402
|
+
if (!isNodeOfType(test, "CallExpression")) return;
|
|
6403
|
+
const testedValue = test.arguments?.[0];
|
|
6404
|
+
if (!testedValue || !isNodeOfType(testedValue, "Identifier")) return;
|
|
6405
|
+
const parsedValueSymbol = scopes.symbolFor(testedValue);
|
|
6406
|
+
if (!parsedValueSymbol) return;
|
|
6407
|
+
const rawValueSymbolId = parsedValueSources.get(parsedValueSymbol.id);
|
|
6408
|
+
if (rawValueSymbolId === void 0) return;
|
|
6409
|
+
const returnedValue = stripParenExpression(tryChild.consequent);
|
|
6410
|
+
if (!isNodeOfType(returnedValue, "Identifier") || scopes.symbolFor(returnedValue)?.id !== parsedValueSymbol.id) return;
|
|
6411
|
+
const validatorFunction = resolveValidatorFunction(test.callee, scopes);
|
|
6412
|
+
if (!validatorFunction || !validatorChecksPayloadProperties(validatorFunction, scopes)) return;
|
|
6413
|
+
const keyValue = rawValueKeys.get(rawValueSymbolId);
|
|
6414
|
+
if (keyValue === void 0 || safeReadSymbolIds.has(rawValueSymbolId)) return;
|
|
6415
|
+
safeReadSymbolIds.add(rawValueSymbolId);
|
|
6416
|
+
incrementCount(safeReadCountsByKey, keyValue);
|
|
6417
|
+
});
|
|
6418
|
+
});
|
|
6419
|
+
const safeKeys = /* @__PURE__ */ new Set();
|
|
6420
|
+
for (const [keyValue, readCount] of readCountsByKey) if (safeReadCountsByKey.get(keyValue) === readCount) safeKeys.add(keyValue);
|
|
6421
|
+
return safeKeys;
|
|
6422
|
+
};
|
|
6423
|
+
//#endregion
|
|
6137
6424
|
//#region src/plugin/rules/client/client-localstorage-no-version.ts
|
|
6138
6425
|
const VERSIONED_KEY_PATTERN = /(?:[._:-]v\d+|@\d+|\bv\d+\b)/i;
|
|
6139
6426
|
const CAMEL_CASE_VERSIONED_KEY_PATTERN = /[a-z]V\d+/;
|
|
@@ -6155,26 +6442,39 @@ const clientLocalstorageNoVersion = defineRule({
|
|
|
6155
6442
|
severity: "warn",
|
|
6156
6443
|
category: "Correctness",
|
|
6157
6444
|
recommendation: "Put a version in the storage key (e.g. \"myKey:v1\"). If you change the data shape later, old saved data can be ignored instead of crashing the app.",
|
|
6158
|
-
create: (context) =>
|
|
6159
|
-
|
|
6160
|
-
|
|
6161
|
-
|
|
6162
|
-
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
|
|
6445
|
+
create: (context) => {
|
|
6446
|
+
let safelyValidatedStorageKeys = /* @__PURE__ */ new Set();
|
|
6447
|
+
return {
|
|
6448
|
+
Program(node) {
|
|
6449
|
+
safelyValidatedStorageKeys = collectSafelyValidatedLocalStorageKeys({
|
|
6450
|
+
programNode: node,
|
|
6451
|
+
scopes: context.scopes,
|
|
6452
|
+
resolveKey: (keyNode) => resolveStringKey(keyNode, context)
|
|
6453
|
+
});
|
|
6454
|
+
},
|
|
6455
|
+
CallExpression(node) {
|
|
6456
|
+
if (!isNodeOfType(node.callee, "MemberExpression")) return;
|
|
6457
|
+
const receiver = stripParenExpression(node.callee.object);
|
|
6458
|
+
if (!isNodeOfType(receiver, "Identifier")) return;
|
|
6459
|
+
if (receiver.name !== "localStorage") return;
|
|
6460
|
+
if (!isNodeOfType(node.callee.property, "Identifier")) return;
|
|
6461
|
+
if (node.callee.property.name !== "setItem") return;
|
|
6462
|
+
const keyArg = node.arguments?.[0];
|
|
6463
|
+
if (!keyArg) return;
|
|
6464
|
+
const keyValue = resolveStringKey(keyArg, context);
|
|
6465
|
+
if (keyValue === null) return;
|
|
6466
|
+
if (isVersionedKey(keyValue)) return;
|
|
6467
|
+
if (safelyValidatedStorageKeys.has(keyValue)) return;
|
|
6468
|
+
const valueArg = node.arguments?.[1];
|
|
6469
|
+
if (!valueArg) return;
|
|
6470
|
+
if (!isJsonStringifyCall(valueArg)) return;
|
|
6471
|
+
context.report({
|
|
6472
|
+
node: keyArg,
|
|
6473
|
+
message: `${receiver.name}.setItem("${keyValue}", JSON.stringify(...)) has no version, so changing the data shape later crashes your users' saved sessions. Add one to the key (e.g. "${keyValue}:v1").`
|
|
6474
|
+
});
|
|
6475
|
+
}
|
|
6476
|
+
};
|
|
6477
|
+
}
|
|
6178
6478
|
});
|
|
6179
6479
|
//#endregion
|
|
6180
6480
|
//#region src/plugin/rules/client/client-passive-event-listeners.ts
|
|
@@ -6793,6 +7093,30 @@ const corsCookieTrustRisk = defineRule({
|
|
|
6793
7093
|
})
|
|
6794
7094
|
});
|
|
6795
7095
|
//#endregion
|
|
7096
|
+
//#region src/plugin/rules/security-scan/utils/find-matching-bracket.ts
|
|
7097
|
+
const findMatchingBracket = (content, openIndex) => {
|
|
7098
|
+
const open = content[openIndex];
|
|
7099
|
+
const close = open === "(" ? ")" : open === "{" ? "}" : open === "[" ? "]" : "";
|
|
7100
|
+
if (close === "") return -1;
|
|
7101
|
+
let depth = 0;
|
|
7102
|
+
let stringDelimiter = null;
|
|
7103
|
+
for (let index = openIndex; index < content.length; index += 1) {
|
|
7104
|
+
const character = content[index];
|
|
7105
|
+
if (stringDelimiter !== null) {
|
|
7106
|
+
if (character === "\\") index += 1;
|
|
7107
|
+
else if (character === stringDelimiter) stringDelimiter = null;
|
|
7108
|
+
continue;
|
|
7109
|
+
}
|
|
7110
|
+
if (character === "\"" || character === "'" || character === "`") stringDelimiter = character;
|
|
7111
|
+
else if (character === open) depth += 1;
|
|
7112
|
+
else if (character === close) {
|
|
7113
|
+
depth -= 1;
|
|
7114
|
+
if (depth === 0) return index;
|
|
7115
|
+
}
|
|
7116
|
+
}
|
|
7117
|
+
return -1;
|
|
7118
|
+
};
|
|
7119
|
+
//#endregion
|
|
6796
7120
|
//#region src/plugin/rules/security-scan/dangerous-html-sink.ts
|
|
6797
7121
|
const DANGEROUS_HTML_PATTERN = /dangerouslySetInnerHTML|(?:\.(?:inner|outer)HTML|\[\s*["'](?:inner|outer)HTML["']\s*\])\s*[+]?=(?!=)|\.insertAdjacentHTML\s*\(|\bdocument\.write(?:ln)?\s*\(|\.(?:createContextualFragment|setHTMLUnsafe)\s*\(/;
|
|
6798
7122
|
const HTML_VALUE_START_PATTERN = /(?:__html\s*:|(?:\.(?:inner|outer)HTML|\[\s*["'](?:inner|outer)HTML["']\s*\])\s*[+]?=(?!=)|\.insertAdjacentHTML\s*\(\s*[^,]*,|\bdocument\.write(?:ln)?\s*\(|\.(?:createContextualFragment|setHTMLUnsafe)\s*\()\s*([\s\S]*)/;
|
|
@@ -6808,6 +7132,7 @@ const I18N_VALUE_PATTERN = /\b(?:t|i18n|translate|formatMessage|intl)\s*[.(]/;
|
|
|
6808
7132
|
const ESCAPING_SERIALIZER_CALL_PATTERN = /^(?:[\w$.]+\.)?(?:toHtml|render[A-Za-z]*(?:Html|HTML)|renderToString|renderToStaticMarkup|codeToHtml|codeToHast|highlight[A-Za-z]*)\s*\(/;
|
|
6809
7133
|
const HIGHLIGHTER_LIBRARY_PATTERN = /\b(?:shiki|prism|hljs|highlightjs|getHighlighter|codeToHtml|codeToHast|refractor|lowlight|starry-night)\b|highlight\.js/i;
|
|
6810
7134
|
const SERIALIZER_ASSIGNMENT_PATTERN = /[:=]\s*[^\n;]*(?:\b(?:katex|shiki|hljs|prism|mermaid)\b|hast-util-to-html|renderHtmlFromRichText|(?:toHtml|render[A-Za-z]*(?:Html|HTML)|renderToString|renderToStaticMarkup|codeToHtml|codeToHast)\s*\()/i;
|
|
7135
|
+
const SERIALIZER_CALL_PROVENANCE_PATTERN = /\b(?:(?:katex|shiki|hljs|prism|mermaid|highlighter)[\w$]*\.(?:render\w*|highlight\w*|codeTo(?:Html|Hast))|(?:toHtml|render(?:Html|HTML)[A-Za-z]*|render[A-Za-z]*(?:Html|HTML)|renderToString|renderToStaticMarkup|codeToHtml|codeToHast|highlight[A-Za-z]*))\s*\(/i;
|
|
6811
7136
|
const BARE_IDENTIFIER_VALUE_PATTERN = /^[\w$]+\s*(?:[;,})\n]|$)/;
|
|
6812
7137
|
const IDENTIFIER_WITH_LITERAL_FALLBACK_PATTERN = /^[\w$]+\s*(?:\|\||\?\?)\s*(?:"[^"\n]*"|'[^'\n]*'|`[^`$]*`)\s*(?:[;,})\n]|$)/;
|
|
6813
7138
|
const MEMBER_OR_INDEX_ACCESS_VALUE_PATTERN = /^[\w$]+(?:\.[\w$]+|\[[^\]]*\])+\s*(?:[;,})\n]|$)/;
|
|
@@ -6829,11 +7154,10 @@ const getTemplateInterpolations = (valueTail) => {
|
|
|
6829
7154
|
const interpolations = valueTail.slice(1, closingBacktickIndex).match(/\$\{[^}]*\}/g);
|
|
6830
7155
|
return interpolations === null ? "" : interpolations.join(" ");
|
|
6831
7156
|
};
|
|
6832
|
-
const getIdentifierDeclarationInitializer = (identifier, fileContent) => {
|
|
6833
|
-
const
|
|
6834
|
-
if (
|
|
6835
|
-
|
|
6836
|
-
return fileContent.slice(initializerStartIndex, initializerStartIndex + STATIC_TEMPLATE_MAX_CHARS);
|
|
7157
|
+
const getIdentifierDeclarationInitializer = (identifier, sinkIndex, fileContent) => {
|
|
7158
|
+
const declaration = findVisibleIdentifierDeclaration(identifier, sinkIndex, fileContent);
|
|
7159
|
+
if (declaration === null) return null;
|
|
7160
|
+
return fileContent.slice(declaration.initializerStartIndex, declaration.initializerStartIndex + STATIC_TEMPLATE_MAX_CHARS);
|
|
6837
7161
|
};
|
|
6838
7162
|
const isPureStringLiteralConcat = (initializerText) => {
|
|
6839
7163
|
if (!/^["']/.test(initializerText)) return false;
|
|
@@ -6903,6 +7227,229 @@ const isAllOperandsDomContentConcat = (valueExpression) => {
|
|
|
6903
7227
|
return !HTML_TAINT_PATTERN.test(withoutCast.replace(DOM_CONTENT_SOURCE_VALUE_PATTERN, ""));
|
|
6904
7228
|
});
|
|
6905
7229
|
};
|
|
7230
|
+
const findMatchingBraceIndex = (fileContent, openingBraceIndex) => {
|
|
7231
|
+
let depth = 0;
|
|
7232
|
+
let quote = null;
|
|
7233
|
+
for (let index = openingBraceIndex; index < fileContent.length; index += 1) {
|
|
7234
|
+
const character = fileContent[index];
|
|
7235
|
+
if (quote !== null) {
|
|
7236
|
+
if (character === quote && fileContent[index - 1] !== "\\") quote = null;
|
|
7237
|
+
continue;
|
|
7238
|
+
}
|
|
7239
|
+
if (character === "\"" || character === "'" || character === "`") {
|
|
7240
|
+
quote = character;
|
|
7241
|
+
continue;
|
|
7242
|
+
}
|
|
7243
|
+
if (character === "{") depth += 1;
|
|
7244
|
+
if (character === "}") {
|
|
7245
|
+
depth -= 1;
|
|
7246
|
+
if (depth === 0) return index;
|
|
7247
|
+
}
|
|
7248
|
+
}
|
|
7249
|
+
return fileContent.length;
|
|
7250
|
+
};
|
|
7251
|
+
const findContainingBlockEndIndex = (fileContent, targetIndex) => {
|
|
7252
|
+
const openingBraceIndexes = [];
|
|
7253
|
+
let quote = null;
|
|
7254
|
+
let isLineComment = false;
|
|
7255
|
+
let isBlockComment = false;
|
|
7256
|
+
for (let index = 0; index < targetIndex; index += 1) {
|
|
7257
|
+
const character = fileContent[index];
|
|
7258
|
+
const nextCharacter = fileContent[index + 1];
|
|
7259
|
+
if (isLineComment) {
|
|
7260
|
+
if (character === "\n") isLineComment = false;
|
|
7261
|
+
continue;
|
|
7262
|
+
}
|
|
7263
|
+
if (isBlockComment) {
|
|
7264
|
+
if (character === "*" && nextCharacter === "/") {
|
|
7265
|
+
isBlockComment = false;
|
|
7266
|
+
index += 1;
|
|
7267
|
+
}
|
|
7268
|
+
continue;
|
|
7269
|
+
}
|
|
7270
|
+
if (quote !== null) {
|
|
7271
|
+
if (character === quote && fileContent[index - 1] !== "\\") quote = null;
|
|
7272
|
+
continue;
|
|
7273
|
+
}
|
|
7274
|
+
if (character === "/" && nextCharacter === "/") {
|
|
7275
|
+
isLineComment = true;
|
|
7276
|
+
index += 1;
|
|
7277
|
+
continue;
|
|
7278
|
+
}
|
|
7279
|
+
if (character === "/" && nextCharacter === "*") {
|
|
7280
|
+
isBlockComment = true;
|
|
7281
|
+
index += 1;
|
|
7282
|
+
continue;
|
|
7283
|
+
}
|
|
7284
|
+
if (character === "\"" || character === "'" || character === "`") {
|
|
7285
|
+
quote = character;
|
|
7286
|
+
continue;
|
|
7287
|
+
}
|
|
7288
|
+
if (character === "{") openingBraceIndexes.push(index);
|
|
7289
|
+
if (character === "}") openingBraceIndexes.pop();
|
|
7290
|
+
}
|
|
7291
|
+
const openingBraceIndex = openingBraceIndexes.at(-1);
|
|
7292
|
+
return openingBraceIndex === void 0 ? fileContent.length : findMatchingBraceIndex(fileContent, openingBraceIndex);
|
|
7293
|
+
};
|
|
7294
|
+
const findVisibleIdentifierDeclaration = (identifier, sinkIndex, fileContent) => {
|
|
7295
|
+
const initializerPattern = new RegExp(`(const|let|var)\\s+${escapeRegExp(identifier)}\\s*(?::[^=\\n]*)?=\\s*([^;\\n]+)`, "g");
|
|
7296
|
+
let nearestDeclaration = null;
|
|
7297
|
+
let nearestDeclarationIndex = -1;
|
|
7298
|
+
for (const match of fileContent.matchAll(initializerPattern)) {
|
|
7299
|
+
const declarationIndex = match.index;
|
|
7300
|
+
if (declarationIndex === void 0 || declarationIndex >= sinkIndex || declarationIndex <= nearestDeclarationIndex || findContainingBlockEndIndex(fileContent, declarationIndex) < sinkIndex) continue;
|
|
7301
|
+
nearestDeclarationIndex = declarationIndex;
|
|
7302
|
+
const initializer = match[2];
|
|
7303
|
+
if (initializer === void 0) continue;
|
|
7304
|
+
nearestDeclaration = {
|
|
7305
|
+
declarationIndex,
|
|
7306
|
+
initializer,
|
|
7307
|
+
initializerStartIndex: declarationIndex + match[0].length - initializer.length,
|
|
7308
|
+
isImmutable: match[1] === "const"
|
|
7309
|
+
};
|
|
7310
|
+
}
|
|
7311
|
+
return nearestDeclaration;
|
|
7312
|
+
};
|
|
7313
|
+
const isDeclarationStable = (identifier, declaration, usageIndex, fileContent) => {
|
|
7314
|
+
if (declaration.isImmutable) return true;
|
|
7315
|
+
const textAfterInitializer = fileContent.slice(declaration.initializerStartIndex + declaration.initializer.length, usageIndex);
|
|
7316
|
+
return !new RegExp(`(?:^|[^\\w$.])${escapeRegExp(identifier)}\\s*=(?!=)`).test(textAfterInitializer);
|
|
7317
|
+
};
|
|
7318
|
+
const isTrustedHighlighterValue = (valueExpression, fileContent, sinkIndex) => {
|
|
7319
|
+
if (!/highlight/i.test(valueExpression)) return false;
|
|
7320
|
+
const identifier = valueExpression.match(/^([\w$]+)/)?.[1];
|
|
7321
|
+
if (identifier === void 0) return false;
|
|
7322
|
+
const declaration = findVisibleIdentifierDeclaration(identifier, sinkIndex, fileContent);
|
|
7323
|
+
if (declaration !== null) return isDeclarationStable(identifier, declaration, sinkIndex, fileContent) && SERIALIZER_CALL_PROVENANCE_PATTERN.test(declaration.initializer);
|
|
7324
|
+
return /highlighted/i.test(valueExpression) || HIGHLIGHTER_LIBRARY_PATTERN.test(fileContent);
|
|
7325
|
+
};
|
|
7326
|
+
const isExplicitlyTrustedHtmlValue = (valueExpression, fileContent, sinkIndex, visitedIdentifiers = /* @__PURE__ */ new Set()) => {
|
|
7327
|
+
const trimmedExpression = valueExpression.trim();
|
|
7328
|
+
const concatenatedParts = splitTopLevelByPlus(trimmedExpression);
|
|
7329
|
+
if (concatenatedParts.length > 1) return concatenatedParts.every((part) => isExplicitlyTrustedHtmlValue(part, fileContent, sinkIndex, visitedIdentifiers));
|
|
7330
|
+
const interpolationParts = [...trimmedExpression.matchAll(/\$\{([^}]*)\}/g)].flatMap((match) => match[1] === void 0 ? [] : [match[1]]);
|
|
7331
|
+
if (interpolationParts.length > 1) return interpolationParts.every((part) => isExplicitlyTrustedHtmlValue(part, fileContent, sinkIndex, visitedIdentifiers));
|
|
7332
|
+
const identifier = trimmedExpression.match(/^([\w$]+)(?:(?:\??\.[\w$]+)|(?:\[\s*(?:"[^"\n]*"|'[^'\n]*'|\d+)\s*\]))*$/)?.[1];
|
|
7333
|
+
if (identifier && !visitedIdentifiers.has(identifier)) {
|
|
7334
|
+
const declaration = findVisibleIdentifierDeclaration(identifier, sinkIndex, fileContent);
|
|
7335
|
+
if (declaration && isDeclarationStable(identifier, declaration, sinkIndex, fileContent)) {
|
|
7336
|
+
const nextVisitedIdentifiers = new Set(visitedIdentifiers);
|
|
7337
|
+
nextVisitedIdentifiers.add(identifier);
|
|
7338
|
+
return isExplicitlyTrustedHtmlValue(declaration.initializer, fileContent, declaration.initializerStartIndex, nextVisitedIdentifiers);
|
|
7339
|
+
}
|
|
7340
|
+
}
|
|
7341
|
+
if (STRING_LITERAL_VALUE_PATTERN.test(`${trimmedExpression};`) || MODULE_CONSTANT_VALUE_PATTERN.test(`${trimmedExpression};`) || SANITIZER_PATTERN.test(trimmedExpression) || ENV_CONFIG_VALUE_PATTERN.test(trimmedExpression) || I18N_VALUE_PATTERN.test(trimmedExpression) || ESCAPING_SERIALIZER_CALL_PATTERN.test(trimmedExpression) || isTrustedHighlighterValue(trimmedExpression, fileContent, sinkIndex)) return true;
|
|
7342
|
+
return false;
|
|
7343
|
+
};
|
|
7344
|
+
const doesExpressionAliasIdentifier = (expression, targetIdentifier, expressionIndex, fileContent, visitedIdentifiers = /* @__PURE__ */ new Set()) => {
|
|
7345
|
+
const identifier = expression.trim().match(/^([\w$]+)$/)?.[1];
|
|
7346
|
+
if (!identifier) return false;
|
|
7347
|
+
if (identifier === targetIdentifier) return true;
|
|
7348
|
+
if (visitedIdentifiers.has(identifier)) return false;
|
|
7349
|
+
const declaration = findVisibleIdentifierDeclaration(identifier, expressionIndex, fileContent);
|
|
7350
|
+
if (!declaration?.isImmutable) return false;
|
|
7351
|
+
const nextVisitedIdentifiers = new Set(visitedIdentifiers);
|
|
7352
|
+
nextVisitedIdentifiers.add(identifier);
|
|
7353
|
+
return doesExpressionAliasIdentifier(declaration.initializer, targetIdentifier, declaration.initializerStartIndex, fileContent, nextVisitedIdentifiers);
|
|
7354
|
+
};
|
|
7355
|
+
const findContainingFunctionParameterSource = (identifier, sinkIndex, fileContent) => {
|
|
7356
|
+
const patterns = [/function\s+([\w$]+)\s*\(([^)]*)\)\s*\{/g, /(?:const|let|var)\s+([\w$]+)\s*=\s*(?:async\s*)?\(([^)]*)\)\s*=>\s*\{/g];
|
|
7357
|
+
let closestSource = null;
|
|
7358
|
+
let closestStartIndex = -1;
|
|
7359
|
+
for (const pattern of patterns) for (const match of fileContent.matchAll(pattern)) {
|
|
7360
|
+
const matchIndex = match.index;
|
|
7361
|
+
if (matchIndex === void 0 || matchIndex >= sinkIndex || matchIndex < closestStartIndex) continue;
|
|
7362
|
+
const bodyEndIndex = findMatchingBraceIndex(fileContent, matchIndex + match[0].lastIndexOf("{"));
|
|
7363
|
+
if (bodyEndIndex < sinkIndex) continue;
|
|
7364
|
+
const parameterIndex = (match[2] ?? "").split(",").findIndex((parameter) => parameter.trim().match(/^(?:\.\.\.)?([\w$]+)/)?.[1] === identifier);
|
|
7365
|
+
if (parameterIndex < 0) continue;
|
|
7366
|
+
closestStartIndex = matchIndex;
|
|
7367
|
+
const functionName = match[1] ?? "";
|
|
7368
|
+
closestSource = {
|
|
7369
|
+
bodyEndIndex,
|
|
7370
|
+
declarationNameIndex: matchIndex + match[0].indexOf(functionName),
|
|
7371
|
+
functionName,
|
|
7372
|
+
parameterIndex
|
|
7373
|
+
};
|
|
7374
|
+
}
|
|
7375
|
+
return closestSource;
|
|
7376
|
+
};
|
|
7377
|
+
const splitTopLevelArguments = (argumentText) => {
|
|
7378
|
+
const argumentsList = [];
|
|
7379
|
+
let startIndex = 0;
|
|
7380
|
+
let depth = 0;
|
|
7381
|
+
let quote = null;
|
|
7382
|
+
for (let index = 0; index < argumentText.length; index += 1) {
|
|
7383
|
+
const character = argumentText[index];
|
|
7384
|
+
if (quote !== null) {
|
|
7385
|
+
if (character === quote && argumentText[index - 1] !== "\\") quote = null;
|
|
7386
|
+
continue;
|
|
7387
|
+
}
|
|
7388
|
+
if (character === "\"" || character === "'" || character === "`") {
|
|
7389
|
+
quote = character;
|
|
7390
|
+
continue;
|
|
7391
|
+
}
|
|
7392
|
+
if (character === "(" || character === "[" || character === "{") depth += 1;
|
|
7393
|
+
if (character === ")" || character === "]" || character === "}") depth -= 1;
|
|
7394
|
+
if (character === "," && depth === 0) {
|
|
7395
|
+
argumentsList.push(argumentText.slice(startIndex, index).trim());
|
|
7396
|
+
startIndex = index + 1;
|
|
7397
|
+
}
|
|
7398
|
+
}
|
|
7399
|
+
argumentsList.push(argumentText.slice(startIndex).trim());
|
|
7400
|
+
return argumentsList;
|
|
7401
|
+
};
|
|
7402
|
+
const isBackedByFunctionParameter = (expression, expressionIndex, fileContent, visitedIdentifiers = /* @__PURE__ */ new Set()) => {
|
|
7403
|
+
const identifier = expression.trim().match(/^([\w$]+)$/)?.[1];
|
|
7404
|
+
if (identifier === void 0 || visitedIdentifiers.has(identifier)) return false;
|
|
7405
|
+
if (findContainingFunctionParameterSource(identifier, expressionIndex, fileContent) !== null) return true;
|
|
7406
|
+
const declaration = findVisibleIdentifierDeclaration(identifier, expressionIndex, fileContent);
|
|
7407
|
+
if (!declaration || !isDeclarationStable(identifier, declaration, expressionIndex, fileContent)) return false;
|
|
7408
|
+
const nextVisitedIdentifiers = new Set(visitedIdentifiers);
|
|
7409
|
+
nextVisitedIdentifiers.add(identifier);
|
|
7410
|
+
return isBackedByFunctionParameter(declaration.initializer, declaration.initializerStartIndex, fileContent, nextVisitedIdentifiers);
|
|
7411
|
+
};
|
|
7412
|
+
const isHtmlTainted = (expression, fileContent, sinkIndex, visitedIdentifiers, visitedCallSites) => {
|
|
7413
|
+
const trimmedExpression = expression.trim();
|
|
7414
|
+
if (isExplicitlyTrustedHtmlValue(trimmedExpression, fileContent, sinkIndex)) return false;
|
|
7415
|
+
const identifier = trimmedExpression.match(/^([\w$]+)(?:\.|\s*(?:[;,})\n]|$))/)?.[1];
|
|
7416
|
+
if (identifier === void 0) return HTML_TAINT_PATTERN.test(trimmedExpression);
|
|
7417
|
+
if (visitedIdentifiers.has(identifier)) return false;
|
|
7418
|
+
visitedIdentifiers.add(identifier);
|
|
7419
|
+
const declaration = findVisibleIdentifierDeclaration(identifier, sinkIndex, fileContent);
|
|
7420
|
+
const parameterSource = findContainingFunctionParameterSource(identifier, sinkIndex, fileContent);
|
|
7421
|
+
if (declaration !== null && (parameterSource === null || declaration.declarationIndex > parameterSource.declarationNameIndex)) {
|
|
7422
|
+
if (isDeclarationStable(identifier, declaration, sinkIndex, fileContent) && isExplicitlyTrustedHtmlValue(declaration.initializer, fileContent, declaration.initializerStartIndex)) return false;
|
|
7423
|
+
if (!isDeclarationStable(identifier, declaration, sinkIndex, fileContent)) return true;
|
|
7424
|
+
if (isHtmlTainted(declaration.initializer, fileContent, declaration.initializerStartIndex, visitedIdentifiers, visitedCallSites)) return true;
|
|
7425
|
+
if (isBackedByFunctionParameter(declaration.initializer, declaration.initializerStartIndex, fileContent)) return false;
|
|
7426
|
+
return HTML_TAINT_PATTERN.test(trimmedExpression);
|
|
7427
|
+
}
|
|
7428
|
+
if (parameterSource !== null && parameterSource.functionName.length > 0) {
|
|
7429
|
+
const callPattern = new RegExp(`\\b${escapeRegExp(parameterSource.functionName)}\\s*\\(`, "g");
|
|
7430
|
+
let didInspectCallArgument = false;
|
|
7431
|
+
let didInspectOnlyExplicitlyTrustedArguments = true;
|
|
7432
|
+
for (const callMatch of fileContent.matchAll(callPattern)) {
|
|
7433
|
+
if (callMatch.index === parameterSource.declarationNameIndex || visitedCallSites.has(callMatch.index)) continue;
|
|
7434
|
+
const openingParenthesisIndex = callMatch.index + callMatch[0].lastIndexOf("(");
|
|
7435
|
+
const closingParenthesisIndex = findMatchingBracket(fileContent, openingParenthesisIndex);
|
|
7436
|
+
if (closingParenthesisIndex < 0) continue;
|
|
7437
|
+
const argument = splitTopLevelArguments(fileContent.slice(openingParenthesisIndex + 1, closingParenthesisIndex))[parameterSource.parameterIndex];
|
|
7438
|
+
if (argument === void 0) continue;
|
|
7439
|
+
if (callMatch.index >= parameterSource.declarationNameIndex && callMatch.index <= parameterSource.bodyEndIndex && doesExpressionAliasIdentifier(argument, identifier, callMatch.index, fileContent)) continue;
|
|
7440
|
+
didInspectCallArgument = true;
|
|
7441
|
+
didInspectOnlyExplicitlyTrustedArguments &&= isExplicitlyTrustedHtmlValue(argument, fileContent, callMatch.index);
|
|
7442
|
+
const callVisitedIdentifiers = new Set(visitedIdentifiers);
|
|
7443
|
+
callVisitedIdentifiers.delete(identifier);
|
|
7444
|
+
const nextVisitedCallSites = new Set(visitedCallSites);
|
|
7445
|
+
nextVisitedCallSites.add(callMatch.index);
|
|
7446
|
+
if (isHtmlTainted(argument, fileContent, callMatch.index, callVisitedIdentifiers, nextVisitedCallSites)) return true;
|
|
7447
|
+
}
|
|
7448
|
+
if (didInspectCallArgument) return !didInspectOnlyExplicitlyTrustedArguments && HTML_TAINT_PATTERN.test(trimmedExpression);
|
|
7449
|
+
return HTML_TAINT_PATTERN.test(trimmedExpression);
|
|
7450
|
+
}
|
|
7451
|
+
return HTML_TAINT_PATTERN.test(trimmedExpression);
|
|
7452
|
+
};
|
|
6906
7453
|
const dangerousHtmlSink = defineRule({
|
|
6907
7454
|
id: "dangerous-html-sink",
|
|
6908
7455
|
title: "HTML injection sink with dynamic content",
|
|
@@ -6929,6 +7476,7 @@ const dangerousHtmlSink = defineRule({
|
|
|
6929
7476
|
const valueTail = fullValueTail.slice(0, VALUE_EXPRESSION_MAX_CHARS);
|
|
6930
7477
|
const terminatorIndex = valueTail.search(/[;}]/);
|
|
6931
7478
|
const valueExpression = terminatorIndex >= 0 ? valueTail.slice(0, terminatorIndex + 1) : valueTail;
|
|
7479
|
+
const sinkIndex = lines.slice(0, lineIndex).join("\n").length + (lineIndex > 0 ? 1 : 0) + line.search(DANGEROUS_HTML_PATTERN);
|
|
6932
7480
|
if (STRING_LITERAL_VALUE_PATTERN.test(valueExpression)) continue;
|
|
6933
7481
|
if (MODULE_CONSTANT_VALUE_PATTERN.test(valueExpression)) continue;
|
|
6934
7482
|
if (DOM_CONTENT_SOURCE_VALUE_PATTERN.test(valueExpression) && !valueExpression.includes("+")) {
|
|
@@ -6939,8 +7487,8 @@ const dangerousHtmlSink = defineRule({
|
|
|
6939
7487
|
const longValueTail = HTML_VALUE_START_PATTERN.exec(lines.slice(lineIndex, lineIndex + 1 + STATIC_TEMPLATE_LOOKAHEAD_LINES).join("\n"))?.[1]?.trimStart();
|
|
6940
7488
|
let templateInterpolations = getTemplateInterpolations(longValueTail ?? fullValueTail);
|
|
6941
7489
|
const valueIdentifier = BARE_IDENTIFIER_VALUE_PATTERN.test(valueExpression) || MEMBER_OR_INDEX_ACCESS_VALUE_PATTERN.test(valueExpression) || IDENTIFIER_WITH_LITERAL_FALLBACK_PATTERN.test(valueExpression) ? valueExpression.match(/^[\w$]+/)?.[0] : void 0;
|
|
6942
|
-
if (templateInterpolations === null && valueIdentifier !== void 0 && BARE_IDENTIFIER_VALUE_PATTERN.test(valueExpression)) {
|
|
6943
|
-
const declarationInitializer = getIdentifierDeclarationInitializer(valueIdentifier, file.content);
|
|
7490
|
+
if (templateInterpolations === null && valueIdentifier !== void 0 && BARE_IDENTIFIER_VALUE_PATTERN.test(valueExpression) && findContainingFunctionParameterSource(valueIdentifier, sinkIndex, file.content) === null) {
|
|
7491
|
+
const declarationInitializer = getIdentifierDeclarationInitializer(valueIdentifier, sinkIndex, file.content);
|
|
6944
7492
|
if (declarationInitializer !== null) {
|
|
6945
7493
|
if (isPureStringLiteralConcat(declarationInitializer)) continue;
|
|
6946
7494
|
templateInterpolations = getTemplateInterpolations(declarationInitializer);
|
|
@@ -6948,17 +7496,23 @@ const dangerousHtmlSink = defineRule({
|
|
|
6948
7496
|
}
|
|
6949
7497
|
if (templateInterpolations === "") continue;
|
|
6950
7498
|
const judgedExpression = templateInterpolations ?? valueExpression;
|
|
6951
|
-
|
|
6952
|
-
if (
|
|
6953
|
-
if (
|
|
6954
|
-
if (!
|
|
7499
|
+
const doesJudgedExpressionCombineValues = splitTopLevelByPlus(judgedExpression).length > 1 || (templateInterpolations?.match(/\$\{/g)?.length ?? 0) > 1;
|
|
7500
|
+
if (!doesJudgedExpressionCombineValues && SANITIZER_PATTERN.test(judgedExpression)) continue;
|
|
7501
|
+
if (!doesJudgedExpressionCombineValues && ENV_CONFIG_VALUE_PATTERN.test(judgedExpression)) continue;
|
|
7502
|
+
if (!doesJudgedExpressionCombineValues && I18N_VALUE_PATTERN.test(judgedExpression)) continue;
|
|
7503
|
+
if (!isHtmlTainted(judgedExpression, file.content, sinkIndex, /* @__PURE__ */ new Set(), /* @__PURE__ */ new Set())) continue;
|
|
6955
7504
|
if (ESCAPING_SERIALIZER_CALL_PATTERN.test(valueExpression)) continue;
|
|
6956
|
-
if (
|
|
6957
|
-
if (/highlight/i.test(valueExpression) && HIGHLIGHTER_LIBRARY_PATTERN.test(file.content)) continue;
|
|
7505
|
+
if (isTrustedHighlighterValue(valueExpression, file.content, sinkIndex)) continue;
|
|
6958
7506
|
if (valueIdentifier !== void 0) {
|
|
6959
7507
|
const escapedIdentifier = escapeRegExp(valueIdentifier);
|
|
6960
|
-
|
|
6961
|
-
|
|
7508
|
+
const visibleDeclaration = findVisibleIdentifierDeclaration(valueIdentifier, sinkIndex, file.content);
|
|
7509
|
+
const visibleInitializer = visibleDeclaration?.initializer;
|
|
7510
|
+
if (!(visibleInitializer !== void 0 && (splitTopLevelByPlus(visibleInitializer).length > 1 || (visibleInitializer.match(/\$\{/g)?.length ?? 0) > 1))) {
|
|
7511
|
+
const fromSerializer = new RegExp(`\\b${escapedIdentifier}\\b\\s*${SERIALIZER_ASSIGNMENT_PATTERN.source}`, "i");
|
|
7512
|
+
if (visibleInitializer === void 0 ? fromSerializer.test(file.content) : visibleDeclaration !== null && isDeclarationStable(valueIdentifier, visibleDeclaration, sinkIndex, file.content) && SERIALIZER_CALL_PROVENANCE_PATTERN.test(visibleInitializer)) continue;
|
|
7513
|
+
const fromSanitizer = new RegExp(`\\b${escapedIdentifier}\\b\\s*${SANITIZED_ASSIGNMENT_PATTERN.source}`, "i");
|
|
7514
|
+
if (visibleInitializer === void 0 ? fromSanitizer.test(file.content) : visibleDeclaration !== null && isDeclarationStable(valueIdentifier, visibleDeclaration, sinkIndex, file.content) && SANITIZED_ASSIGNMENT_PATTERN.test(`=${visibleInitializer}`)) continue;
|
|
7515
|
+
}
|
|
6962
7516
|
if (new RegExp(`\\b${escapedIdentifier}\\b\\s*${DOM_CONTENT_ASSIGNMENT_PATTERN.source}`).test(file.content)) continue;
|
|
6963
7517
|
if (new RegExp(`highlight[\\w$]*\\s*\\.map\\(\\s*(?:async\\s+)?\\(?\\s*${escapedIdentifier}\\b`, "i").test(file.content) && HIGHLIGHTER_LIBRARY_PATTERN.test(file.content)) continue;
|
|
6964
7518
|
}
|
|
@@ -7043,7 +7597,7 @@ const findJsxAttribute = (attributes, attributeName) => attributes?.find((attrib
|
|
|
7043
7597
|
const getOpeningElementTagName = (openingElement) => {
|
|
7044
7598
|
if (!openingElement) return null;
|
|
7045
7599
|
if (!isNodeOfType(openingElement, "JSXOpeningElement")) return null;
|
|
7046
|
-
if (isNodeOfType(openingElement.name, "JSXIdentifier")) return openingElement
|
|
7600
|
+
if (isNodeOfType(openingElement.name, "JSXIdentifier")) return resolveJsxElementType(openingElement);
|
|
7047
7601
|
if (isNodeOfType(openingElement.name, "JSXMemberExpression")) {
|
|
7048
7602
|
let cursor = openingElement.name;
|
|
7049
7603
|
while (isNodeOfType(cursor, "JSXMemberExpression")) cursor = cursor.property;
|
|
@@ -7295,7 +7849,7 @@ const dialogHasAccessibleName = defineRule({
|
|
|
7295
7849
|
if (isTestlikeFilename(context.filename)) return {};
|
|
7296
7850
|
return { JSXOpeningElement(node) {
|
|
7297
7851
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
7298
|
-
const tagName = node
|
|
7852
|
+
const tagName = resolveJsxElementType(node);
|
|
7299
7853
|
if (tagName[0] !== tagName[0]?.toLowerCase()) return;
|
|
7300
7854
|
const roleAttribute = hasJsxPropIgnoreCase(node.attributes, "role");
|
|
7301
7855
|
const roleValue = roleAttribute ? getJsxPropStringValue(roleAttribute) : null;
|
|
@@ -7398,7 +7952,7 @@ const containsJsx$1 = (root) => {
|
|
|
7398
7952
|
containsJsxCache.set(root, found);
|
|
7399
7953
|
return found;
|
|
7400
7954
|
};
|
|
7401
|
-
const getStaticMemberName$
|
|
7955
|
+
const getStaticMemberName$2 = (node) => {
|
|
7402
7956
|
if (!isNodeOfType(node, "MemberExpression")) return null;
|
|
7403
7957
|
if (!node.computed && isNodeOfType(node.property, "Identifier")) return node.property.name;
|
|
7404
7958
|
if (node.computed && isNodeOfType(node.property, "Literal") && typeof node.property.value === "string") return node.property.value;
|
|
@@ -7412,7 +7966,7 @@ const getAssignedName = (node) => {
|
|
|
7412
7966
|
if (isNodeOfType(parent, "AssignmentExpression")) {
|
|
7413
7967
|
const left = parent.left;
|
|
7414
7968
|
if (isNodeOfType(left, "Identifier")) return left.name;
|
|
7415
|
-
if (isNodeOfType(left, "MemberExpression")) return getStaticMemberName$
|
|
7969
|
+
if (isNodeOfType(left, "MemberExpression")) return getStaticMemberName$2(left);
|
|
7416
7970
|
}
|
|
7417
7971
|
return null;
|
|
7418
7972
|
};
|
|
@@ -7420,20 +7974,20 @@ const isModuleExportsAssignment = (node) => {
|
|
|
7420
7974
|
const parent = node.parent;
|
|
7421
7975
|
if (!parent || !isNodeOfType(parent, "AssignmentExpression")) return false;
|
|
7422
7976
|
const left = parent.left;
|
|
7423
|
-
return isNodeOfType(left, "MemberExpression") && isNodeOfType(left.object, "Identifier") && left.object.name === "module" && getStaticMemberName$
|
|
7977
|
+
return isNodeOfType(left, "MemberExpression") && isNodeOfType(left.object, "Identifier") && left.object.name === "module" && getStaticMemberName$2(left) === "exports";
|
|
7424
7978
|
};
|
|
7425
7979
|
const isCreateClassLikeCall = (node) => {
|
|
7426
7980
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
7427
7981
|
if (isEs5Component(node)) return true;
|
|
7428
7982
|
const callee = node.callee;
|
|
7429
|
-
if (isNodeOfType(callee, "MemberExpression")) return getStaticMemberName$
|
|
7983
|
+
if (isNodeOfType(callee, "MemberExpression")) return getStaticMemberName$2(callee) === "createClass";
|
|
7430
7984
|
return false;
|
|
7431
7985
|
};
|
|
7432
|
-
const isCreateContextCall = (node) => {
|
|
7986
|
+
const isCreateContextCall$1 = (node) => {
|
|
7433
7987
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
7434
7988
|
const callee = node.callee;
|
|
7435
7989
|
if (isNodeOfType(callee, "Identifier")) return callee.name === "createContext";
|
|
7436
|
-
return isNodeOfType(callee, "MemberExpression") && getStaticMemberName$
|
|
7990
|
+
return isNodeOfType(callee, "MemberExpression") && getStaticMemberName$2(callee) === "createContext";
|
|
7437
7991
|
};
|
|
7438
7992
|
const isNamedFunctionLike = (node) => (isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) && Boolean(node.id?.name);
|
|
7439
7993
|
const firstCallArgument = (node) => {
|
|
@@ -7491,7 +8045,7 @@ const memberExpressionPath = (node) => {
|
|
|
7491
8045
|
if (isNodeOfType(node, "Identifier")) return [node.name];
|
|
7492
8046
|
if (!isNodeOfType(node, "MemberExpression")) return [];
|
|
7493
8047
|
const objectPath = memberExpressionPath(node.object);
|
|
7494
|
-
const propertyName = getStaticMemberName$
|
|
8048
|
+
const propertyName = getStaticMemberName$2(node);
|
|
7495
8049
|
return propertyName ? [...objectPath, propertyName] : objectPath;
|
|
7496
8050
|
};
|
|
7497
8051
|
const displayNameAssignmentIndexCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -7501,7 +8055,7 @@ const getDisplayNameAssignmentIndex = (programRoot) => {
|
|
|
7501
8055
|
const identifierTargets = /* @__PURE__ */ new Set();
|
|
7502
8056
|
const memberPathSegments = /* @__PURE__ */ new Set();
|
|
7503
8057
|
const visit = (node) => {
|
|
7504
|
-
if (isNodeOfType(node, "AssignmentExpression") && isNodeOfType(node.left, "MemberExpression") && getStaticMemberName$
|
|
8058
|
+
if (isNodeOfType(node, "AssignmentExpression") && isNodeOfType(node.left, "MemberExpression") && getStaticMemberName$2(node.left) === "displayName") {
|
|
7505
8059
|
if (isNodeOfType(node.left.object, "Identifier")) identifierTargets.add(node.left.object.name);
|
|
7506
8060
|
for (const segment of memberExpressionPath(node.left.object)) memberPathSegments.add(segment);
|
|
7507
8061
|
}
|
|
@@ -7606,7 +8160,7 @@ const displayName = defineRule({
|
|
|
7606
8160
|
}
|
|
7607
8161
|
},
|
|
7608
8162
|
CallExpression(node) {
|
|
7609
|
-
if (settings.checkContextObjects && isReactVersionAtLeast$1(settings.reactVersion, 16, 3) && isCreateContextCall(node)) {
|
|
8163
|
+
if (settings.checkContextObjects && isReactVersionAtLeast$1(settings.reactVersion, 16, 3) && isCreateContextCall$1(node)) {
|
|
7610
8164
|
const assignedName = getAssignedName(node);
|
|
7611
8165
|
if (assignedName) {
|
|
7612
8166
|
const programRoot = findProgramRoot(node);
|
|
@@ -7653,21 +8207,6 @@ const displayName = defineRule({
|
|
|
7653
8207
|
}
|
|
7654
8208
|
});
|
|
7655
8209
|
//#endregion
|
|
7656
|
-
//#region src/plugin/utils/get-static-property-key-name.ts
|
|
7657
|
-
const getStaticPropertyKeyName = (node, options = {}) => {
|
|
7658
|
-
if (!isNodeOfType(node, "Property")) return null;
|
|
7659
|
-
if (node.computed) {
|
|
7660
|
-
if (options.allowComputedString && isNodeOfType(node.key, "Literal") && typeof node.key.value === "string") return node.key.value;
|
|
7661
|
-
return null;
|
|
7662
|
-
}
|
|
7663
|
-
if (isNodeOfType(node.key, "Identifier")) return node.key.name;
|
|
7664
|
-
if (isNodeOfType(node.key, "Literal")) {
|
|
7665
|
-
if (typeof node.key.value === "string") return node.key.value;
|
|
7666
|
-
if (options.stringifyNonStringLiterals) return String(node.key.value);
|
|
7667
|
-
}
|
|
7668
|
-
return null;
|
|
7669
|
-
};
|
|
7670
|
-
//#endregion
|
|
7671
8210
|
//#region src/plugin/utils/read-static-boolean.ts
|
|
7672
8211
|
const readStaticBoolean = (node) => {
|
|
7673
8212
|
if (!node) return null;
|
|
@@ -7676,31 +8215,16 @@ const readStaticBoolean = (node) => {
|
|
|
7676
8215
|
return unwrappedNode.value;
|
|
7677
8216
|
};
|
|
7678
8217
|
//#endregion
|
|
7679
|
-
//#region src/plugin/utils/resolve-const-identifier-alias.ts
|
|
7680
|
-
const resolveConstIdentifierAlias = (identifier, scopes) => {
|
|
7681
|
-
if (!isNodeOfType(identifier, "Identifier")) return null;
|
|
7682
|
-
const visitedSymbolIds = /* @__PURE__ */ new Set();
|
|
7683
|
-
let symbol = scopes.symbolFor(identifier);
|
|
7684
|
-
while (symbol?.kind === "const") {
|
|
7685
|
-
if (visitedSymbolIds.has(symbol.id) || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return null;
|
|
7686
|
-
visitedSymbolIds.add(symbol.id);
|
|
7687
|
-
const initializer = stripParenExpression(symbol.initializer);
|
|
7688
|
-
if (!isNodeOfType(initializer, "Identifier")) return symbol;
|
|
7689
|
-
symbol = scopes.symbolFor(initializer);
|
|
7690
|
-
}
|
|
7691
|
-
return symbol;
|
|
7692
|
-
};
|
|
7693
|
-
//#endregion
|
|
7694
8218
|
//#region src/plugin/utils/is-react-api-call.ts
|
|
7695
8219
|
const includesApiName = (apiNames, apiName) => typeof apiNames === "string" ? apiNames === apiName : apiNames.has(apiName);
|
|
7696
8220
|
const isImportedFromReact = (symbol) => {
|
|
7697
8221
|
if (symbol.kind !== "import") return false;
|
|
7698
8222
|
const importDeclaration = symbol.declarationNode.parent;
|
|
7699
|
-
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && importDeclaration.source.value === "
|
|
8223
|
+
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && typeof importDeclaration.source.value === "string" && REACT_RUNTIME_MODULE_SOURCES.has(importDeclaration.source.value));
|
|
7700
8224
|
};
|
|
7701
|
-
const isNamedReactApiImport = (identifier, apiNames, scopes) => {
|
|
8225
|
+
const isNamedReactApiImport = (identifier, apiNames, scopes, resolveAliases) => {
|
|
7702
8226
|
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
7703
|
-
const symbol = scopes.symbolFor(identifier);
|
|
8227
|
+
const symbol = resolveAliases ? resolveConstIdentifierAlias(identifier, scopes) : scopes.symbolFor(identifier);
|
|
7704
8228
|
if (!symbol || !isImportedFromReact(symbol)) return false;
|
|
7705
8229
|
const importedName = getImportedName(symbol.declarationNode);
|
|
7706
8230
|
return Boolean(importedName && includesApiName(apiNames, importedName));
|
|
@@ -7714,7 +8238,7 @@ const isReactApiCall = (node, apiNames, scopes, options = {}) => {
|
|
|
7714
8238
|
if (!isNodeOfType(node, "CallExpression")) return false;
|
|
7715
8239
|
const callee = stripParenExpression(node.callee);
|
|
7716
8240
|
if (isNodeOfType(callee, "Identifier")) {
|
|
7717
|
-
if (isNamedReactApiImport(callee, apiNames, scopes)) return true;
|
|
8241
|
+
if (isNamedReactApiImport(callee, apiNames, scopes, Boolean(options.resolveNamedAliases))) return true;
|
|
7718
8242
|
return Boolean(options.allowUnboundBareCalls && includesApiName(apiNames, callee.name) && scopes.isGlobalReference(callee));
|
|
7719
8243
|
}
|
|
7720
8244
|
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier") || !includesApiName(apiNames, callee.property.name)) return false;
|
|
@@ -8817,6 +9341,30 @@ const doMatchingNodesCoverEveryPathAfterUsage = (usageNode, matchingNodes, conte
|
|
|
8817
9341
|
}
|
|
8818
9342
|
return matchingBlocks.size > 0;
|
|
8819
9343
|
};
|
|
9344
|
+
const doMatchingNodesCoverEveryPathFromFunctionEntry = (owner, matchingNodes, context) => {
|
|
9345
|
+
const functionCfg = context.cfg.cfgFor(owner);
|
|
9346
|
+
if (!functionCfg) return false;
|
|
9347
|
+
const matchingBlocks = new Set(matchingNodes.flatMap((matchingNode) => {
|
|
9348
|
+
if (context.cfg.enclosingFunction(matchingNode) !== owner) return [];
|
|
9349
|
+
const matchingBlock = functionCfg.blockOf(matchingNode);
|
|
9350
|
+
return matchingBlock ? [matchingBlock] : [];
|
|
9351
|
+
}));
|
|
9352
|
+
if (matchingBlocks.size === 0) return false;
|
|
9353
|
+
const visitedBlocks = new Set([functionCfg.entry]);
|
|
9354
|
+
const pendingBlocks = [functionCfg.entry];
|
|
9355
|
+
while (pendingBlocks.length > 0) {
|
|
9356
|
+
const currentBlock = pendingBlocks.pop();
|
|
9357
|
+
if (!currentBlock) break;
|
|
9358
|
+
if (matchingBlocks.has(currentBlock)) continue;
|
|
9359
|
+
for (const edge of currentBlock.successors) {
|
|
9360
|
+
if (edge.to === functionCfg.exit) return false;
|
|
9361
|
+
if (visitedBlocks.has(edge.to)) continue;
|
|
9362
|
+
visitedBlocks.add(edge.to);
|
|
9363
|
+
pendingBlocks.push(edge.to);
|
|
9364
|
+
}
|
|
9365
|
+
}
|
|
9366
|
+
return true;
|
|
9367
|
+
};
|
|
8820
9368
|
const removeSynchronouslyReleasedUsages = (callback, usages, context) => {
|
|
8821
9369
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return usages;
|
|
8822
9370
|
if (!isNodeOfType(callback.body, "BlockStatement")) return usages;
|
|
@@ -9017,6 +9565,83 @@ const doesCleanupFunctionReleaseUsage = (cleanupFunction, usage, context, visite
|
|
|
9017
9565
|
});
|
|
9018
9566
|
return didCleanupFunctionMatch;
|
|
9019
9567
|
};
|
|
9568
|
+
const doesBoundCleanupReleaseUsage = (expression, usage, context) => {
|
|
9569
|
+
const callExpression = stripParenExpression(expression);
|
|
9570
|
+
if (!isNodeOfType(callExpression, "CallExpression")) return false;
|
|
9571
|
+
const bindCallee = stripParenExpression(callExpression.callee);
|
|
9572
|
+
if (!isNodeOfType(bindCallee, "MemberExpression") || bindCallee.computed || !isNodeOfType(bindCallee.property, "Identifier") || bindCallee.property.name !== "bind") return false;
|
|
9573
|
+
const releaseMember = stripParenExpression(bindCallee.object);
|
|
9574
|
+
if (!isNodeOfType(releaseMember, "MemberExpression") || releaseMember.computed || !isNodeOfType(releaseMember.property, "Identifier")) return false;
|
|
9575
|
+
const releaseReceiverKey = resolveExpressionKey$1(releaseMember.object, context);
|
|
9576
|
+
if (releaseReceiverKey === null || releaseReceiverKey !== resolveExpressionKey$1(callExpression.arguments?.[0], context)) return false;
|
|
9577
|
+
const releaseVerbName = releaseMember.property.name;
|
|
9578
|
+
if (usage.kind === "socket") return usage.handleKey === releaseReceiverKey && (SOCKET_RELEASE_VERB_NAMES.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName));
|
|
9579
|
+
return usage.kind === "subscribe" && usage.handleKey === releaseReceiverKey && (releaseVerbName === "unsubscribe" || releaseVerbName === "unsub" || releaseVerbName === "close" || releaseVerbName === "unwatch" || releaseVerbName === "unlisten" || BOUND_RESOURCE_RELEASE_METHOD_NAMES.has(releaseVerbName));
|
|
9580
|
+
};
|
|
9581
|
+
const callbackReturnsCleanupForUsage = (callback, usage, context) => {
|
|
9582
|
+
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
|
|
9583
|
+
const doesReturnedValueReleaseUsage = (returnedValue) => {
|
|
9584
|
+
if (doesBoundCleanupReleaseUsage(returnedValue, usage, context)) return true;
|
|
9585
|
+
const cleanupFunction = resolveStableValue(returnedValue, context);
|
|
9586
|
+
if (cleanupFunction && doesBoundCleanupReleaseUsage(cleanupFunction, usage, context)) return true;
|
|
9587
|
+
return Boolean(cleanupFunction && isFunctionLike$1(cleanupFunction) && doesCleanupFunctionReleaseUsage(cleanupFunction, usage, context));
|
|
9588
|
+
};
|
|
9589
|
+
if (!isNodeOfType(callback.body, "BlockStatement")) return doesReturnedValueReleaseUsage(stripParenExpression(callback.body));
|
|
9590
|
+
const matchingCleanupReturns = [];
|
|
9591
|
+
walkInsideStatementBlocks(callback.body, (child) => {
|
|
9592
|
+
if (isNodeOfType(child, "ReturnStatement") && child.argument && doesReturnedValueReleaseUsage(stripParenExpression(child.argument))) matchingCleanupReturns.push(child);
|
|
9593
|
+
});
|
|
9594
|
+
return doMatchingNodesCoverEveryPathFromFunctionEntry(callback, matchingCleanupReturns, context);
|
|
9595
|
+
};
|
|
9596
|
+
const hasRerunReleaseBeforeUsage = (callback, usage, context) => {
|
|
9597
|
+
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression") || !isNodeOfType(callback.body, "BlockStatement")) return false;
|
|
9598
|
+
const functionCfg = context.cfg.cfgFor(callback);
|
|
9599
|
+
const usageBlock = functionCfg?.blockOf(usage.node);
|
|
9600
|
+
const usageStart = getRangeStart(usage.node);
|
|
9601
|
+
if (!functionCfg || !usageBlock || usageStart === null) return false;
|
|
9602
|
+
const findHandleGuard = (releaseCall) => {
|
|
9603
|
+
if (usage.handleKey === null) return null;
|
|
9604
|
+
let ancestor = releaseCall.parent;
|
|
9605
|
+
while (ancestor && ancestor !== callback.body) {
|
|
9606
|
+
if (isNodeOfType(ancestor, "IfStatement")) return ancestor.alternate === null && resolveExpressionKey$1(ancestor.test, context) === usage.handleKey && getRangeStart(ancestor) !== null && (getRangeStart(ancestor) ?? usageStart) < usageStart ? ancestor : null;
|
|
9607
|
+
ancestor = ancestor.parent;
|
|
9608
|
+
}
|
|
9609
|
+
return null;
|
|
9610
|
+
};
|
|
9611
|
+
const matchingReleaseAnchors = [];
|
|
9612
|
+
walkInsideStatementBlocks(callback.body, (child) => {
|
|
9613
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
9614
|
+
const releaseStart = getRangeStart(child);
|
|
9615
|
+
const handleGuard = findHandleGuard(child);
|
|
9616
|
+
if (releaseStart === null || releaseStart >= usageStart || functionCfg.blockOf(child) !== usageBlock && !handleGuard) return;
|
|
9617
|
+
if (doesReleaseCallMatchUsage(child, usage, context)) {
|
|
9618
|
+
matchingReleaseAnchors.push(handleGuard ?? child);
|
|
9619
|
+
return;
|
|
9620
|
+
}
|
|
9621
|
+
const helperFunction = resolveStableValue(child.callee, context);
|
|
9622
|
+
if (helperFunction && isFunctionLike$1(helperFunction) && doesCleanupFunctionReleaseUsage(helperFunction, usage, context)) matchingReleaseAnchors.push(handleGuard ?? child);
|
|
9623
|
+
});
|
|
9624
|
+
return doMatchingNodesCoverEveryPathFromFunctionEntry(callback, matchingReleaseAnchors, context);
|
|
9625
|
+
};
|
|
9626
|
+
const hasStableUnmountCleanupForUsage = (callback, usage, context) => {
|
|
9627
|
+
const componentFunction = findEnclosingFunction(callback);
|
|
9628
|
+
if (!componentFunction || !isNodeOfType(componentFunction, "ArrowFunctionExpression") && !isNodeOfType(componentFunction, "FunctionExpression") && !isNodeOfType(componentFunction, "FunctionDeclaration")) return false;
|
|
9629
|
+
let didFindUnmountCleanup = false;
|
|
9630
|
+
walkAst(componentFunction.body, (child) => {
|
|
9631
|
+
if (didFindUnmountCleanup) return false;
|
|
9632
|
+
if (!isNodeOfType(child, "CallExpression") || findEnclosingFunction(child) !== componentFunction) return;
|
|
9633
|
+
if (!isHookCall$2(child, CLEANUP_EFFECT_HOOK_NAMES)) return;
|
|
9634
|
+
const dependencyList = child.arguments?.[1];
|
|
9635
|
+
if (!isNodeOfType(dependencyList, "ArrayExpression") || dependencyList.elements.length > 0) return;
|
|
9636
|
+
const cleanupCallback = getEffectCallback(child);
|
|
9637
|
+
if (cleanupCallback && cleanupCallback !== callback && callbackReturnsCleanupForUsage(cleanupCallback, usage, context)) {
|
|
9638
|
+
didFindUnmountCleanup = true;
|
|
9639
|
+
return false;
|
|
9640
|
+
}
|
|
9641
|
+
});
|
|
9642
|
+
return didFindUnmountCleanup;
|
|
9643
|
+
};
|
|
9644
|
+
const hasSplitLifecycleCleanup = (callback, usage, context) => usage.handleKey !== null && hasRerunReleaseBeforeUsage(callback, usage, context) && hasStableUnmountCleanupForUsage(callback, usage, context);
|
|
9020
9645
|
const effectHasCleanupForUsage = (callback, usage, context) => {
|
|
9021
9646
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
|
|
9022
9647
|
if (usage.kind === "subscribe" && findEnclosingFunction(usage.node) === callback && doesResourceResultEscape(usage.node, true) && isCleanupReturningSubscribeLikeCallExpression(usage.node)) return true;
|
|
@@ -9029,6 +9654,10 @@ const effectHasCleanupForUsage = (callback, usage, context) => {
|
|
|
9029
9654
|
if (returnStart !== null && usageStart !== null && returnStart < usageStart) return;
|
|
9030
9655
|
const returnedValue = child.argument ? stripParenExpression(child.argument) : null;
|
|
9031
9656
|
if (!returnedValue) return;
|
|
9657
|
+
if (doesBoundCleanupReleaseUsage(returnedValue, usage, context)) {
|
|
9658
|
+
matchingCleanupReturns.push(child);
|
|
9659
|
+
return;
|
|
9660
|
+
}
|
|
9032
9661
|
if (usage.kind === "subscribe" && (returnedValue === usage.node || getRangeStart(returnedValue) !== null && getRangeStart(returnedValue) === getRangeStart(usage.node)) && isCleanupReturningSubscribeLikeCallExpression(returnedValue)) {
|
|
9033
9662
|
matchingCleanupReturns.push(child);
|
|
9034
9663
|
return;
|
|
@@ -9044,13 +9673,17 @@ const effectHasCleanupForUsage = (callback, usage, context) => {
|
|
|
9044
9673
|
if (!context.scopes.symbolFor(returnedValue)?.initializer) return;
|
|
9045
9674
|
}
|
|
9046
9675
|
const cleanupFunction = resolveStableValue(returnedValue, context);
|
|
9676
|
+
if (cleanupFunction && doesBoundCleanupReleaseUsage(cleanupFunction, usage, context)) {
|
|
9677
|
+
matchingCleanupReturns.push(child);
|
|
9678
|
+
return;
|
|
9679
|
+
}
|
|
9047
9680
|
if (!cleanupFunction || !isFunctionLike$1(cleanupFunction)) return;
|
|
9048
9681
|
if (doesCleanupFunctionReleaseUsage(cleanupFunction, usage, context)) matchingCleanupReturns.push(child);
|
|
9049
9682
|
});
|
|
9050
9683
|
return doMatchingNodesCoverEveryPathAfterUsage(resolveCleanupPathAnchor(usage.node, callback, context), matchingCleanupReturns, context);
|
|
9051
9684
|
};
|
|
9052
9685
|
const findFirstUsageWithoutCleanup = (callback, usages, context) => {
|
|
9053
|
-
for (const usage of usages) if (!effectHasCleanupForUsage(callback, usage, context)) return usage;
|
|
9686
|
+
for (const usage of usages) if (!effectHasCleanupForUsage(callback, usage, context) && !hasSplitLifecycleCleanup(callback, usage, context)) return usage;
|
|
9054
9687
|
return null;
|
|
9055
9688
|
};
|
|
9056
9689
|
const isLocalAbortControllerExpression = (expression, context, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
@@ -9646,6 +10279,11 @@ const walkParameterReferences = (pattern, state) => {
|
|
|
9646
10279
|
const walk = (node, state) => {
|
|
9647
10280
|
if (isFunctionLike$1(node)) {
|
|
9648
10281
|
if (isNodeOfType(node, "FunctionDeclaration") && node.id) handleFunctionDeclaration(node, state);
|
|
10282
|
+
const functionParams = node.params ?? [];
|
|
10283
|
+
for (const param of functionParams) {
|
|
10284
|
+
if (!("decorators" in param) || !Array.isArray(param.decorators)) continue;
|
|
10285
|
+
for (const decorator of param.decorators) if (isAstNode(decorator)) walk(decorator, state);
|
|
10286
|
+
}
|
|
9649
10287
|
setNodeScope(node, state);
|
|
9650
10288
|
const fnScope = pushScope(node.type === "ArrowFunctionExpression" ? "arrow-function" : "function", node, state);
|
|
9651
10289
|
if ((isNodeOfType(node, "FunctionExpression") || isNodeOfType(node, "FunctionDeclaration")) && node.id) {
|
|
@@ -9658,7 +10296,6 @@ const walk = (node, state) => {
|
|
|
9658
10296
|
});
|
|
9659
10297
|
tagAsBinding(state, node.id);
|
|
9660
10298
|
}
|
|
9661
|
-
const functionParams = node.params ?? [];
|
|
9662
10299
|
handleFunctionParameters(functionParams, fnScope, state);
|
|
9663
10300
|
for (const param of functionParams) walkParameterReferences(param, state);
|
|
9664
10301
|
const body = node.body;
|
|
@@ -9668,6 +10305,9 @@ const walk = (node, state) => {
|
|
|
9668
10305
|
}
|
|
9669
10306
|
if (isNodeOfType(node, "ClassDeclaration") || isNodeOfType(node, "ClassExpression")) {
|
|
9670
10307
|
if (isNodeOfType(node, "ClassDeclaration") && node.id) handleClassDeclaration(node, state);
|
|
10308
|
+
if (Array.isArray(node.decorators)) {
|
|
10309
|
+
for (const decorator of node.decorators) if (isAstNode(decorator)) walk(decorator, state);
|
|
10310
|
+
}
|
|
9671
10311
|
const classScope = pushScope("class", node, state);
|
|
9672
10312
|
setNodeScope(node, state);
|
|
9673
10313
|
if (isNodeOfType(node, "ClassExpression") && node.id) {
|
|
@@ -9870,7 +10510,7 @@ const computeClosureCaptures = (functionNode, scopes) => {
|
|
|
9870
10510
|
const functionScope = scopes.ownScopeFor(functionNode) ?? scopes.scopeFor(functionNode);
|
|
9871
10511
|
const out = [];
|
|
9872
10512
|
const seen = /* @__PURE__ */ new Set();
|
|
9873
|
-
|
|
10513
|
+
walkAst(functionNode, (node) => {
|
|
9874
10514
|
if (node !== functionNode && isFunctionLike$1(node)) {
|
|
9875
10515
|
const innerCaptures = closureCaptures(node, scopes);
|
|
9876
10516
|
for (const reference of innerCaptures) if (reference.resolvedSymbol && !isDescendantScope(reference.resolvedSymbol.scope, functionScope)) {
|
|
@@ -9879,7 +10519,7 @@ const computeClosureCaptures = (functionNode, scopes) => {
|
|
|
9879
10519
|
seen.add(reference.id);
|
|
9880
10520
|
}
|
|
9881
10521
|
}
|
|
9882
|
-
return;
|
|
10522
|
+
return false;
|
|
9883
10523
|
}
|
|
9884
10524
|
const reference = scopes.referenceFor(node);
|
|
9885
10525
|
if (reference && reference.resolvedSymbol) {
|
|
@@ -9890,17 +10530,7 @@ const computeClosureCaptures = (functionNode, scopes) => {
|
|
|
9890
10530
|
}
|
|
9891
10531
|
}
|
|
9892
10532
|
}
|
|
9893
|
-
|
|
9894
|
-
for (const key of Object.keys(record)) {
|
|
9895
|
-
if (key === "parent") continue;
|
|
9896
|
-
if (TYPE_POSITION_CHILD_KEYS.has(key)) continue;
|
|
9897
|
-
const child = record[key];
|
|
9898
|
-
if (Array.isArray(child)) {
|
|
9899
|
-
for (const item of child) if (isAstNode(item)) visit(item);
|
|
9900
|
-
} else if (isAstNode(child)) visit(child);
|
|
9901
|
-
}
|
|
9902
|
-
};
|
|
9903
|
-
visit(functionNode);
|
|
10533
|
+
});
|
|
9904
10534
|
return out;
|
|
9905
10535
|
};
|
|
9906
10536
|
const capturesByAnalysis = /* @__PURE__ */ new WeakMap();
|
|
@@ -10926,6 +11556,11 @@ If the missing value is recreated every render, move it inside the hook or stabi
|
|
|
10926
11556
|
return { CallExpression(node) {
|
|
10927
11557
|
const hookName = getHookName(node.callee, context.scopes);
|
|
10928
11558
|
if (!hookName || !isHookOfInterest(hookName, node.callee)) return;
|
|
11559
|
+
if (HOOKS_REQUIRING_DEPS_MATCH.has(hookName) && !isReactApiCall(node, HOOKS_REQUIRING_DEPS_MATCH, context.scopes, {
|
|
11560
|
+
allowGlobalReactNamespace: true,
|
|
11561
|
+
allowUnboundBareCalls: true,
|
|
11562
|
+
resolveNamedAliases: true
|
|
11563
|
+
})) return;
|
|
10929
11564
|
const callbackArgumentIndex = getCallbackArgumentIndex(hookName);
|
|
10930
11565
|
const depsArgumentIndex = getDepsArgumentIndex(hookName);
|
|
10931
11566
|
const callbackArgument = node.arguments[callbackArgumentIndex];
|
|
@@ -11446,7 +12081,7 @@ const forbidDomProps = defineRule({
|
|
|
11446
12081
|
return { JSXOpeningElement(node) {
|
|
11447
12082
|
if (forbidMap.size === 0) return;
|
|
11448
12083
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
11449
|
-
const elementName = node
|
|
12084
|
+
const elementName = resolveJsxElementType(node);
|
|
11450
12085
|
if (isReactComponentName(elementName)) return;
|
|
11451
12086
|
for (const attribute of node.attributes) {
|
|
11452
12087
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
@@ -11524,7 +12159,7 @@ const forbidElements = defineRule({
|
|
|
11524
12159
|
return {
|
|
11525
12160
|
JSXOpeningElement(node) {
|
|
11526
12161
|
if (forbidMap.size === 0) return;
|
|
11527
|
-
const fullName =
|
|
12162
|
+
const fullName = resolveJsxElementType(node);
|
|
11528
12163
|
if (!fullName || !forbidMap.has(fullName)) return;
|
|
11529
12164
|
context.report({
|
|
11530
12165
|
node: node.name,
|
|
@@ -11886,8 +12521,7 @@ const buildMessage$22 = (childTagName) => `Your users get reshuffled HTML becaus
|
|
|
11886
12521
|
const isParagraphElement = (candidate) => {
|
|
11887
12522
|
if (!isNodeOfType(candidate, "JSXElement")) return false;
|
|
11888
12523
|
const opening = candidate.openingElement;
|
|
11889
|
-
|
|
11890
|
-
return opening.name.name === "p";
|
|
12524
|
+
return resolveJsxElementType(opening) === "p";
|
|
11891
12525
|
};
|
|
11892
12526
|
const findEnclosingParagraph = (openingElement) => {
|
|
11893
12527
|
const owningElement = openingElement.parent;
|
|
@@ -11908,8 +12542,7 @@ const htmlNoInvalidParagraphChild = defineRule({
|
|
|
11908
12542
|
severity: "warn",
|
|
11909
12543
|
recommendation: "Swap the `<p>` for a `<div>`, or move the child outside the paragraph.",
|
|
11910
12544
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
11911
|
-
|
|
11912
|
-
const childTagName = node.name.name;
|
|
12545
|
+
const childTagName = resolveJsxElementType(node);
|
|
11913
12546
|
if (!BLOCK_LEVEL_ELEMENTS.has(childTagName)) return;
|
|
11914
12547
|
if (!findEnclosingParagraph(node)) return;
|
|
11915
12548
|
context.report({
|
|
@@ -11940,7 +12573,7 @@ const getHostTagName = (jsxElement) => {
|
|
|
11940
12573
|
if (!isNodeOfType(jsxElement, "JSXElement")) return null;
|
|
11941
12574
|
const opening = jsxElement.openingElement;
|
|
11942
12575
|
if (!isNodeOfType(opening.name, "JSXIdentifier")) return null;
|
|
11943
|
-
const tagName = opening
|
|
12576
|
+
const tagName = resolveJsxElementType(opening);
|
|
11944
12577
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return null;
|
|
11945
12578
|
return tagName;
|
|
11946
12579
|
};
|
|
@@ -11970,7 +12603,7 @@ const findClosestHostAncestor = (jsxElement) => {
|
|
|
11970
12603
|
if (isNodeOfType(ancestor, "JSXElement")) {
|
|
11971
12604
|
const opening = ancestor.openingElement;
|
|
11972
12605
|
if (isNodeOfType(opening.name, "JSXIdentifier")) {
|
|
11973
|
-
const ancestorTag = opening
|
|
12606
|
+
const ancestorTag = resolveJsxElementType(opening);
|
|
11974
12607
|
if (ancestorTag.length === 0) {
|
|
11975
12608
|
previous = ancestor;
|
|
11976
12609
|
ancestor = ancestor.parent ?? null;
|
|
@@ -12052,8 +12685,7 @@ const buildMessage$20 = (tagName) => `Your users get broken clicks, focus & scre
|
|
|
12052
12685
|
const isJsxElementWithTagName = (candidate, tagName) => {
|
|
12053
12686
|
if (!isNodeOfType(candidate, "JSXElement")) return false;
|
|
12054
12687
|
const opening = candidate.openingElement;
|
|
12055
|
-
|
|
12056
|
-
return opening.name.name === tagName;
|
|
12688
|
+
return resolveJsxElementType(opening) === tagName;
|
|
12057
12689
|
};
|
|
12058
12690
|
const findEnclosingSameTag = (openingElement, tagName) => {
|
|
12059
12691
|
const owningElement = openingElement.parent;
|
|
@@ -12074,8 +12706,7 @@ const htmlNoNestedInteractive = defineRule({
|
|
|
12074
12706
|
severity: "warn",
|
|
12075
12707
|
recommendation: "Move the inner `<a>` or `<button>` so it's a sibling, or change the outer one to a plain `<div>` or `<span>`.",
|
|
12076
12708
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
12077
|
-
|
|
12078
|
-
const tagName = node.name.name;
|
|
12709
|
+
const tagName = resolveJsxElementType(node);
|
|
12079
12710
|
if (tagName !== "a" && tagName !== "button") return;
|
|
12080
12711
|
if (!findEnclosingSameTag(node, tagName)) return;
|
|
12081
12712
|
context.report({
|
|
@@ -12190,7 +12821,7 @@ const iframeMissingSandbox = defineRule({
|
|
|
12190
12821
|
matchByOccurrence: true,
|
|
12191
12822
|
create: skipNonProductionFiles((context) => ({
|
|
12192
12823
|
JSXOpeningElement(node) {
|
|
12193
|
-
if (
|
|
12824
|
+
if (resolveJsxElementType(node) !== "iframe") return;
|
|
12194
12825
|
const sandboxAttr = hasJsxPropIgnoreCase(node.attributes, "sandbox");
|
|
12195
12826
|
if (!sandboxAttr) {
|
|
12196
12827
|
const hasExplicitSrc = Boolean(hasJsxPropIgnoreCase(node.attributes, "src"));
|
|
@@ -12426,30 +13057,6 @@ const insecureCryptoRisk = defineRule({
|
|
|
12426
13057
|
}
|
|
12427
13058
|
});
|
|
12428
13059
|
//#endregion
|
|
12429
|
-
//#region src/plugin/rules/security-scan/utils/find-matching-bracket.ts
|
|
12430
|
-
const findMatchingBracket = (content, openIndex) => {
|
|
12431
|
-
const open = content[openIndex];
|
|
12432
|
-
const close = open === "(" ? ")" : open === "{" ? "}" : open === "[" ? "]" : "";
|
|
12433
|
-
if (close === "") return -1;
|
|
12434
|
-
let depth = 0;
|
|
12435
|
-
let stringDelimiter = null;
|
|
12436
|
-
for (let index = openIndex; index < content.length; index += 1) {
|
|
12437
|
-
const character = content[index];
|
|
12438
|
-
if (stringDelimiter !== null) {
|
|
12439
|
-
if (character === "\\") index += 1;
|
|
12440
|
-
else if (character === stringDelimiter) stringDelimiter = null;
|
|
12441
|
-
continue;
|
|
12442
|
-
}
|
|
12443
|
-
if (character === "\"" || character === "'" || character === "`") stringDelimiter = character;
|
|
12444
|
-
else if (character === open) depth += 1;
|
|
12445
|
-
else if (character === close) {
|
|
12446
|
-
depth -= 1;
|
|
12447
|
-
if (depth === 0) return index;
|
|
12448
|
-
}
|
|
12449
|
-
}
|
|
12450
|
-
return -1;
|
|
12451
|
-
};
|
|
12452
|
-
//#endregion
|
|
12453
13060
|
//#region src/plugin/rules/security-scan/insecure-session-cookie.ts
|
|
12454
13061
|
const AUTH_COOKIE_NAME_TOKEN = `(?<![A-Za-z0-9])(?:session|sess|sid|connect\\.sid|auth|jwt|access[_-]?token|refresh[_-]?token|id[_-]?token)(?![A-Za-z0-9])`;
|
|
12455
13062
|
const AUTH_COOKIE_NAME_LITERAL = `[\`"'][^\`"']*?${AUTH_COOKIE_NAME_TOKEN}[^\`"']*[\`"']`;
|
|
@@ -13206,6 +13813,28 @@ const jsAsyncReduceWithoutAwaitedAcc = defineRule({
|
|
|
13206
13813
|
} })
|
|
13207
13814
|
});
|
|
13208
13815
|
//#endregion
|
|
13816
|
+
//#region src/plugin/utils/unwrap-discarded-expression.ts
|
|
13817
|
+
const unwrapDiscardedExpression = (node) => {
|
|
13818
|
+
let expression = isNodeOfType(node, "ExpressionStatement") ? node.expression : node;
|
|
13819
|
+
for (;;) {
|
|
13820
|
+
expression = stripParenExpression(expression);
|
|
13821
|
+
if (isNodeOfType(expression, "UnaryExpression") && expression.operator === "void") {
|
|
13822
|
+
expression = expression.argument;
|
|
13823
|
+
continue;
|
|
13824
|
+
}
|
|
13825
|
+
if (isNodeOfType(expression, "SequenceExpression")) {
|
|
13826
|
+
const expressions = expression.expressions ?? [];
|
|
13827
|
+
const finalExpression = expressions.at(-1);
|
|
13828
|
+
const prefixExpressions = expressions.slice(0, -1);
|
|
13829
|
+
if (finalExpression && prefixExpressions.length > 0 && prefixExpressions.every((prefixExpression) => isNodeOfType(stripParenExpression(prefixExpression), "Literal"))) {
|
|
13830
|
+
expression = finalExpression;
|
|
13831
|
+
continue;
|
|
13832
|
+
}
|
|
13833
|
+
}
|
|
13834
|
+
return expression;
|
|
13835
|
+
}
|
|
13836
|
+
};
|
|
13837
|
+
//#endregion
|
|
13209
13838
|
//#region src/plugin/rules/js-performance/js-batch-dom-css.ts
|
|
13210
13839
|
const ITERATOR_METHOD_NAMES$2 = new Set([
|
|
13211
13840
|
"forEach",
|
|
@@ -13390,9 +14019,19 @@ const hasAttachmentBefore = (scopeOwner, elementName, beforeStart) => {
|
|
|
13390
14019
|
});
|
|
13391
14020
|
return foundAttachment;
|
|
13392
14021
|
};
|
|
14022
|
+
const getStyleAssignment = (node) => {
|
|
14023
|
+
if (!isNodeOfType(node, "ExpressionStatement")) return null;
|
|
14024
|
+
const expression = unwrapDiscardedExpression(node);
|
|
14025
|
+
if (!isNodeOfType(expression, "AssignmentExpression")) return null;
|
|
14026
|
+
if (!isNodeOfType(expression.left, "MemberExpression")) return null;
|
|
14027
|
+
if (!isNodeOfType(expression.left.object, "MemberExpression")) return null;
|
|
14028
|
+
if (!isNodeOfType(expression.left.object.property, "Identifier")) return null;
|
|
14029
|
+
return expression.left.object.property.name === "style" ? expression : null;
|
|
14030
|
+
};
|
|
13393
14031
|
const isProvablyDetachedAtWrite = (styleWriteStatement) => {
|
|
13394
|
-
|
|
13395
|
-
|
|
14032
|
+
const assignment = getStyleAssignment(styleWriteStatement);
|
|
14033
|
+
if (!assignment || !isNodeOfType(assignment.left, "MemberExpression") || !isNodeOfType(assignment.left.object, "MemberExpression")) return false;
|
|
14034
|
+
const elementExpression = assignment.left.object.object;
|
|
13396
14035
|
const creationRoot = resolveDetachedCreationRoot(elementExpression, 0);
|
|
13397
14036
|
if (!creationRoot) return false;
|
|
13398
14037
|
return !hasAttachmentBefore(creationRoot.scopeOwner, creationRoot.rootName, getNodeStart$1(styleWriteStatement));
|
|
@@ -13404,15 +14043,17 @@ const jsBatchDomCss = defineRule({
|
|
|
13404
14043
|
severity: "warn",
|
|
13405
14044
|
recommendation: "Do all your reads first, then all your writes. Mixing them inside a loop makes the browser recalculate the layout again and again, which is slow",
|
|
13406
14045
|
create: (context) => {
|
|
13407
|
-
const
|
|
13408
|
-
|
|
14046
|
+
const writesLayoutAffectingProperty = (node) => {
|
|
14047
|
+
const assignment = getStyleAssignment(node);
|
|
14048
|
+
return assignment !== null && isNodeOfType(assignment.left, "MemberExpression") && isNodeOfType(assignment.left.property, "Identifier") && !LAYOUT_NEUTRAL_STYLE_PROPERTY_NAMES.has(assignment.left.property.name);
|
|
14049
|
+
};
|
|
13409
14050
|
return { BlockStatement(node) {
|
|
13410
14051
|
const perIterationBody = findEnclosingPerIterationBody(node);
|
|
13411
14052
|
if (!perIterationBody) return;
|
|
13412
14053
|
const statements = node.body ?? [];
|
|
13413
14054
|
let layoutReads = null;
|
|
13414
14055
|
for (let statementIndex = 1; statementIndex < statements.length; statementIndex++) {
|
|
13415
|
-
if (
|
|
14056
|
+
if (getStyleAssignment(statements[statementIndex]) === null || getStyleAssignment(statements[statementIndex - 1]) === null) continue;
|
|
13416
14057
|
if (!writesLayoutAffectingProperty(statements[statementIndex]) && !writesLayoutAffectingProperty(statements[statementIndex - 1])) continue;
|
|
13417
14058
|
layoutReads ??= scanPerIterationLayoutReads(perIterationBody);
|
|
13418
14059
|
if (!layoutReads.hasUsedLayoutRead || layoutReads.hasDeliberateForcedReflow) return;
|
|
@@ -14108,6 +14749,18 @@ const jsHoistRegexp = defineRule({
|
|
|
14108
14749
|
}, { treatIteratorCallbacksAsLoops: true })
|
|
14109
14750
|
});
|
|
14110
14751
|
//#endregion
|
|
14752
|
+
//#region src/plugin/utils/resolve-first-argument-binding.ts
|
|
14753
|
+
const resolveFirstArgumentBinding = (firstParameter) => {
|
|
14754
|
+
if (!firstParameter) return null;
|
|
14755
|
+
if (!isNodeOfType(firstParameter, "RestElement")) return firstParameter;
|
|
14756
|
+
if (!isNodeOfType(firstParameter.argument, "ArrayPattern")) return null;
|
|
14757
|
+
const elements = firstParameter.argument.elements ?? [];
|
|
14758
|
+
if (elements.length !== 1) return null;
|
|
14759
|
+
const firstBinding = elements[0];
|
|
14760
|
+
if (!firstBinding || isNodeOfType(firstBinding, "RestElement")) return null;
|
|
14761
|
+
return firstBinding;
|
|
14762
|
+
};
|
|
14763
|
+
//#endregion
|
|
14111
14764
|
//#region src/plugin/rules/js-performance/js-index-maps.ts
|
|
14112
14765
|
const referencesParameter = (expression, parameterName) => {
|
|
14113
14766
|
if (!expression) return false;
|
|
@@ -14118,7 +14771,7 @@ const referencesParameter = (expression, parameterName) => {
|
|
|
14118
14771
|
const isSingleFieldEqualityPredicate = (node) => {
|
|
14119
14772
|
const callback = node.arguments?.[0];
|
|
14120
14773
|
if (!isInlineFunctionExpression(callback)) return false;
|
|
14121
|
-
const firstParameter = callback.params?.[0];
|
|
14774
|
+
const firstParameter = resolveFirstArgumentBinding(callback.params?.[0]);
|
|
14122
14775
|
if (!firstParameter || !isNodeOfType(firstParameter, "Identifier")) return false;
|
|
14123
14776
|
let predicate = null;
|
|
14124
14777
|
const body = callback.body;
|
|
@@ -14220,7 +14873,11 @@ const jsIndexMaps = defineRule({
|
|
|
14220
14873
|
//#region src/plugin/utils/are-expressions-structurally-equal.ts
|
|
14221
14874
|
const areExpressionsStructurallyEqual = (a, b) => {
|
|
14222
14875
|
if (!a || !b) return a === b;
|
|
14876
|
+
const unwrappedA = stripParenExpression(a);
|
|
14877
|
+
const unwrappedB = stripParenExpression(b);
|
|
14878
|
+
if (unwrappedA !== a || unwrappedB !== b) return areExpressionsStructurallyEqual(unwrappedA, unwrappedB);
|
|
14223
14879
|
if (a.type !== b.type) return false;
|
|
14880
|
+
if (isNodeOfType(a, "ThisExpression")) return true;
|
|
14224
14881
|
if (isNodeOfType(a, "Identifier") && isNodeOfType(b, "Identifier")) return a.name === b.name;
|
|
14225
14882
|
if (isNodeOfType(a, "Literal") && isNodeOfType(b, "Literal")) return a.value === b.value;
|
|
14226
14883
|
if (isNodeOfType(a, "MemberExpression") && isNodeOfType(b, "MemberExpression")) {
|
|
@@ -14806,9 +15463,21 @@ const isSmallFixedListMember = (receiver) => {
|
|
|
14806
15463
|
};
|
|
14807
15464
|
const getResolvedInitializer = (receiver) => {
|
|
14808
15465
|
if (!isNodeOfType(receiver, "Identifier")) return null;
|
|
14809
|
-
const
|
|
14810
|
-
|
|
14811
|
-
return
|
|
15466
|
+
const binding = findVariableInitializer(receiver, receiver.name);
|
|
15467
|
+
const initializer = binding?.initializer ?? null;
|
|
15468
|
+
if (!binding || !initializer) return null;
|
|
15469
|
+
const isDefault = isNodeOfType(binding.bindingIdentifier.parent, "AssignmentPattern");
|
|
15470
|
+
if (isNodeOfType(initializer, "Identifier")) {
|
|
15471
|
+
const aliased = findVariableInitializer(initializer, initializer.name);
|
|
15472
|
+
if (aliased?.initializer) return {
|
|
15473
|
+
initializer: aliased.initializer,
|
|
15474
|
+
isDefault: isDefault || isNodeOfType(aliased.bindingIdentifier.parent, "AssignmentPattern")
|
|
15475
|
+
};
|
|
15476
|
+
}
|
|
15477
|
+
return {
|
|
15478
|
+
initializer,
|
|
15479
|
+
isDefault
|
|
15480
|
+
};
|
|
14812
15481
|
};
|
|
14813
15482
|
const isSplitCall = (expression) => {
|
|
14814
15483
|
if (!expression) return false;
|
|
@@ -14974,8 +15643,8 @@ const isBoundedConstantCollection = (collection) => {
|
|
|
14974
15643
|
if (isScreamingSnakeCaseConstantReceiver(stripped)) return true;
|
|
14975
15644
|
if (isSmallInlineLiteralArray(stripped)) return true;
|
|
14976
15645
|
if (isNodeOfType(stripped, "Identifier")) {
|
|
14977
|
-
const
|
|
14978
|
-
if (
|
|
15646
|
+
const resolved = getResolvedInitializer(stripped);
|
|
15647
|
+
if (resolved && !resolved.isDefault && isSmallInlineLiteralArray(resolved.initializer)) return true;
|
|
14979
15648
|
}
|
|
14980
15649
|
return false;
|
|
14981
15650
|
};
|
|
@@ -15027,8 +15696,8 @@ const jsSetMapLookups = defineRule({
|
|
|
15027
15696
|
if (isIndexedArrayElementWithStringArgument(receiver, node.arguments?.[0])) return;
|
|
15028
15697
|
const resolvedInitializer = getResolvedInitializer(receiver);
|
|
15029
15698
|
if (resolvedInitializer) {
|
|
15030
|
-
if (isLikelyStringReceiver(resolvedInitializer)) return;
|
|
15031
|
-
if (isSmallInlineLiteralArray(resolvedInitializer)) return;
|
|
15699
|
+
if (isLikelyStringReceiver(resolvedInitializer.initializer)) return;
|
|
15700
|
+
if (!resolvedInitializer.isDefault && isSmallInlineLiteralArray(resolvedInitializer.initializer)) return;
|
|
15032
15701
|
}
|
|
15033
15702
|
if (isStringElementOfSplitIteration(receiver)) return;
|
|
15034
15703
|
if (isReceiverDeclaredInNearestLoop(receiver, node)) return;
|
|
@@ -15441,14 +16110,21 @@ const jsxFilenameExtension = defineRule({
|
|
|
15441
16110
|
});
|
|
15442
16111
|
//#endregion
|
|
15443
16112
|
//#region src/plugin/utils/is-jsx-fragment-element.ts
|
|
15444
|
-
const isJsxFragmentElement = (node) => {
|
|
16113
|
+
const isJsxFragmentElement = (node, scopes) => {
|
|
15445
16114
|
if (!isNodeOfType(node, "JSXOpeningElement")) return false;
|
|
15446
16115
|
const elementName = node.name;
|
|
15447
|
-
if (isNodeOfType(elementName, "JSXIdentifier"))
|
|
16116
|
+
if (isNodeOfType(elementName, "JSXIdentifier")) {
|
|
16117
|
+
if (!scopes) return elementName.name === "Fragment";
|
|
16118
|
+
const symbol = resolveConstIdentifierAlias(elementName, scopes);
|
|
16119
|
+
if (!symbol) return elementName.name === "Fragment" && scopes.isGlobalReference(elementName);
|
|
16120
|
+
return isImportedFromReact(symbol) && getImportedName(symbol.declarationNode) === "Fragment";
|
|
16121
|
+
}
|
|
15448
16122
|
if (isNodeOfType(elementName, "JSXMemberExpression")) {
|
|
15449
16123
|
if (!isNodeOfType(elementName.object, "JSXIdentifier")) return false;
|
|
15450
|
-
if (elementName.
|
|
15451
|
-
return elementName.
|
|
16124
|
+
if (elementName.property.name !== "Fragment") return false;
|
|
16125
|
+
if (!scopes) return elementName.object.name === "React";
|
|
16126
|
+
if (isReactNamespaceImport(elementName.object, scopes)) return true;
|
|
16127
|
+
return elementName.object.name === "React" && scopes.isGlobalReference(elementName.object);
|
|
15452
16128
|
}
|
|
15453
16129
|
return false;
|
|
15454
16130
|
};
|
|
@@ -15474,7 +16150,7 @@ const jsxFragments = defineRule({
|
|
|
15474
16150
|
if (mode !== "syntax") return;
|
|
15475
16151
|
if (!node.closingElement) return;
|
|
15476
16152
|
const openingElement = node.openingElement;
|
|
15477
|
-
if (!isJsxFragmentElement(openingElement)) return;
|
|
16153
|
+
if (!isJsxFragmentElement(openingElement, context.scopes)) return;
|
|
15478
16154
|
if (openingElement.attributes.length > 0) return;
|
|
15479
16155
|
context.report({
|
|
15480
16156
|
node: openingElement,
|
|
@@ -18155,10 +18831,6 @@ const resolveSettings$29 = (settings) => {
|
|
|
18155
18831
|
if (typeof reactDoctor !== "object" || reactDoctor === null) return {};
|
|
18156
18832
|
return reactDoctor.jsxNoScriptUrl ?? {};
|
|
18157
18833
|
};
|
|
18158
|
-
const getElementName = (node) => {
|
|
18159
|
-
if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
|
|
18160
|
-
return null;
|
|
18161
|
-
};
|
|
18162
18834
|
const isLinkPropForElement = (elementName, attributeName, options) => {
|
|
18163
18835
|
if (elementName === "a" && attributeName === "href") return true;
|
|
18164
18836
|
const explicit = options.components?.[elementName];
|
|
@@ -18178,7 +18850,8 @@ const jsxNoScriptUrl = defineRule({
|
|
|
18178
18850
|
create: (context) => {
|
|
18179
18851
|
const options = resolveSettings$29(context.settings);
|
|
18180
18852
|
return { JSXOpeningElement(node) {
|
|
18181
|
-
|
|
18853
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
18854
|
+
const elementName = resolveJsxElementType(node);
|
|
18182
18855
|
if (!elementName) return;
|
|
18183
18856
|
for (const attribute of node.attributes) {
|
|
18184
18857
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
@@ -18365,11 +19038,6 @@ const checkTarget = (attributeValue) => {
|
|
|
18365
19038
|
alternate: false
|
|
18366
19039
|
};
|
|
18367
19040
|
};
|
|
18368
|
-
const getOpeningElementName = (node) => {
|
|
18369
|
-
const name = node.name;
|
|
18370
|
-
if (isNodeOfType(name, "JSXIdentifier")) return name.name;
|
|
18371
|
-
return null;
|
|
18372
|
-
};
|
|
18373
19041
|
const jsxNoTargetBlank = defineRule({
|
|
18374
19042
|
id: "jsx-no-target-blank",
|
|
18375
19043
|
title: "Unsafe target=_blank link",
|
|
@@ -18389,7 +19057,8 @@ const jsxNoTargetBlank = defineRule({
|
|
|
18389
19057
|
return settings.formComponents.has(tagName);
|
|
18390
19058
|
};
|
|
18391
19059
|
return { JSXOpeningElement(node) {
|
|
18392
|
-
|
|
19060
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
19061
|
+
const tagName = resolveJsxElementType(node);
|
|
18393
19062
|
if (!tagName) return;
|
|
18394
19063
|
if (!isLink(tagName) && !isForm(tagName)) return;
|
|
18395
19064
|
const linkAttributeNames = settings.linkComponents.get(tagName) ?? ["href"];
|
|
@@ -18622,7 +19291,7 @@ const jsxNoUselessFragment = defineRule({
|
|
|
18622
19291
|
return {
|
|
18623
19292
|
JSXElement(node) {
|
|
18624
19293
|
const openingElement = node.openingElement;
|
|
18625
|
-
if (!isJsxFragmentElement(openingElement)) return;
|
|
19294
|
+
if (!isJsxFragmentElement(openingElement, context.scopes)) return;
|
|
18626
19295
|
if (hasJsxKeyAttribute(openingElement)) return;
|
|
18627
19296
|
if (checkChildren(node, openingElement, node.children)) return;
|
|
18628
19297
|
if (isChildOfHtmlElement(node)) context.report({
|
|
@@ -18924,6 +19593,7 @@ const jwtInsecureVerification = defineRule({
|
|
|
18924
19593
|
});
|
|
18925
19594
|
//#endregion
|
|
18926
19595
|
//#region src/plugin/rules/security-scan/key-lifecycle-risk.ts
|
|
19596
|
+
const KEY_MATERIAL_MARKER_PATTERN = /PRIVATE KEY|SSH_PRIVATE_KEY|GPG_PRIVATE_KEY|DEPLOY_KEY|SIGNING_KEY/i;
|
|
18927
19597
|
const keyLifecycleRisk = defineRule({
|
|
18928
19598
|
id: "key-lifecycle-risk",
|
|
18929
19599
|
title: "Long-lived key material in repository",
|
|
@@ -18931,7 +19601,7 @@ const keyLifecycleRisk = defineRule({
|
|
|
18931
19601
|
committedFilesOnly: true,
|
|
18932
19602
|
recommendation: "Remove private keys from source, rotate exposed credentials, prefer short-lived deploy credentials, and document revocation/expiry for release keys.",
|
|
18933
19603
|
scan: scanByPattern({
|
|
18934
|
-
shouldScan: (file) => !TEST_CONTEXT_PATTERN.test(file.relativePath) && !DOCUMENTATION_CONTEXT_PATTERN.test(file.relativePath),
|
|
19604
|
+
shouldScan: (file) => !TEST_CONTEXT_PATTERN.test(file.relativePath) && !DOCUMENTATION_CONTEXT_PATTERN.test(file.relativePath) && KEY_MATERIAL_MARKER_PATTERN.test(file.content),
|
|
18935
19605
|
pattern: /(?<!(?:placeholder|example|sample|dummy|fake)[\s\S]{0,40})-----BEGIN (?:RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----(?:\s|\\r|\\n)*[A-Za-z0-9+/=][A-Za-z0-9+/=\s]{38,}(?![^-]{0,160}\.\.\.)|\b(?:SSH_PRIVATE_KEY|GPG_PRIVATE_KEY|DEPLOY_KEY|SIGNING_KEY)\b\s*[:=]\s*["'][^"'\n]{16,}["']/i,
|
|
18936
19606
|
message: "Private or long-lived release key material appears in the repository."
|
|
18937
19607
|
})
|
|
@@ -19637,15 +20307,21 @@ const localRpcNativeBridgeRisk = defineRule({
|
|
|
19637
20307
|
message: "Code appears to bridge browser code to localhost/native capabilities with weak origin or update/install checks."
|
|
19638
20308
|
})
|
|
19639
20309
|
});
|
|
20310
|
+
//#endregion
|
|
20311
|
+
//#region src/plugin/rules/security-scan/mcp-tool-capability-risk.ts
|
|
20312
|
+
const MCP_IMPORT_PATTERN = /\bfrom\s+["']@modelcontextprotocol\/sdk[^"']*["']|\bMcpServer\b|\bMcpAgent\b/;
|
|
20313
|
+
const MCP_IMPORT_PREFILTER_PATTERN = /@modelcontextprotocol\/sdk|\bMcpServer\b|\bMcpAgent\b/;
|
|
20314
|
+
const MCP_TOOL_SURFACE_PATTERN = /\bserver\.\s*tool\s*\(|\bregisterTool\s*\(|\bsetRequestHandler\s*\(\s*CallToolRequestSchema/;
|
|
20315
|
+
const MCP_TOOL_SURFACE_PREFILTER_PATTERN = /\b(?:tool|registerTool|setRequestHandler)\b/;
|
|
19640
20316
|
const mcpToolCapabilityRisk = defineRule({
|
|
19641
20317
|
id: "mcp-tool-capability-risk",
|
|
19642
20318
|
title: "MCP tool exposes dangerous capability",
|
|
19643
20319
|
severity: "warn",
|
|
19644
20320
|
recommendation: "MCP tool calls run with the connecting client's authority. Validate inputs, enforce per-tool authorization, and avoid raw filesystem/shell/network access where possible.",
|
|
19645
20321
|
scan: scanByPattern({
|
|
19646
|
-
shouldScan: (file) => isProductionSourcePath(file.relativePath),
|
|
19647
|
-
pattern:
|
|
19648
|
-
requireAll: [
|
|
20322
|
+
shouldScan: (file) => isProductionSourcePath(file.relativePath) && MCP_IMPORT_PREFILTER_PATTERN.test(file.content) && MCP_TOOL_SURFACE_PREFILTER_PATTERN.test(file.content) && AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN.test(file.content),
|
|
20323
|
+
pattern: MCP_TOOL_SURFACE_PATTERN,
|
|
20324
|
+
requireAll: [MCP_IMPORT_PATTERN, AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN],
|
|
19649
20325
|
ignoreStringLiterals: true,
|
|
19650
20326
|
message: "An MCP tool/resource/prompt handler appears to expose file, shell, network, or code-execution capability."
|
|
19651
20327
|
})
|
|
@@ -19857,11 +20533,24 @@ const hasDirective = (programNode, directive) => {
|
|
|
19857
20533
|
return Boolean(programNode.body?.some((statement) => isNodeOfType(statement, "ExpressionStatement") && isNodeOfType(statement.expression, "Literal") && statement.expression.value === directive));
|
|
19858
20534
|
};
|
|
19859
20535
|
//#endregion
|
|
19860
|
-
//#region src/plugin/utils/
|
|
19861
|
-
const
|
|
19862
|
-
|
|
19863
|
-
|
|
19864
|
-
|
|
20536
|
+
//#region src/plugin/utils/unwrap-object-integrity-expression.ts
|
|
20537
|
+
const OBJECT_INTEGRITY_METHOD_NAMES = new Set([
|
|
20538
|
+
"freeze",
|
|
20539
|
+
"seal",
|
|
20540
|
+
"preventExtensions"
|
|
20541
|
+
]);
|
|
20542
|
+
const OBJECT_FREEZE_OR_SEAL_METHOD_NAMES = new Set(["freeze", "seal"]);
|
|
20543
|
+
const unwrapObjectIntegrityExpression = (node, scopes, methodNames = OBJECT_INTEGRITY_METHOD_NAMES) => {
|
|
20544
|
+
let expression = stripParenExpression(node);
|
|
20545
|
+
while (isNodeOfType(expression, "CallExpression")) {
|
|
20546
|
+
const callee = stripParenExpression(expression.callee);
|
|
20547
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.object, "Identifier") || callee.object.name !== "Object" || !scopes.isGlobalReference(callee.object) || !isNodeOfType(callee.property, "Identifier") || !methodNames.has(callee.property.name)) break;
|
|
20548
|
+
const wrappedExpression = expression.arguments[0];
|
|
20549
|
+
if (!wrappedExpression || isNodeOfType(wrappedExpression, "SpreadElement")) break;
|
|
20550
|
+
expression = stripParenExpression(wrappedExpression);
|
|
20551
|
+
}
|
|
20552
|
+
return expression;
|
|
20553
|
+
};
|
|
19865
20554
|
//#endregion
|
|
19866
20555
|
//#region src/plugin/rules/nextjs/nextjs-async-client-component.ts
|
|
19867
20556
|
const nextjsAsyncClientComponent = defineRule({
|
|
@@ -19887,10 +20576,10 @@ const nextjsAsyncClientComponent = defineRule({
|
|
|
19887
20576
|
},
|
|
19888
20577
|
VariableDeclarator(node) {
|
|
19889
20578
|
if (!fileHasUseClient) return;
|
|
19890
|
-
if (!isComponentAssignment(node)) return;
|
|
19891
|
-
if (!isInlineFunctionExpression(node.init)) return;
|
|
19892
|
-
if (!node.init.async) return;
|
|
19893
20579
|
if (!isNodeOfType(node.id, "Identifier")) return;
|
|
20580
|
+
if (!isUppercaseName(node.id.name) || !node.init) return;
|
|
20581
|
+
const componentFunction = unwrapObjectIntegrityExpression(node.init, context.scopes, OBJECT_FREEZE_OR_SEAL_METHOD_NAMES);
|
|
20582
|
+
if (!isInlineFunctionExpression(componentFunction) || !componentFunction.async) return;
|
|
19894
20583
|
context.report({
|
|
19895
20584
|
node,
|
|
19896
20585
|
message: `Async client component "${node.id.name}" fails to render because client components can't be async.`
|
|
@@ -20192,7 +20881,7 @@ const nextjsNoAElement = defineRule({
|
|
|
20192
20881
|
severity: "warn",
|
|
20193
20882
|
recommendation: "`import Link from 'next/link'` for client-side navigation, prefetching, and preserved scroll position",
|
|
20194
20883
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
20195
|
-
if (
|
|
20884
|
+
if (resolveJsxElementType(node) !== "a") return;
|
|
20196
20885
|
const attributes = node.attributes ?? [];
|
|
20197
20886
|
const downloadAttribute = findJsxAttribute(attributes, "download");
|
|
20198
20887
|
if (downloadAttribute) {
|
|
@@ -20388,7 +21077,7 @@ const nextjsNoCssLink = defineRule({
|
|
|
20388
21077
|
severity: "warn",
|
|
20389
21078
|
recommendation: "Import CSS directly or use CSS Modules so Next.js can bundle, order, and optimize the stylesheet.",
|
|
20390
21079
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
20391
|
-
if (
|
|
21080
|
+
if (resolveJsxElementType(node) !== "link") return;
|
|
20392
21081
|
const attributes = node.attributes ?? [];
|
|
20393
21082
|
const relAttribute = findJsxAttribute(attributes, "rel");
|
|
20394
21083
|
if (!relAttribute?.value) return;
|
|
@@ -20496,7 +21185,7 @@ const nextjsNoFontLink = defineRule({
|
|
|
20496
21185
|
severity: "warn",
|
|
20497
21186
|
recommendation: "`import { Inter } from \"next/font/google\"` for self-hosting, zero layout shift, and no render-blocking requests",
|
|
20498
21187
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
20499
|
-
if (
|
|
21188
|
+
if (resolveJsxElementType(node) !== "link") return;
|
|
20500
21189
|
const attributes = node.attributes ?? [];
|
|
20501
21190
|
const hrefAttribute = findJsxAttribute(attributes, "href");
|
|
20502
21191
|
if (!hrefAttribute?.value) return;
|
|
@@ -20671,7 +21360,7 @@ const nextjsNoImgElement = defineRule({
|
|
|
20671
21360
|
create: (context) => {
|
|
20672
21361
|
if (isGeneratedImageRenderContext(context)) return {};
|
|
20673
21362
|
return { JSXOpeningElement(node) {
|
|
20674
|
-
if (
|
|
21363
|
+
if (resolveJsxElementType(node) !== "img") return;
|
|
20675
21364
|
if (isGeneratedImageRenderContext(context, node)) return;
|
|
20676
21365
|
const programRoot = findProgramRoot(node);
|
|
20677
21366
|
if (programRoot && hasEmailTemplateImport(programRoot)) return;
|
|
@@ -20718,8 +21407,8 @@ const nextjsNoPolyfillScript = defineRule({
|
|
|
20718
21407
|
severity: "warn",
|
|
20719
21408
|
recommendation: "Next.js includes polyfills for fetch, Promise, Object.assign, Array.from, and 50+ others automatically",
|
|
20720
21409
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
20721
|
-
|
|
20722
|
-
if (
|
|
21410
|
+
const elementName = resolveJsxElementType(node);
|
|
21411
|
+
if (elementName !== "script" && elementName !== "Script") return;
|
|
20723
21412
|
const srcAttribute = findJsxAttribute(node.attributes ?? [], "src");
|
|
20724
21413
|
if (!srcAttribute?.value) return;
|
|
20725
21414
|
const srcValue = isNodeOfType(srcAttribute.value, "Literal") ? srcAttribute.value.value : null;
|
|
@@ -20756,8 +21445,8 @@ const catchClauseRethrowsCaught = (handler) => {
|
|
|
20756
21445
|
return didRethrow;
|
|
20757
21446
|
};
|
|
20758
21447
|
//#endregion
|
|
20759
|
-
//#region src/plugin/utils/
|
|
20760
|
-
const
|
|
21448
|
+
//#region src/plugin/utils/is-immediately-invoked-function.ts
|
|
21449
|
+
const isImmediatelyInvokedFunction = (functionNode) => {
|
|
20761
21450
|
let wrappedCallee = functionNode;
|
|
20762
21451
|
let enclosing = functionNode.parent;
|
|
20763
21452
|
while (enclosing && stripParenExpression(enclosing) === functionNode) {
|
|
@@ -20766,11 +21455,13 @@ const isImmediatelyInvokedFunctionCallee = (functionNode) => {
|
|
|
20766
21455
|
}
|
|
20767
21456
|
return Boolean(enclosing && isNodeOfType(enclosing, "CallExpression") && enclosing.callee === wrappedCallee);
|
|
20768
21457
|
};
|
|
21458
|
+
//#endregion
|
|
21459
|
+
//#region src/plugin/utils/find-guarding-try-statement.ts
|
|
20769
21460
|
const findGuardingTryStatement = (node) => {
|
|
20770
21461
|
let child = node;
|
|
20771
21462
|
let ancestor = node.parent;
|
|
20772
21463
|
while (ancestor) {
|
|
20773
|
-
if (isFunctionLike$1(ancestor) && !
|
|
21464
|
+
if (isFunctionLike$1(ancestor) && !isImmediatelyInvokedFunction(ancestor)) return null;
|
|
20774
21465
|
if (isNodeOfType(ancestor, "TryStatement") && ancestor.block === child && ancestor.handler && !catchClauseRethrowsCaught(ancestor.handler)) return ancestor;
|
|
20775
21466
|
child = ancestor;
|
|
20776
21467
|
ancestor = ancestor.parent ?? null;
|
|
@@ -21322,7 +22013,7 @@ const FILENAME_TO_LANG = {
|
|
|
21322
22013
|
const resolveLang = (filename) => {
|
|
21323
22014
|
return FILENAME_TO_LANG[path.extname(filename).toLowerCase()] ?? "tsx";
|
|
21324
22015
|
};
|
|
21325
|
-
const parseSourceText = (filename, sourceText) => {
|
|
22016
|
+
const parseSourceText = ({ filename, sourceText, shouldAttachParentReferences = true }) => {
|
|
21326
22017
|
try {
|
|
21327
22018
|
const result = parseSync(filename, sourceText, {
|
|
21328
22019
|
astType: "ts",
|
|
@@ -21330,7 +22021,7 @@ const parseSourceText = (filename, sourceText) => {
|
|
|
21330
22021
|
});
|
|
21331
22022
|
if (result.errors.some((parseError) => parseError.severity === "Error")) return null;
|
|
21332
22023
|
const parsedProgram = result.program;
|
|
21333
|
-
attachParentReferences(parsedProgram);
|
|
22024
|
+
if (shouldAttachParentReferences) attachParentReferences(parsedProgram);
|
|
21334
22025
|
return parsedProgram;
|
|
21335
22026
|
} catch {
|
|
21336
22027
|
return null;
|
|
@@ -21368,7 +22059,10 @@ const parseSourceFile = (absoluteFilePath) => {
|
|
|
21368
22059
|
});
|
|
21369
22060
|
return null;
|
|
21370
22061
|
}
|
|
21371
|
-
const parsedProgram = parseSourceText(
|
|
22062
|
+
const parsedProgram = parseSourceText({
|
|
22063
|
+
filename: absoluteFilePath,
|
|
22064
|
+
sourceText
|
|
22065
|
+
});
|
|
21372
22066
|
parseCache.set(absoluteFilePath, {
|
|
21373
22067
|
mtimeMs: fileStat.mtimeMs,
|
|
21374
22068
|
size: fileStat.size,
|
|
@@ -21900,6 +22594,8 @@ const DOM_QUERY_MEMBER_NAMES = new Set([
|
|
|
21900
22594
|
]);
|
|
21901
22595
|
const LAYOUT_MEASUREMENT_MEMBER_NAMES = new Set([
|
|
21902
22596
|
"current",
|
|
22597
|
+
"textContent",
|
|
22598
|
+
"innerText",
|
|
21903
22599
|
"scrollWidth",
|
|
21904
22600
|
"clientWidth",
|
|
21905
22601
|
"offsetWidth",
|
|
@@ -21921,7 +22617,7 @@ const POST_MOUNT_GLOBAL_NAMES = new Set([
|
|
|
21921
22617
|
"navigator"
|
|
21922
22618
|
]);
|
|
21923
22619
|
const REF_FACTORY_CALLEE_NAMES = new Set(["useRef", "createRef"]);
|
|
21924
|
-
const hasRefLikeName = (name) => name === "ref" || name.endsWith("Ref") || name.endsWith("ref");
|
|
22620
|
+
const hasRefLikeName = (name) => name === "ref" || name.endsWith("Ref") || name.endsWith("ref") || name.endsWith("Node") || name.endsWith("node") || name.endsWith("Element") || name.endsWith("element");
|
|
21925
22621
|
const isRefFactoryInitializer = (init) => {
|
|
21926
22622
|
if (!init || !isNodeOfType(init, "CallExpression")) return false;
|
|
21927
22623
|
const callee = init.callee;
|
|
@@ -22004,88 +22700,37 @@ const readsPostMountValue = (root) => {
|
|
|
22004
22700
|
return found;
|
|
22005
22701
|
};
|
|
22006
22702
|
//#endregion
|
|
22007
|
-
//#region src/plugin/rules/state-and-effects/utils/effect/
|
|
22008
|
-
const
|
|
22009
|
-
TSAsExpression: ["expression", "typeAnnotation"],
|
|
22010
|
-
TSNonNullExpression: ["expression"],
|
|
22011
|
-
TSSatisfiesExpression: ["expression", "typeAnnotation"],
|
|
22012
|
-
TSTypeAssertion: ["typeAnnotation", "expression"],
|
|
22013
|
-
TSInstantiationExpression: ["expression", "typeArguments"]
|
|
22014
|
-
};
|
|
22015
|
-
const VISITOR_KEYS = {
|
|
22016
|
-
...eslintVisitorKeys.KEYS,
|
|
22017
|
-
...TYPESCRIPT_VISITOR_KEYS
|
|
22018
|
-
};
|
|
22703
|
+
//#region src/plugin/rules/state-and-effects/utils/effect/get-ast-child-keys.ts
|
|
22704
|
+
const getAstChildKeys = (node) => visitorKeys[node.type] ?? Object.keys(node).filter((key) => key !== "parent");
|
|
22019
22705
|
//#endregion
|
|
22020
22706
|
//#region src/plugin/rules/state-and-effects/utils/effect/get-program-analysis.ts
|
|
22021
22707
|
const programToAnalysis = /* @__PURE__ */ new WeakMap();
|
|
22022
|
-
const stripAndRecordParents = (root) => {
|
|
22023
|
-
const restorations = [];
|
|
22024
|
-
const seen = /* @__PURE__ */ new WeakSet();
|
|
22025
|
-
const visit = (value) => {
|
|
22026
|
-
if (!value || typeof value !== "object") return;
|
|
22027
|
-
if (seen.has(value)) return;
|
|
22028
|
-
seen.add(value);
|
|
22029
|
-
if (Array.isArray(value)) {
|
|
22030
|
-
for (const item of value) visit(item);
|
|
22031
|
-
return;
|
|
22032
|
-
}
|
|
22033
|
-
const record = value;
|
|
22034
|
-
if (!("type" in record)) return;
|
|
22035
|
-
if ("parent" in record) {
|
|
22036
|
-
restorations.push({
|
|
22037
|
-
node: record,
|
|
22038
|
-
originalParent: record.parent
|
|
22039
|
-
});
|
|
22040
|
-
record.parent = null;
|
|
22041
|
-
}
|
|
22042
|
-
for (const key of Object.keys(record)) {
|
|
22043
|
-
if (key === "parent") continue;
|
|
22044
|
-
visit(record[key]);
|
|
22045
|
-
}
|
|
22046
|
-
};
|
|
22047
|
-
visit(root);
|
|
22048
|
-
return restorations;
|
|
22049
|
-
};
|
|
22050
|
-
const restoreParents = (restorations) => {
|
|
22051
|
-
for (const restoration of restorations) restoration.node.parent = restoration.originalParent;
|
|
22052
|
-
};
|
|
22053
22708
|
const getProgramAnalysis = (anyNode) => {
|
|
22054
22709
|
const programNode = findProgramRoot(anyNode);
|
|
22055
22710
|
if (!programNode) return null;
|
|
22056
22711
|
const cached = programToAnalysis.get(programNode);
|
|
22057
22712
|
if (cached) return cached;
|
|
22058
|
-
const restorations = stripAndRecordParents(programNode);
|
|
22059
|
-
let scopeManager;
|
|
22060
|
-
try {
|
|
22061
|
-
scopeManager = analyze(programNode, {
|
|
22062
|
-
ecmaVersion: 2024,
|
|
22063
|
-
sourceType: "module",
|
|
22064
|
-
childVisitorKeys: VISITOR_KEYS,
|
|
22065
|
-
fallback: "iteration"
|
|
22066
|
-
});
|
|
22067
|
-
} finally {
|
|
22068
|
-
restoreParents(restorations);
|
|
22069
|
-
}
|
|
22070
22713
|
const analysis = {
|
|
22071
22714
|
programNode,
|
|
22072
|
-
scopeManager
|
|
22715
|
+
scopeManager: analyze(programNode, {
|
|
22716
|
+
ecmaVersion: 2024,
|
|
22717
|
+
sourceType: "module",
|
|
22718
|
+
childVisitorKeys: RUNTIME_VISITOR_KEYS,
|
|
22719
|
+
fallback: getAstChildKeys
|
|
22720
|
+
}),
|
|
22721
|
+
scopeByNode: /* @__PURE__ */ new WeakMap(),
|
|
22722
|
+
referenceByIdentifier: /* @__PURE__ */ new WeakMap()
|
|
22073
22723
|
};
|
|
22074
22724
|
programToAnalysis.set(programNode, analysis);
|
|
22075
22725
|
return analysis;
|
|
22076
22726
|
};
|
|
22077
|
-
const
|
|
22078
|
-
const getScopeForNode = (node, manager) => {
|
|
22727
|
+
const getScopeForNode = (node, analysis) => {
|
|
22079
22728
|
if (!node.range) return null;
|
|
22080
|
-
|
|
22081
|
-
if (!scopeByNode) {
|
|
22082
|
-
scopeByNode = /* @__PURE__ */ new WeakMap();
|
|
22083
|
-
scopeByNodeCache.set(manager, scopeByNode);
|
|
22084
|
-
}
|
|
22729
|
+
const scopeByNode = analysis.scopeByNode;
|
|
22085
22730
|
if (scopeByNode.has(node)) return scopeByNode.get(node) ?? null;
|
|
22086
22731
|
let bestScope = null;
|
|
22087
22732
|
let bestSize = Infinity;
|
|
22088
|
-
for (const scope of
|
|
22733
|
+
for (const scope of analysis.scopeManager.scopes) {
|
|
22089
22734
|
const block = scope.block;
|
|
22090
22735
|
if (!block?.range) continue;
|
|
22091
22736
|
if (node.range[0] < block.range[0] || node.range[1] > block.range[1]) continue;
|
|
@@ -22100,7 +22745,6 @@ const getScopeForNode = (node, manager) => {
|
|
|
22100
22745
|
};
|
|
22101
22746
|
//#endregion
|
|
22102
22747
|
//#region src/plugin/rules/state-and-effects/utils/effect/ast.ts
|
|
22103
|
-
const getChildKeys = (node) => VISITOR_KEYS[node.type] ?? Object.keys(node).filter((key) => key !== "parent");
|
|
22104
22748
|
const HOOK_NAME_PATTERN$2 = /^use[A-Z0-9]/;
|
|
22105
22749
|
const isInsideCallbackArgumentOf = (identifier, initializer) => {
|
|
22106
22750
|
if (!isNodeOfType(initializer, "CallExpression") && !isNodeOfType(initializer, "NewExpression")) return false;
|
|
@@ -22136,7 +22780,7 @@ const descend = (node, visit, visited = /* @__PURE__ */ new Set()) => {
|
|
|
22136
22780
|
if (visited.has(node)) return;
|
|
22137
22781
|
visit(node);
|
|
22138
22782
|
visited.add(node);
|
|
22139
|
-
const keys =
|
|
22783
|
+
const keys = getAstChildKeys(node);
|
|
22140
22784
|
const record = node;
|
|
22141
22785
|
for (const key of keys) {
|
|
22142
22786
|
const child = record[key];
|
|
@@ -22169,16 +22813,11 @@ const findDownstreamNodes = (topNode, type) => {
|
|
|
22169
22813
|
});
|
|
22170
22814
|
return nodes;
|
|
22171
22815
|
};
|
|
22172
|
-
const refByIdentifierCache = /* @__PURE__ */ new WeakMap();
|
|
22173
22816
|
const getRef = (analysis, identifier) => {
|
|
22174
|
-
|
|
22175
|
-
if (!refByIdentifier) {
|
|
22176
|
-
refByIdentifier = /* @__PURE__ */ new WeakMap();
|
|
22177
|
-
refByIdentifierCache.set(analysis, refByIdentifier);
|
|
22178
|
-
}
|
|
22817
|
+
const refByIdentifier = analysis.referenceByIdentifier;
|
|
22179
22818
|
if (refByIdentifier.has(identifier)) return refByIdentifier.get(identifier) ?? null;
|
|
22180
22819
|
let resolvedReference = null;
|
|
22181
|
-
const scope = getScopeForNode(identifier, analysis
|
|
22820
|
+
const scope = getScopeForNode(identifier, analysis);
|
|
22182
22821
|
if (scope) {
|
|
22183
22822
|
for (const reference of scope.references) if (reference.identifier === identifier) {
|
|
22184
22823
|
resolvedReference = reference;
|
|
@@ -22426,7 +23065,7 @@ const isReactFunctionalHOC = (analysis, node) => {
|
|
|
22426
23065
|
const isWrappedSeparately = () => {
|
|
22427
23066
|
if (!isNodeOfType(node.id, "Identifier")) return false;
|
|
22428
23067
|
const bindingName = node.id.name;
|
|
22429
|
-
const containingScope = getScopeForNode(node, analysis
|
|
23068
|
+
const containingScope = getScopeForNode(node, analysis);
|
|
22430
23069
|
if (!containingScope) return false;
|
|
22431
23070
|
const variable = containingScope.variables.find((v) => v.name === bindingName);
|
|
22432
23071
|
if (!variable) return false;
|
|
@@ -22577,10 +23216,15 @@ const isProp = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
|
22577
23216
|
if (!declaringNode) return false;
|
|
22578
23217
|
return isReactFunctionalComponent(declaringNode) && !isReactFunctionalHOC(analysis, declaringNode) || isCustomHook(declaringNode);
|
|
22579
23218
|
}));
|
|
23219
|
+
const isWholePropsParameterBinding = (bindingNode) => {
|
|
23220
|
+
if (isFunctionLike$1(bindingNode.parent)) return true;
|
|
23221
|
+
let declaringFunction = bindingNode.parent;
|
|
23222
|
+
while (declaringFunction && !isFunctionLike$1(declaringFunction)) declaringFunction = declaringFunction.parent;
|
|
23223
|
+
return Boolean(declaringFunction && resolveFirstArgumentBinding(declaringFunction.params?.[0]) === bindingNode);
|
|
23224
|
+
};
|
|
22580
23225
|
const isWholePropsObjectReference = (analysis, ref) => isProp(analysis, ref) && Boolean(ref.resolved?.defs.some((def) => {
|
|
22581
23226
|
if (def.type !== "Parameter") return false;
|
|
22582
|
-
|
|
22583
|
-
return isFunctionLike$1(bindingParent);
|
|
23227
|
+
return isWholePropsParameterBinding(def.name);
|
|
22584
23228
|
}));
|
|
22585
23229
|
const isIdentifierOrMemberExpression = (node) => isNodeOfType(node, "Identifier") || isNodeOfType(node, "MemberExpression");
|
|
22586
23230
|
const isPropAlias = (analysis, ref) => {
|
|
@@ -22657,11 +23301,14 @@ const hasCleanupReturn = (analysis, node, visited = /* @__PURE__ */ new WeakSet(
|
|
|
22657
23301
|
if (isNodeOfType(node, "ReturnStatement") && node.argument != null) return isCleanupReturnArgument(analysis, node.argument);
|
|
22658
23302
|
if (!isNodeOfType(node, "BlockStatement") && isFunctionLike$1(node)) return false;
|
|
22659
23303
|
const record = node;
|
|
22660
|
-
|
|
22661
|
-
|
|
22662
|
-
|
|
22663
|
-
|
|
22664
|
-
|
|
23304
|
+
const childKeys = getAstChildKeys(node);
|
|
23305
|
+
for (let keyIndex = 0; keyIndex < childKeys.length; keyIndex += 1) {
|
|
23306
|
+
const value = record[childKeys[keyIndex]];
|
|
23307
|
+
if (Array.isArray(value)) for (let itemIndex = 0; itemIndex < value.length; itemIndex += 1) {
|
|
23308
|
+
const item = value[itemIndex];
|
|
23309
|
+
if (isAstNode(item) && hasCleanupReturn(analysis, item, visited)) return true;
|
|
23310
|
+
}
|
|
23311
|
+
else if (isAstNode(value) && hasCleanupReturn(analysis, value, visited)) return true;
|
|
22665
23312
|
}
|
|
22666
23313
|
return false;
|
|
22667
23314
|
};
|
|
@@ -22889,61 +23536,73 @@ const PURE_GLOBAL_CALLEE_NAMES = new Set([
|
|
|
22889
23536
|
"Array",
|
|
22890
23537
|
"BigInt",
|
|
22891
23538
|
"Boolean",
|
|
23539
|
+
"encodeURIComponent",
|
|
22892
23540
|
"Number",
|
|
22893
23541
|
"Object",
|
|
22894
23542
|
"String",
|
|
22895
23543
|
"parseFloat",
|
|
22896
|
-
"parseInt"
|
|
23544
|
+
"parseInt",
|
|
23545
|
+
"structuredClone"
|
|
23546
|
+
]);
|
|
23547
|
+
const PURE_GLOBAL_CONSTRUCTOR_NAMES = new Set(["Date", "Set"]);
|
|
23548
|
+
const PURE_HELPER_NAMESPACE_MEMBER_NAMES = new Map([
|
|
23549
|
+
["Array", new Set(["from"])],
|
|
23550
|
+
["JSON", new Set([
|
|
23551
|
+
"isRawJSON",
|
|
23552
|
+
"parse",
|
|
23553
|
+
"rawJSON",
|
|
23554
|
+
"stringify"
|
|
23555
|
+
])],
|
|
23556
|
+
["Math", new Set([
|
|
23557
|
+
"abs",
|
|
23558
|
+
"acos",
|
|
23559
|
+
"acosh",
|
|
23560
|
+
"asin",
|
|
23561
|
+
"asinh",
|
|
23562
|
+
"atan",
|
|
23563
|
+
"atan2",
|
|
23564
|
+
"atanh",
|
|
23565
|
+
"cbrt",
|
|
23566
|
+
"ceil",
|
|
23567
|
+
"clz32",
|
|
23568
|
+
"cos",
|
|
23569
|
+
"cosh",
|
|
23570
|
+
"exp",
|
|
23571
|
+
"floor",
|
|
23572
|
+
"fround",
|
|
23573
|
+
"hypot",
|
|
23574
|
+
"imul",
|
|
23575
|
+
"log",
|
|
23576
|
+
"log10",
|
|
23577
|
+
"log1p",
|
|
23578
|
+
"log2",
|
|
23579
|
+
"max",
|
|
23580
|
+
"min",
|
|
23581
|
+
"pow",
|
|
23582
|
+
"round",
|
|
23583
|
+
"sign",
|
|
23584
|
+
"sin",
|
|
23585
|
+
"sinh",
|
|
23586
|
+
"sqrt",
|
|
23587
|
+
"tan",
|
|
23588
|
+
"tanh",
|
|
23589
|
+
"trunc"
|
|
23590
|
+
])],
|
|
23591
|
+
["Object", new Set(["assign"])]
|
|
22897
23592
|
]);
|
|
22898
|
-
const PURE_GLOBAL_NAMESPACE_NAMES = new Set(["JSON", "Math"]);
|
|
22899
|
-
const PURE_HELPER_NAMESPACE_MEMBER_NAMES = new Map([["JSON", new Set([
|
|
22900
|
-
"isRawJSON",
|
|
22901
|
-
"parse",
|
|
22902
|
-
"rawJSON",
|
|
22903
|
-
"stringify"
|
|
22904
|
-
])], ["Math", new Set([
|
|
22905
|
-
"abs",
|
|
22906
|
-
"acos",
|
|
22907
|
-
"acosh",
|
|
22908
|
-
"asin",
|
|
22909
|
-
"asinh",
|
|
22910
|
-
"atan",
|
|
22911
|
-
"atan2",
|
|
22912
|
-
"atanh",
|
|
22913
|
-
"cbrt",
|
|
22914
|
-
"ceil",
|
|
22915
|
-
"clz32",
|
|
22916
|
-
"cos",
|
|
22917
|
-
"cosh",
|
|
22918
|
-
"exp",
|
|
22919
|
-
"floor",
|
|
22920
|
-
"fround",
|
|
22921
|
-
"hypot",
|
|
22922
|
-
"imul",
|
|
22923
|
-
"log",
|
|
22924
|
-
"log10",
|
|
22925
|
-
"log1p",
|
|
22926
|
-
"log2",
|
|
22927
|
-
"max",
|
|
22928
|
-
"min",
|
|
22929
|
-
"pow",
|
|
22930
|
-
"round",
|
|
22931
|
-
"sign",
|
|
22932
|
-
"sin",
|
|
22933
|
-
"sinh",
|
|
22934
|
-
"sqrt",
|
|
22935
|
-
"tan",
|
|
22936
|
-
"tanh",
|
|
22937
|
-
"trunc"
|
|
22938
|
-
])]]);
|
|
22939
23593
|
const PURE_MEMBER_TRANSFORM_NAMES = new Set([
|
|
22940
23594
|
"concat",
|
|
22941
23595
|
"filter",
|
|
23596
|
+
"flatMap",
|
|
22942
23597
|
"join",
|
|
22943
23598
|
"map",
|
|
23599
|
+
"reduce",
|
|
23600
|
+
"replace",
|
|
23601
|
+
"slice",
|
|
22944
23602
|
"split",
|
|
22945
23603
|
"toLowerCase",
|
|
22946
23604
|
"toString",
|
|
23605
|
+
"toSorted",
|
|
22947
23606
|
"toUpperCase",
|
|
22948
23607
|
"trim"
|
|
22949
23608
|
]);
|
|
@@ -22952,7 +23611,7 @@ const STORAGE_GLOBAL_NAMES$1 = new Set([
|
|
|
22952
23611
|
"localStorage",
|
|
22953
23612
|
"sessionStorage"
|
|
22954
23613
|
]);
|
|
22955
|
-
const getStaticMemberName = (node) => {
|
|
23614
|
+
const getStaticMemberName$1 = (node) => {
|
|
22956
23615
|
if (!isNodeOfType(node, "MemberExpression")) return null;
|
|
22957
23616
|
if (!node.computed && isNodeOfType(node.property, "Identifier")) return node.property.name;
|
|
22958
23617
|
if (node.computed && isNodeOfType(node.property, "Literal")) return typeof node.property.value === "string" ? node.property.value : null;
|
|
@@ -22967,7 +23626,7 @@ const getCallCalleeName$1 = (callExpression) => {
|
|
|
22967
23626
|
if (!isNodeOfType(callExpression, "CallExpression")) return null;
|
|
22968
23627
|
const callee = stripParenExpression(callExpression.callee);
|
|
22969
23628
|
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
22970
|
-
return getStaticMemberName(callee);
|
|
23629
|
+
return getStaticMemberName$1(callee);
|
|
22971
23630
|
};
|
|
22972
23631
|
const getFunctionParameters = (functionNode) => isFunctionLike$1(functionNode) ? functionNode.params ?? [] : [];
|
|
22973
23632
|
const getIdentifierBindingIdentity = (analysis, identifier) => {
|
|
@@ -23083,14 +23742,14 @@ const localUseEventPreservesCallback = (analysis, callee) => {
|
|
|
23083
23742
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
23084
23743
|
const forwardedCallee = stripParenExpression(child.callee);
|
|
23085
23744
|
if (!isNodeOfType(forwardedCallee, "MemberExpression")) return;
|
|
23086
|
-
if (getStaticMemberName(forwardedCallee) !== "current") return;
|
|
23745
|
+
if (getStaticMemberName$1(forwardedCallee) !== "current") return;
|
|
23087
23746
|
if (!isNodeOfType(forwardedCallee.object, "Identifier")) return;
|
|
23088
23747
|
const refReference = getRef(analysis, forwardedCallee.object);
|
|
23089
23748
|
if (!callbackRefDeclarators.find((declarator) => refReference?.resolved?.defs.some((definition) => definition.node === declarator)) || !refReference?.resolved) return;
|
|
23090
23749
|
if (!refReference.resolved.references.some((candidateReference) => {
|
|
23091
23750
|
const memberExpression = candidateReference.identifier.parent;
|
|
23092
23751
|
const assignmentExpression = memberExpression?.parent;
|
|
23093
|
-
if (!memberExpression || !isNodeOfType(memberExpression, "MemberExpression") || getStaticMemberName(memberExpression) !== "current" || !assignmentExpression || !isNodeOfType(assignmentExpression, "AssignmentExpression") || assignmentExpression.left !== memberExpression) return false;
|
|
23752
|
+
if (!memberExpression || !isNodeOfType(memberExpression, "MemberExpression") || getStaticMemberName$1(memberExpression) !== "current" || !assignmentExpression || !isNodeOfType(assignmentExpression, "AssignmentExpression") || assignmentExpression.left !== memberExpression) return false;
|
|
23094
23753
|
return getIdentifierBindingIdentity(analysis, assignmentExpression.right) !== callbackBinding;
|
|
23095
23754
|
})) forwardsCallback = true;
|
|
23096
23755
|
});
|
|
@@ -23115,7 +23774,7 @@ const resolveWrappedCallable = (analysis, node) => {
|
|
|
23115
23774
|
return callback && isFunctionLike$1(callback) ? callback : null;
|
|
23116
23775
|
}
|
|
23117
23776
|
if (!isNodeOfType(candidate, "MemberExpression")) return null;
|
|
23118
|
-
if (getStaticMemberName(candidate) !== "current") return null;
|
|
23777
|
+
if (getStaticMemberName$1(candidate) !== "current") return null;
|
|
23119
23778
|
if (!isNodeOfType(candidate.object, "Identifier")) return null;
|
|
23120
23779
|
const reference = getRef(analysis, candidate.object);
|
|
23121
23780
|
const declarator = reference?.resolved?.defs.map((definition) => definition.node).find((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
|
|
@@ -23127,7 +23786,7 @@ const resolveWrappedCallable = (analysis, node) => {
|
|
|
23127
23786
|
return Boolean(reference?.resolved?.references.some((candidateReference) => {
|
|
23128
23787
|
const member = candidateReference.identifier.parent;
|
|
23129
23788
|
const assignment = member?.parent;
|
|
23130
|
-
return Boolean(member && isNodeOfType(member, "MemberExpression") && getStaticMemberName(member) === "current" && assignment && isNodeOfType(assignment, "AssignmentExpression") && assignment.left === member);
|
|
23789
|
+
return Boolean(member && isNodeOfType(member, "MemberExpression") && getStaticMemberName$1(member) === "current" && assignment && isNodeOfType(assignment, "AssignmentExpression") && assignment.left === member);
|
|
23131
23790
|
})) ? null : initializer;
|
|
23132
23791
|
};
|
|
23133
23792
|
const functionInvokesItself = (analysis, functionNode) => {
|
|
@@ -23166,7 +23825,7 @@ const collectBoundedEffectExecutionFrames = (analysis, effectNode, currentFilena
|
|
|
23166
23825
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
23167
23826
|
const callee = stripParenExpression(child.callee);
|
|
23168
23827
|
const calleeName = getCallCalleeName$1(child);
|
|
23169
|
-
const memberName = getStaticMemberName(callee);
|
|
23828
|
+
const memberName = getStaticMemberName$1(callee);
|
|
23170
23829
|
const isDeferringCall = calleeName !== null && DEFERRING_CALLEE_NAMES.has(calleeName) || memberName !== null && DEFERRING_MEMBER_NAMES.has(memberName);
|
|
23171
23830
|
const isIteratorCall = memberName !== null && SYNCHRONOUS_ITERATOR_METHOD_NAMES$1.has(memberName) && isNodeOfType(callee, "MemberExpression");
|
|
23172
23831
|
const enqueueFrame = (callableNode, argumentsForCallable, isDeferred, introducedBindings, allowWithoutInvocationEvidence = false) => {
|
|
@@ -23325,14 +23984,19 @@ const analyzeHelperExpression = (expression, environment, usedParameterIndices)
|
|
|
23325
23984
|
const callbackSummary = analyzeHelperStatements(node.body.body ?? [], callbackEnvironment, usedParameterIndices, analyzeHelperExpression);
|
|
23326
23985
|
return callbackSummary.isValid && !callbackSummary.canContinue;
|
|
23327
23986
|
}
|
|
23987
|
+
if (isNodeOfType(node, "NewExpression")) {
|
|
23988
|
+
const callee = stripParenExpression(node.callee);
|
|
23989
|
+
if (!isNodeOfType(callee, "Identifier") || !PURE_GLOBAL_CONSTRUCTOR_NAMES.has(callee.name) || environment.parameterIndices.has(callee.name) || environment.recursiveNames.has(callee.name) || environment.shadowedGlobalNames.has(callee.name)) return false;
|
|
23990
|
+
return (node.arguments ?? []).every((argument) => analyzeHelperExpression(argument, environment, usedParameterIndices));
|
|
23991
|
+
}
|
|
23328
23992
|
if (isNodeOfType(node, "CallExpression")) {
|
|
23329
23993
|
const callee = stripParenExpression(node.callee);
|
|
23330
23994
|
const calleeRoot = getMemberRoot(callee);
|
|
23331
23995
|
const isPureGlobalCall = isNodeOfType(callee, "Identifier") && PURE_GLOBAL_CALLEE_NAMES.has(callee.name) && !environment.parameterIndices.has(callee.name) && !environment.recursiveNames.has(callee.name) && !environment.shadowedGlobalNames.has(callee.name);
|
|
23332
23996
|
const namespaceName = isNodeOfType(calleeRoot, "Identifier") && !environment.parameterIndices.has(calleeRoot.name) && !environment.recursiveNames.has(calleeRoot.name) && !environment.shadowedGlobalNames.has(calleeRoot.name) ? calleeRoot.name : null;
|
|
23333
|
-
const namespaceMemberName = getStaticMemberName(callee);
|
|
23997
|
+
const namespaceMemberName = getStaticMemberName$1(callee);
|
|
23334
23998
|
const isPureNamespaceCall = isNodeOfType(callee, "MemberExpression") && namespaceName !== null && namespaceMemberName !== null && PURE_HELPER_NAMESPACE_MEMBER_NAMES.get(namespaceName)?.has(namespaceMemberName) === true;
|
|
23335
|
-
const isPureMemberTransform = isNodeOfType(callee, "MemberExpression") && PURE_MEMBER_TRANSFORM_NAMES.has(getStaticMemberName(callee) ?? "");
|
|
23999
|
+
const isPureMemberTransform = isNodeOfType(callee, "MemberExpression") && PURE_MEMBER_TRANSFORM_NAMES.has(getStaticMemberName$1(callee) ?? "");
|
|
23336
24000
|
if (!isPureGlobalCall && !isPureNamespaceCall && !isPureMemberTransform) return false;
|
|
23337
24001
|
if (isPureMemberTransform && isNodeOfType(callee, "MemberExpression") && !analyzeHelperExpression(callee.object, environment, usedParameterIndices)) return false;
|
|
23338
24002
|
return (node.arguments ?? []).every((argument) => analyzeHelperExpression(argument, environment, usedParameterIndices));
|
|
@@ -23406,7 +24070,7 @@ const isLocallyConstructedObjectMember = (reference, memberExpression) => isNode
|
|
|
23406
24070
|
return isNodeOfType(definitionNode, "VariableDeclarator") && Boolean(definitionNode.init) && (isNodeOfType(definitionNode.init, "ObjectExpression") || isNodeOfType(definitionNode.init, "ArrayExpression"));
|
|
23407
24071
|
}) === true;
|
|
23408
24072
|
const getUseRefDeclarator = (analysis, memberExpression) => {
|
|
23409
|
-
if (!isNodeOfType(memberExpression, "MemberExpression") || getStaticMemberName(memberExpression) !== "current" || !isNodeOfType(memberExpression.object, "Identifier")) return null;
|
|
24073
|
+
if (!isNodeOfType(memberExpression, "MemberExpression") || getStaticMemberName$1(memberExpression) !== "current" || !isNodeOfType(memberExpression.object, "Identifier")) return null;
|
|
23410
24074
|
return getRef(analysis, memberExpression.object)?.resolved?.defs.map((definition) => definition.node).find((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator") && isNodeOfType(definitionNode.init, "CallExpression") && getCallCalleeName$1(definitionNode.init) === "useRef") ?? null;
|
|
23411
24075
|
};
|
|
23412
24076
|
const collectValueEvidence = (analysis, expression, frame, remainingCallFrames, visitedBindings = /* @__PURE__ */ new Set()) => {
|
|
@@ -23475,7 +24139,7 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
|
|
|
23475
24139
|
return collectValueEvidence(analysis, initializer.init, frame, remainingCallFrames, visitedBindings);
|
|
23476
24140
|
}
|
|
23477
24141
|
if (isNodeOfType(node, "MemberExpression")) {
|
|
23478
|
-
if (getStaticMemberName(node) === "current" && isNodeOfType(node.object, "Identifier")) {
|
|
24142
|
+
if (getStaticMemberName$1(node) === "current" && isNodeOfType(node.object, "Identifier")) {
|
|
23479
24143
|
const refBinding = getRef(analysis, node.object)?.resolved;
|
|
23480
24144
|
const refDeclarator = useRefDeclarator;
|
|
23481
24145
|
if (refBinding && refDeclarator && isNodeOfType(refDeclarator, "VariableDeclarator") && isNodeOfType(refDeclarator.init, "CallExpression")) {
|
|
@@ -23491,7 +24155,7 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
|
|
|
23491
24155
|
if (candidateReference.init) continue;
|
|
23492
24156
|
const identifier = candidateReference.identifier;
|
|
23493
24157
|
const member = identifier.parent;
|
|
23494
|
-
if (!member || !isNodeOfType(member, "MemberExpression") || member.object !== identifier || getStaticMemberName(member) !== "current") {
|
|
24158
|
+
if (!member || !isNodeOfType(member, "MemberExpression") || member.object !== identifier || getStaticMemberName$1(member) !== "current") {
|
|
23495
24159
|
evidence.hasUnknownSource = true;
|
|
23496
24160
|
continue;
|
|
23497
24161
|
}
|
|
@@ -23527,8 +24191,8 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
|
|
|
23527
24191
|
}
|
|
23528
24192
|
const callee = stripParenExpression(node.callee);
|
|
23529
24193
|
const calleeRoot = getMemberRoot(callee);
|
|
23530
|
-
const isPureGlobalCall = isNodeOfType(callee, "Identifier") && PURE_GLOBAL_CALLEE_NAMES.has(callee.name) && getIdentifierBindingIdentity(analysis, callee) === null || isNodeOfType(
|
|
23531
|
-
const isPureMemberTransform = isNodeOfType(callee, "MemberExpression") && PURE_MEMBER_TRANSFORM_NAMES.has(getStaticMemberName(callee) ?? "");
|
|
24194
|
+
const isPureGlobalCall = isNodeOfType(callee, "Identifier") && PURE_GLOBAL_CALLEE_NAMES.has(callee.name) && getIdentifierBindingIdentity(analysis, callee) === null || isNodeOfType(callee, "MemberExpression") && isNodeOfType(calleeRoot, "Identifier") && getIdentifierBindingIdentity(analysis, calleeRoot) === null && PURE_HELPER_NAMESPACE_MEMBER_NAMES.get(calleeRoot.name)?.has(getStaticMemberName$1(callee) ?? "") === true;
|
|
24195
|
+
const isPureMemberTransform = isNodeOfType(callee, "MemberExpression") && PURE_MEMBER_TRANSFORM_NAMES.has(getStaticMemberName$1(callee) ?? "");
|
|
23532
24196
|
if (isPureGlobalCall || isPureMemberTransform) {
|
|
23533
24197
|
if (isPureMemberTransform && isNodeOfType(callee, "MemberExpression")) mergeEvidence(evidence, collectValueEvidence(analysis, callee.object, frame, remainingCallFrames, new Set(visitedBindings)));
|
|
23534
24198
|
for (const argument of node.arguments ?? []) {
|
|
@@ -23580,6 +24244,15 @@ const collectValueEvidence = (analysis, expression, frame, remainingCallFrames,
|
|
|
23580
24244
|
}
|
|
23581
24245
|
return evidence;
|
|
23582
24246
|
}
|
|
24247
|
+
if (isNodeOfType(node, "NewExpression")) {
|
|
24248
|
+
const callee = stripParenExpression(node.callee);
|
|
24249
|
+
if (!isNodeOfType(callee, "Identifier") || !PURE_GLOBAL_CONSTRUCTOR_NAMES.has(callee.name) || getIdentifierBindingIdentity(analysis, callee) !== null) {
|
|
24250
|
+
evidence.hasUnknownSource = true;
|
|
24251
|
+
return evidence;
|
|
24252
|
+
}
|
|
24253
|
+
for (const argument of node.arguments ?? []) mergeEvidence(evidence, collectValueEvidence(analysis, argument, frame, remainingCallFrames, new Set(visitedBindings)));
|
|
24254
|
+
return evidence;
|
|
24255
|
+
}
|
|
23583
24256
|
if (isFunctionLike$1(node) || isNodeOfType(node, "AwaitExpression")) {
|
|
23584
24257
|
evidence.hasUnknownSource = true;
|
|
23585
24258
|
return evidence;
|
|
@@ -23998,6 +24671,26 @@ const STATEFUL_HTML_DESCENDANT_TAGS = new Set([
|
|
|
23998
24671
|
"dialog",
|
|
23999
24672
|
"canvas"
|
|
24000
24673
|
]);
|
|
24674
|
+
const STATEFUL_HTML_ATTRIBUTE_NAMES = new Set([
|
|
24675
|
+
"autofocus",
|
|
24676
|
+
"contenteditable",
|
|
24677
|
+
"draggable",
|
|
24678
|
+
"tabindex"
|
|
24679
|
+
]);
|
|
24680
|
+
const isStaticallyFalseAttributeValue = (attribute) => {
|
|
24681
|
+
if (!isNodeOfType(attribute, "JSXAttribute") || !attribute.value) return false;
|
|
24682
|
+
const value = isNodeOfType(attribute.value, "JSXExpressionContainer") ? attribute.value.expression : attribute.value;
|
|
24683
|
+
return isNodeOfType(value, "Literal") && (value.value === false || value.value === "false");
|
|
24684
|
+
};
|
|
24685
|
+
const hasStatefulHtmlAttribute = (openingElement) => {
|
|
24686
|
+
if (!isNodeOfType(openingElement, "JSXOpeningElement")) return false;
|
|
24687
|
+
return openingElement.attributes.some((attribute) => {
|
|
24688
|
+
if (!isNodeOfType(attribute, "JSXAttribute") || !isNodeOfType(attribute.name, "JSXIdentifier")) return false;
|
|
24689
|
+
const attributeName = attribute.name.name.toLowerCase();
|
|
24690
|
+
if (!STATEFUL_HTML_ATTRIBUTE_NAMES.has(attributeName)) return false;
|
|
24691
|
+
return attributeName === "tabindex" || !isStaticallyFalseAttributeValue(attribute);
|
|
24692
|
+
});
|
|
24693
|
+
};
|
|
24001
24694
|
const STATEFUL_DESCENDANT_SCAN_BUDGET = 200;
|
|
24002
24695
|
const rootIdentifierNameOf = (expression) => {
|
|
24003
24696
|
let object = expression;
|
|
@@ -24015,7 +24708,8 @@ const containsStatefulDescendant = (jsxElement, options = {}) => {
|
|
|
24015
24708
|
budget -= 1;
|
|
24016
24709
|
const node = stack.pop();
|
|
24017
24710
|
if (isNodeOfType(node, "JSXElement")) {
|
|
24018
|
-
const
|
|
24711
|
+
const opening = node.openingElement;
|
|
24712
|
+
const name = opening.name;
|
|
24019
24713
|
if (name && isNodeOfType(name, "JSXIdentifier")) {
|
|
24020
24714
|
const tagName = name.name;
|
|
24021
24715
|
const firstChar = tagName.charCodeAt(0);
|
|
@@ -24023,6 +24717,7 @@ const containsStatefulDescendant = (jsxElement, options = {}) => {
|
|
|
24023
24717
|
if (STATEFUL_HTML_DESCENDANT_TAGS.has(tagName)) return true;
|
|
24024
24718
|
}
|
|
24025
24719
|
if (name && isNodeOfType(name, "JSXMemberExpression")) return true;
|
|
24720
|
+
if (hasStatefulHtmlAttribute(opening)) return true;
|
|
24026
24721
|
const children = node.children ?? [];
|
|
24027
24722
|
for (const child of children) stack.push(child);
|
|
24028
24723
|
continue;
|
|
@@ -24116,6 +24811,7 @@ const ARITHMETIC_KEY_OPERATORS = new Set([
|
|
|
24116
24811
|
"/",
|
|
24117
24812
|
"%"
|
|
24118
24813
|
]);
|
|
24814
|
+
const bindingMutationResultsByProgram = /* @__PURE__ */ new WeakMap();
|
|
24119
24815
|
const extractCandidateIdentifiers = (expression) => {
|
|
24120
24816
|
const node = stripParenExpression(expression);
|
|
24121
24817
|
if (isNodeOfType(node, "Identifier")) return [node];
|
|
@@ -24156,6 +24852,13 @@ const isArrayFromCall = (node) => {
|
|
|
24156
24852
|
const isBindingReassignedOrMutated = (referenceNode, bindingName) => {
|
|
24157
24853
|
const programRoot = findProgramRoot(referenceNode);
|
|
24158
24854
|
if (!programRoot) return false;
|
|
24855
|
+
let mutationResultsByBindingName = bindingMutationResultsByProgram.get(programRoot);
|
|
24856
|
+
if (!mutationResultsByBindingName) {
|
|
24857
|
+
mutationResultsByBindingName = /* @__PURE__ */ new Map();
|
|
24858
|
+
bindingMutationResultsByProgram.set(programRoot, mutationResultsByBindingName);
|
|
24859
|
+
}
|
|
24860
|
+
const cachedResult = mutationResultsByBindingName.get(bindingName);
|
|
24861
|
+
if (cachedResult !== void 0) return cachedResult;
|
|
24159
24862
|
let didFindWrite = false;
|
|
24160
24863
|
walkAst(programRoot, (child) => {
|
|
24161
24864
|
if (didFindWrite) return false;
|
|
@@ -24172,6 +24875,7 @@ const isBindingReassignedOrMutated = (referenceNode, bindingName) => {
|
|
|
24172
24875
|
return false;
|
|
24173
24876
|
}
|
|
24174
24877
|
});
|
|
24878
|
+
mutationResultsByBindingName.set(bindingName, didFindWrite);
|
|
24175
24879
|
return didFindWrite;
|
|
24176
24880
|
};
|
|
24177
24881
|
/**
|
|
@@ -24573,6 +25277,14 @@ const templateHasOuterMemberIdentity = (template, bindingFunction) => {
|
|
|
24573
25277
|
}
|
|
24574
25278
|
return false;
|
|
24575
25279
|
};
|
|
25280
|
+
const findBareItemNamesReferencedByTemplate = (template, itemNames) => {
|
|
25281
|
+
const referencedItemNames = /* @__PURE__ */ new Set();
|
|
25282
|
+
for (const expression of template.expressions ?? []) {
|
|
25283
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
25284
|
+
if (isNodeOfType(unwrappedExpression, "Identifier") && itemNames.has(unwrappedExpression.name)) referencedItemNames.add(unwrappedExpression.name);
|
|
25285
|
+
}
|
|
25286
|
+
return referencedItemNames;
|
|
25287
|
+
};
|
|
24576
25288
|
const forLoopTestReadsDataLength = (test) => {
|
|
24577
25289
|
let didFindLengthRead = false;
|
|
24578
25290
|
walkAst(test, (child) => {
|
|
@@ -24709,9 +25421,11 @@ const noArrayIndexAsKey = defineRule({
|
|
|
24709
25421
|
} else if (STATELESS_HTML_LEAF_TAGS.has(elementName.name)) {
|
|
24710
25422
|
const jsxElement = openingElement.parent;
|
|
24711
25423
|
if (jsxElement && isNodeOfType(jsxElement, "JSXElement")) {
|
|
25424
|
+
const isInlineTextRun = INLINE_TEXT_LEAF_TAGS.has(elementName.name);
|
|
25425
|
+
const primitiveItemNames = keyTemplate ? findBareItemNamesReferencedByTemplate(keyTemplate, itemNames) : EMPTY_NAME_SET;
|
|
24712
25426
|
if (!containsStatefulDescendant(jsxElement, {
|
|
24713
|
-
memberRootNames:
|
|
24714
|
-
bareIdentifierNames: derivedNames
|
|
25427
|
+
memberRootNames: isInlineTextRun ? itemNames : EMPTY_NAME_SET,
|
|
25428
|
+
bareIdentifierNames: primitiveItemNames.size > 0 ? new Set([...derivedNames, ...primitiveItemNames]) : derivedNames
|
|
24715
25429
|
})) return;
|
|
24716
25430
|
}
|
|
24717
25431
|
}
|
|
@@ -24879,7 +25593,11 @@ const noAsyncEffectCallback = defineRule({
|
|
|
24879
25593
|
severity: "warn",
|
|
24880
25594
|
recommendation: "Don't make the effect callback `async`. Define an async function inside the effect and call it, then return a real cleanup function if you need one.",
|
|
24881
25595
|
create: (context) => ({ CallExpression(node) {
|
|
24882
|
-
if (!
|
|
25596
|
+
if (!isReactApiCall(node, EFFECT_HOOK_NAMES$1, context.scopes, {
|
|
25597
|
+
allowGlobalReactNamespace: true,
|
|
25598
|
+
allowUnboundBareCalls: true,
|
|
25599
|
+
resolveNamedAliases: true
|
|
25600
|
+
})) return;
|
|
24883
25601
|
const callback = getEffectCallback(node);
|
|
24884
25602
|
if (!callback) return;
|
|
24885
25603
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
|
|
@@ -25272,10 +25990,10 @@ const declaresAnyDependency = (manifest) => DEPENDENCY_SECTION_NAMES.some((secti
|
|
|
25272
25990
|
const section = manifest[sectionName];
|
|
25273
25991
|
return typeof section === "object" && section !== null && Object.keys(section).length > 0;
|
|
25274
25992
|
});
|
|
25275
|
-
const declaresDependency = (manifest, dependencyName) => {
|
|
25276
|
-
|
|
25277
|
-
return
|
|
25278
|
-
};
|
|
25993
|
+
const declaresDependency = (manifest, dependencyName) => DEPENDENCY_SECTION_NAMES.some((sectionName) => {
|
|
25994
|
+
const section = manifest[sectionName];
|
|
25995
|
+
return typeof section === "object" && section !== null && Object.prototype.propertyIsEnumerable.call(section, dependencyName);
|
|
25996
|
+
});
|
|
25279
25997
|
const cachedPlatformByManifest = /* @__PURE__ */ new WeakMap();
|
|
25280
25998
|
const classifyPackagePlatform = (filename) => {
|
|
25281
25999
|
const manifest = readNearestPackageManifest(filename);
|
|
@@ -25503,6 +26221,56 @@ const noBarrelImport = defineRule({
|
|
|
25503
26221
|
}
|
|
25504
26222
|
});
|
|
25505
26223
|
//#endregion
|
|
26224
|
+
//#region src/plugin/utils/function-returns-matching-expression.ts
|
|
26225
|
+
const collectReturnedExpressions = (functionNode) => {
|
|
26226
|
+
if (!isFunctionLike$1(functionNode)) return [];
|
|
26227
|
+
const body = functionNode.body;
|
|
26228
|
+
if (!body) return [];
|
|
26229
|
+
if (!isNodeOfType(body, "BlockStatement")) return [body];
|
|
26230
|
+
const returnedExpressions = [];
|
|
26231
|
+
walkAst(body, (node) => {
|
|
26232
|
+
if (node !== body && (isFunctionLike$1(node) || isNodeOfType(node, "ClassDeclaration") || isNodeOfType(node, "ClassExpression"))) return false;
|
|
26233
|
+
if (isNodeOfType(node, "ReturnStatement") && node.argument) returnedExpressions.push(node.argument);
|
|
26234
|
+
});
|
|
26235
|
+
return returnedExpressions;
|
|
26236
|
+
};
|
|
26237
|
+
const functionReturnsMatchingExpression = (functionNode, scopes, matchesExpression) => {
|
|
26238
|
+
const visitedExpressions = /* @__PURE__ */ new Set();
|
|
26239
|
+
const visitedFunctions = /* @__PURE__ */ new Set();
|
|
26240
|
+
const functionMatches = (candidateFunction) => {
|
|
26241
|
+
if (visitedFunctions.has(candidateFunction)) return false;
|
|
26242
|
+
visitedFunctions.add(candidateFunction);
|
|
26243
|
+
return collectReturnedExpressions(candidateFunction).some(expressionMatches);
|
|
26244
|
+
};
|
|
26245
|
+
const expressionMatches = (expression) => {
|
|
26246
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
26247
|
+
if (visitedExpressions.has(unwrappedExpression)) return false;
|
|
26248
|
+
visitedExpressions.add(unwrappedExpression);
|
|
26249
|
+
if (matchesExpression(unwrappedExpression)) return true;
|
|
26250
|
+
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
26251
|
+
const symbol = scopes.symbolFor(unwrappedExpression);
|
|
26252
|
+
if (!symbol || symbol.kind !== "const" || !symbol.initializer) return false;
|
|
26253
|
+
const initializer = stripParenExpression(symbol.initializer);
|
|
26254
|
+
if (isFunctionLike$1(initializer)) return false;
|
|
26255
|
+
return expressionMatches(initializer);
|
|
26256
|
+
}
|
|
26257
|
+
if (isNodeOfType(unwrappedExpression, "CallExpression")) {
|
|
26258
|
+
if (unwrappedExpression.arguments.length !== 0) return false;
|
|
26259
|
+
if (!isNodeOfType(unwrappedExpression.callee, "Identifier")) return false;
|
|
26260
|
+
const symbol = scopes.symbolFor(unwrappedExpression.callee);
|
|
26261
|
+
if (!symbol || symbol.kind !== "const" && symbol.kind !== "function") return false;
|
|
26262
|
+
const initializer = symbol.initializer ? stripParenExpression(symbol.initializer) : null;
|
|
26263
|
+
const candidateFunction = isFunctionLike$1(initializer) ? initializer : isFunctionLike$1(symbol.declarationNode) ? symbol.declarationNode : null;
|
|
26264
|
+
if (!candidateFunction || candidateFunction.async || candidateFunction.generator || candidateFunction.params.length !== 0) return false;
|
|
26265
|
+
return functionMatches(candidateFunction);
|
|
26266
|
+
}
|
|
26267
|
+
if (isNodeOfType(unwrappedExpression, "ConditionalExpression")) return expressionMatches(unwrappedExpression.consequent) || expressionMatches(unwrappedExpression.alternate);
|
|
26268
|
+
if (isNodeOfType(unwrappedExpression, "LogicalExpression")) return expressionMatches(unwrappedExpression.left) || expressionMatches(unwrappedExpression.right);
|
|
26269
|
+
return false;
|
|
26270
|
+
};
|
|
26271
|
+
return functionMatches(functionNode);
|
|
26272
|
+
};
|
|
26273
|
+
//#endregion
|
|
25506
26274
|
//#region src/plugin/utils/function-contains-react-render-output.ts
|
|
25507
26275
|
const NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES = new Set([
|
|
25508
26276
|
"FunctionDeclaration",
|
|
@@ -25522,25 +26290,24 @@ const isCallArgumentFunctionExpression = (node) => {
|
|
|
25522
26290
|
return parent.arguments.some((argumentNode) => argumentNode === node);
|
|
25523
26291
|
};
|
|
25524
26292
|
const isNestedRenderEvidenceBoundary = (node) => NESTED_RENDER_EVIDENCE_BOUNDARY_TYPES.has(node.type) && !isCallArgumentFunctionExpression(node);
|
|
25525
|
-
const
|
|
25526
|
-
|
|
25527
|
-
|
|
25528
|
-
|
|
25529
|
-
|
|
25530
|
-
|
|
25531
|
-
if (
|
|
25532
|
-
|
|
25533
|
-
|
|
25534
|
-
|
|
25535
|
-
|
|
25536
|
-
|
|
25537
|
-
return false;
|
|
26293
|
+
const isRenderOutputExpression = (node, scopes) => node.type === "JSXElement" || node.type === "JSXFragment" || isReactApiCall(node, "createElement", scopes, REACT_CREATE_ELEMENT_OPTIONS);
|
|
26294
|
+
const containsRenderOutput$1 = (rootNode, scopes) => {
|
|
26295
|
+
let hasRenderOutput = false;
|
|
26296
|
+
walkAst(rootNode, (node) => {
|
|
26297
|
+
if (hasRenderOutput) return false;
|
|
26298
|
+
if (node !== rootNode && isNestedRenderEvidenceBoundary(node)) return false;
|
|
26299
|
+
if (isRenderOutputExpression(node, scopes)) {
|
|
26300
|
+
hasRenderOutput = true;
|
|
26301
|
+
return false;
|
|
26302
|
+
}
|
|
26303
|
+
});
|
|
26304
|
+
return hasRenderOutput;
|
|
25538
26305
|
};
|
|
25539
26306
|
const renderOutputCache = /* @__PURE__ */ new WeakMap();
|
|
25540
26307
|
const functionContainsReactRenderOutput = (functionNode, scopes) => {
|
|
25541
26308
|
const cachedEntry = renderOutputCache.get(functionNode);
|
|
25542
26309
|
if (cachedEntry && cachedEntry.scopes === scopes) return cachedEntry.hasRenderOutput;
|
|
25543
|
-
const hasRenderOutput = containsRenderOutput$1(functionNode, functionNode, scopes);
|
|
26310
|
+
const hasRenderOutput = containsRenderOutput$1(functionNode, scopes) || functionReturnsMatchingExpression(functionNode, scopes, (expression) => isRenderOutputExpression(expression, scopes));
|
|
25544
26311
|
renderOutputCache.set(functionNode, {
|
|
25545
26312
|
scopes,
|
|
25546
26313
|
hasRenderOutput
|
|
@@ -25548,6 +26315,9 @@ const functionContainsReactRenderOutput = (functionNode, scopes) => {
|
|
|
25548
26315
|
return hasRenderOutput;
|
|
25549
26316
|
};
|
|
25550
26317
|
//#endregion
|
|
26318
|
+
//#region src/plugin/utils/is-component-assignment.ts
|
|
26319
|
+
const isComponentAssignment = (node) => isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "Identifier") && isUppercaseName(node.id.name) && Boolean(node.init) && (isNodeOfType(node.init, "ArrowFunctionExpression") || isNodeOfType(node.init, "FunctionExpression"));
|
|
26320
|
+
//#endregion
|
|
25551
26321
|
//#region src/plugin/utils/is-component-declaration.ts
|
|
25552
26322
|
const isComponentDeclaration = (node) => isNodeOfType(node, "FunctionDeclaration") && node.id !== null && Boolean(node.id?.name) && isUppercaseName(node.id.name);
|
|
25553
26323
|
//#endregion
|
|
@@ -26079,6 +26849,19 @@ const noCloneElement = defineRule({
|
|
|
26079
26849
|
} })
|
|
26080
26850
|
});
|
|
26081
26851
|
//#endregion
|
|
26852
|
+
//#region src/plugin/utils/get-call-method-name.ts
|
|
26853
|
+
/**
|
|
26854
|
+
* Returns the static method name of a call's callee when it's a
|
|
26855
|
+
* non-computed MemberExpression (`obj.method` → `"method"`), or
|
|
26856
|
+
* `null` otherwise. Used by `no-pass-data-to-parent` and
|
|
26857
|
+
* `no-pass-live-state-to-parent` to match the receiver-bound
|
|
26858
|
+
* method-call shape against an allow/block list.
|
|
26859
|
+
*/
|
|
26860
|
+
const getCallMethodName = (callee) => {
|
|
26861
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
26862
|
+
return null;
|
|
26863
|
+
};
|
|
26864
|
+
//#endregion
|
|
26082
26865
|
//#region src/plugin/rules/state-and-effects/no-create-context-in-render.ts
|
|
26083
26866
|
const MESSAGE$31 = "createContext() builds a new context every render, so every consumer gets cut off & resets.";
|
|
26084
26867
|
const CONTEXT_MODULES = [
|
|
@@ -26086,23 +26869,35 @@ const CONTEXT_MODULES = [
|
|
|
26086
26869
|
"use-context-selector",
|
|
26087
26870
|
"react-tracked"
|
|
26088
26871
|
];
|
|
26089
|
-
const
|
|
26872
|
+
const getSupportedContextImportSource = (symbol) => {
|
|
26873
|
+
if (symbol?.kind !== "import") return null;
|
|
26874
|
+
const importDeclaration = symbol.declarationNode.parent;
|
|
26875
|
+
if (!importDeclaration || !isNodeOfType(importDeclaration, "ImportDeclaration") || typeof importDeclaration.source.value !== "string" || !CONTEXT_MODULES.includes(importDeclaration.source.value)) return null;
|
|
26876
|
+
return importDeclaration.source.value;
|
|
26877
|
+
};
|
|
26878
|
+
const isSupportedNamespaceSymbol = (symbol) => getSupportedContextImportSource(symbol) !== null && Boolean(symbol && (isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier")));
|
|
26879
|
+
const isDestructuredCreateContextBinding = (identifier, scopes) => {
|
|
26880
|
+
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
26881
|
+
const symbol = scopes.symbolFor(identifier);
|
|
26882
|
+
if (symbol?.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || !isNodeOfType(symbol.declarationNode.id, "ObjectPattern")) return false;
|
|
26883
|
+
const property = symbol.bindingIdentifier.parent;
|
|
26884
|
+
if (!property || !isNodeOfType(property, "Property") || getStaticPropertyKeyName(property, { allowComputedString: true }) !== "createContext") return false;
|
|
26885
|
+
const initializer = stripParenExpression(symbol.initializer);
|
|
26886
|
+
return isNodeOfType(initializer, "Identifier") && isSupportedNamespaceSymbol(resolveConstIdentifierAlias(initializer, scopes));
|
|
26887
|
+
};
|
|
26888
|
+
const isCreateContextCall = (node, scopes) => {
|
|
26889
|
+
const callee = stripParenExpression(node.callee);
|
|
26090
26890
|
if (isNodeOfType(callee, "Identifier")) {
|
|
26091
|
-
|
|
26092
|
-
|
|
26891
|
+
if (isDestructuredCreateContextBinding(callee, scopes)) return true;
|
|
26892
|
+
const symbol = resolveConstIdentifierAlias(callee, scopes);
|
|
26893
|
+
return getSupportedContextImportSource(symbol) !== null && Boolean(symbol && getImportedName(symbol.declarationNode) === "createContext");
|
|
26093
26894
|
}
|
|
26094
|
-
if (isNodeOfType(callee, "MemberExpression")
|
|
26095
|
-
|
|
26096
|
-
|
|
26097
|
-
|
|
26098
|
-
|
|
26099
|
-
|
|
26100
|
-
const namespaceName = namespaceIdentifier.name;
|
|
26101
|
-
if (isCanonicalReactNamespaceName(namespaceName)) return true;
|
|
26102
|
-
const importSource = getImportSourceForName(namespaceIdentifier, namespaceName);
|
|
26103
|
-
return importSource !== null && CONTEXT_MODULES.includes(importSource);
|
|
26104
|
-
}
|
|
26105
|
-
return false;
|
|
26895
|
+
if (!isNodeOfType(callee, "MemberExpression")) return false;
|
|
26896
|
+
if ((getCallMethodName(callee) ?? (callee.computed && isNodeOfType(callee.property, "Literal") && typeof callee.property.value === "string" ? callee.property.value : null)) !== "createContext") return false;
|
|
26897
|
+
const receiver = stripParenExpression(callee.object);
|
|
26898
|
+
if (!isNodeOfType(receiver, "Identifier")) return false;
|
|
26899
|
+
if (receiver.name === "React" && scopes.isGlobalReference(receiver)) return true;
|
|
26900
|
+
return isSupportedNamespaceSymbol(resolveConstIdentifierAlias(receiver, scopes));
|
|
26106
26901
|
};
|
|
26107
26902
|
const noCreateContextInRender = defineRule({
|
|
26108
26903
|
id: "no-create-context-in-render",
|
|
@@ -26111,7 +26906,7 @@ const noCreateContextInRender = defineRule({
|
|
|
26111
26906
|
category: "Correctness",
|
|
26112
26907
|
recommendation: "Move `createContext(...)` outside the component, to the top level of the file, so it stays the same on every render.",
|
|
26113
26908
|
create: (context) => ({ CallExpression(node) {
|
|
26114
|
-
if (!
|
|
26909
|
+
if (!isCreateContextCall(node, context.scopes)) return;
|
|
26115
26910
|
const componentOrHookName = enclosingComponentOrHookName(node);
|
|
26116
26911
|
if (!componentOrHookName) return;
|
|
26117
26912
|
context.report({
|
|
@@ -27346,7 +28141,7 @@ const isSetStateCallInLifecycle = (setStateCall, lifecycleNames, options = {}) =
|
|
|
27346
28141
|
let ancestor = setStateCall.parent;
|
|
27347
28142
|
while (ancestor) {
|
|
27348
28143
|
if (!lifecycleMember) {
|
|
27349
|
-
if (FUNCTION_NODE_TYPES$1.has(ancestor.type)) nestedFunctionCount += 1;
|
|
28144
|
+
if (FUNCTION_NODE_TYPES$1.has(ancestor.type) && (!isImmediatelyInvokedFunction(ancestor) || !("async" in ancestor) || ancestor.async === true)) nestedFunctionCount += 1;
|
|
27350
28145
|
if (isLifecycleMember(ancestor, lifecycleNames)) lifecycleMember = ancestor;
|
|
27351
28146
|
} else if (isEs5Component(ancestor) || isEs6Component(ancestor)) {
|
|
27352
28147
|
if (nestedFunctionCount > 1 && !options.disallowInNestedFunctions) return false;
|
|
@@ -27548,7 +28343,7 @@ const collectDiffSourceLocalNames = (lifecycleFunction, paramNames) => {
|
|
|
27548
28343
|
const body = lifecycleFunction.body;
|
|
27549
28344
|
if (!body) return derivedNames;
|
|
27550
28345
|
walkAst(body, (node) => {
|
|
27551
|
-
if (FUNCTION_NODE_TYPES.has(node.type)) return false;
|
|
28346
|
+
if (FUNCTION_NODE_TYPES.has(node.type) && !isImmediatelyInvokedFunction(node)) return false;
|
|
27552
28347
|
if (!isNodeOfType(node, "VariableDeclarator")) return;
|
|
27553
28348
|
const init = node.init;
|
|
27554
28349
|
if (!init) return;
|
|
@@ -27558,6 +28353,67 @@ const collectDiffSourceLocalNames = (lifecycleFunction, paramNames) => {
|
|
|
27558
28353
|
return derivedNames;
|
|
27559
28354
|
};
|
|
27560
28355
|
const isStatefulOperand = (node, paramNames, derivedNames) => referencesAnyName(node, paramNames) || referencesAnyName(node, derivedNames) || containsThisStateOrProps(node);
|
|
28356
|
+
const getStaticMemberName = (node) => {
|
|
28357
|
+
if (!isNodeOfType(node, "MemberExpression") || node.computed === true) return null;
|
|
28358
|
+
return isNodeOfType(node.property, "Identifier") ? node.property.name : null;
|
|
28359
|
+
};
|
|
28360
|
+
const getThisStateFieldName = (node) => {
|
|
28361
|
+
const unwrappedNode = stripParenExpression(node);
|
|
28362
|
+
if (!isNodeOfType(unwrappedNode, "MemberExpression")) return null;
|
|
28363
|
+
const object = stripParenExpression(unwrappedNode.object);
|
|
28364
|
+
if (!isNodeOfType(object, "MemberExpression") || !isNodeOfType(stripParenExpression(object.object), "ThisExpression") || getStaticMemberName(object) !== "state") return null;
|
|
28365
|
+
return getStaticMemberName(unwrappedNode);
|
|
28366
|
+
};
|
|
28367
|
+
const collectLocalInitializers = (lifecycleFunction) => {
|
|
28368
|
+
const initializers = /* @__PURE__ */ new Map();
|
|
28369
|
+
const body = lifecycleFunction.body;
|
|
28370
|
+
if (!body) return initializers;
|
|
28371
|
+
walkAst(body, (node) => {
|
|
28372
|
+
if (FUNCTION_NODE_TYPES.has(node.type) && !isImmediatelyInvokedFunction(node)) return false;
|
|
28373
|
+
if (isNodeOfType(node, "VariableDeclarator") && isNodeOfType(node.id, "Identifier") && node.init) initializers.set(node.id.name, node.init);
|
|
28374
|
+
});
|
|
28375
|
+
return initializers;
|
|
28376
|
+
};
|
|
28377
|
+
const derivesFromPostMountValue = (node, localInitializers, visitedNames = /* @__PURE__ */ new Set()) => {
|
|
28378
|
+
if (readsPostMountValue(node)) return true;
|
|
28379
|
+
const referencedNames = /* @__PURE__ */ new Set();
|
|
28380
|
+
collectReferenceIdentifierNames(node, referencedNames);
|
|
28381
|
+
for (const referencedName of referencedNames) {
|
|
28382
|
+
if (visitedNames.has(referencedName)) continue;
|
|
28383
|
+
const initializer = localInitializers.get(referencedName);
|
|
28384
|
+
if (!initializer) continue;
|
|
28385
|
+
if (derivesFromPostMountValue(initializer, localInitializers, new Set([...visitedNames, referencedName]))) return true;
|
|
28386
|
+
}
|
|
28387
|
+
return false;
|
|
28388
|
+
};
|
|
28389
|
+
const getSetStateFieldValue = (setStateCall, fieldName) => {
|
|
28390
|
+
if (!isNodeOfType(setStateCall, "CallExpression")) return null;
|
|
28391
|
+
const argument = setStateCall.arguments?.[0];
|
|
28392
|
+
if (!argument || !isNodeOfType(argument, "ObjectExpression")) return null;
|
|
28393
|
+
for (const property of argument.properties ?? []) {
|
|
28394
|
+
if (!isNodeOfType(property, "Property") || property.computed === true) continue;
|
|
28395
|
+
if ((isNodeOfType(property.key, "Identifier") && property.key.name || isNodeOfType(property.key, "Literal") && typeof property.key.value === "string" && property.key.value || null) === fieldName) return property.value;
|
|
28396
|
+
}
|
|
28397
|
+
return null;
|
|
28398
|
+
};
|
|
28399
|
+
const isConvergentPostMountGuard = (test, setStateCall, localInitializers) => {
|
|
28400
|
+
let qualifies = false;
|
|
28401
|
+
walkAst(test, (node) => {
|
|
28402
|
+
if (qualifies) return false;
|
|
28403
|
+
if (!isNodeOfType(node, "BinaryExpression") || !EQUALITY_OPERATORS.has(node.operator)) return;
|
|
28404
|
+
const leftFieldName = getThisStateFieldName(node.left);
|
|
28405
|
+
const rightFieldName = getThisStateFieldName(node.right);
|
|
28406
|
+
const fieldName = leftFieldName ?? rightFieldName;
|
|
28407
|
+
const comparedValue = leftFieldName ? node.right : node.left;
|
|
28408
|
+
if (!fieldName || !leftFieldName && !rightFieldName) return;
|
|
28409
|
+
const assignedValue = getSetStateFieldValue(setStateCall, fieldName);
|
|
28410
|
+
if (!assignedValue || !areExpressionsStructurallyEqual(comparedValue, assignedValue)) return;
|
|
28411
|
+
if (!derivesFromPostMountValue(comparedValue, localInitializers)) return;
|
|
28412
|
+
qualifies = true;
|
|
28413
|
+
return false;
|
|
28414
|
+
});
|
|
28415
|
+
return qualifies;
|
|
28416
|
+
};
|
|
27561
28417
|
const isDiffGuardTest = (test, paramNames, derivedNames) => {
|
|
27562
28418
|
if (referencesAnyName(test, paramNames)) return true;
|
|
27563
28419
|
let qualifies = false;
|
|
@@ -27565,7 +28421,7 @@ const isDiffGuardTest = (test, paramNames, derivedNames) => {
|
|
|
27565
28421
|
if (qualifies) return false;
|
|
27566
28422
|
if (!isNodeOfType(node, "BinaryExpression")) return;
|
|
27567
28423
|
if (!EQUALITY_OPERATORS.has(node.operator)) return;
|
|
27568
|
-
if (isStatefulOperand(node.left, paramNames, derivedNames) && isStatefulOperand(node.right, paramNames, derivedNames)) {
|
|
28424
|
+
if (isStatefulOperand(node.left, paramNames, derivedNames) && isStatefulOperand(node.right, paramNames, derivedNames) && (referencesAnyName(node.left, derivedNames) || referencesAnyName(node.right, derivedNames))) {
|
|
27569
28425
|
qualifies = true;
|
|
27570
28426
|
return false;
|
|
27571
28427
|
}
|
|
@@ -27578,11 +28434,12 @@ const isInsideDiffGuard = (setStateCall) => {
|
|
|
27578
28434
|
const paramNames = /* @__PURE__ */ new Set();
|
|
27579
28435
|
for (const param of lifecycleFunction.params ?? []) collectPatternNames(param, paramNames);
|
|
27580
28436
|
const derivedNames = collectDiffSourceLocalNames(lifecycleFunction, paramNames);
|
|
28437
|
+
const localInitializers = collectLocalInitializers(lifecycleFunction);
|
|
27581
28438
|
let child = setStateCall;
|
|
27582
28439
|
let ancestor = setStateCall.parent;
|
|
27583
28440
|
while (ancestor && ancestor !== lifecycleFunction) {
|
|
27584
28441
|
const guardTest = isNodeOfType(ancestor, "IfStatement") && child !== ancestor.test && ancestor.test || isNodeOfType(ancestor, "ConditionalExpression") && child !== ancestor.test && ancestor.test || isNodeOfType(ancestor, "LogicalExpression") && ancestor.operator === "&&" && child === ancestor.right && ancestor.left || null;
|
|
27585
|
-
if (guardTest && isDiffGuardTest(guardTest, paramNames, derivedNames)) return true;
|
|
28442
|
+
if (guardTest && (isDiffGuardTest(guardTest, paramNames, derivedNames) || isConvergentPostMountGuard(guardTest, setStateCall, localInitializers))) return true;
|
|
27586
28443
|
child = ancestor;
|
|
27587
28444
|
ancestor = ancestor.parent ?? null;
|
|
27588
28445
|
}
|
|
@@ -27763,7 +28620,16 @@ const producesOpaqueInstanceValue = (expression) => {
|
|
|
27763
28620
|
const collectSetterValueObservations = (componentBody, setterNames) => {
|
|
27764
28621
|
const plainFedSetterNames = /* @__PURE__ */ new Set();
|
|
27765
28622
|
const opaqueFedSetterNames = /* @__PURE__ */ new Set();
|
|
28623
|
+
const callbackRefSetterNames = /* @__PURE__ */ new Set();
|
|
27766
28624
|
walkComponentRespectingShadows(componentBody, /* @__PURE__ */ new Set(), (node, currentlyShadowed) => {
|
|
28625
|
+
if (isNodeOfType(node, "JSXAttribute")) {
|
|
28626
|
+
const attributeName = node.name;
|
|
28627
|
+
if (isNodeOfType(attributeName, "JSXIdentifier") && attributeName.name === "ref" && node.value && isNodeOfType(node.value, "JSXExpressionContainer")) {
|
|
28628
|
+
const expression = stripParenExpression(node.value.expression);
|
|
28629
|
+
if (isNodeOfType(expression, "Identifier") && setterNames.has(expression.name)) callbackRefSetterNames.add(expression.name);
|
|
28630
|
+
}
|
|
28631
|
+
return;
|
|
28632
|
+
}
|
|
27767
28633
|
if (!isNodeOfType(node, "CallExpression")) return;
|
|
27768
28634
|
if (!isNodeOfType(node.callee, "Identifier")) return;
|
|
27769
28635
|
const setterName = node.callee.name;
|
|
@@ -27780,21 +28646,10 @@ const collectSetterValueObservations = (componentBody, setterNames) => {
|
|
|
27780
28646
|
}, true);
|
|
27781
28647
|
return {
|
|
27782
28648
|
plainFedSetterNames,
|
|
27783
|
-
opaqueFedSetterNames
|
|
28649
|
+
opaqueFedSetterNames,
|
|
28650
|
+
callbackRefSetterNames
|
|
27784
28651
|
};
|
|
27785
28652
|
};
|
|
27786
|
-
const collectCallbackRefSetterNames = (componentBody) => {
|
|
27787
|
-
const callbackRefSetterNames = /* @__PURE__ */ new Set();
|
|
27788
|
-
walkAst(componentBody, (node) => {
|
|
27789
|
-
if (!isNodeOfType(node, "JSXAttribute")) return;
|
|
27790
|
-
const attributeName = node.name;
|
|
27791
|
-
if (isNodeOfType(attributeName, "JSXIdentifier") && attributeName.name === "ref" && node.value && isNodeOfType(node.value, "JSXExpressionContainer")) {
|
|
27792
|
-
const expression = stripParenExpression(node.value.expression);
|
|
27793
|
-
if (isNodeOfType(expression, "Identifier")) callbackRefSetterNames.add(expression.name);
|
|
27794
|
-
}
|
|
27795
|
-
});
|
|
27796
|
-
return callbackRefSetterNames;
|
|
27797
|
-
};
|
|
27798
28653
|
const collectFunctionLocalBindings = (functionNode) => {
|
|
27799
28654
|
const localBindings = /* @__PURE__ */ new Set();
|
|
27800
28655
|
if (!isNodeOfType(functionNode, "FunctionDeclaration") && !isNodeOfType(functionNode, "FunctionExpression") && !isNodeOfType(functionNode, "ArrowFunctionExpression")) return localBindings;
|
|
@@ -27806,8 +28661,8 @@ const collectFunctionLocalBindings = (functionNode) => {
|
|
|
27806
28661
|
return localBindings;
|
|
27807
28662
|
};
|
|
27808
28663
|
const collectBlockScopedBindings = (node) => {
|
|
27809
|
-
const blockBindings = /* @__PURE__ */ new Set();
|
|
27810
28664
|
if (isNodeOfType(node, "BlockStatement")) {
|
|
28665
|
+
const blockBindings = /* @__PURE__ */ new Set();
|
|
27811
28666
|
for (const statement of node.body ?? []) {
|
|
27812
28667
|
if (!isNodeOfType(statement, "VariableDeclaration")) continue;
|
|
27813
28668
|
for (const declarator of statement.declarations ?? []) collectPatternNames(declarator.id, blockBindings);
|
|
@@ -27815,17 +28670,22 @@ const collectBlockScopedBindings = (node) => {
|
|
|
27815
28670
|
return blockBindings;
|
|
27816
28671
|
}
|
|
27817
28672
|
if (isNodeOfType(node, "ForStatement") && isNodeOfType(node.init, "VariableDeclaration")) {
|
|
28673
|
+
const blockBindings = /* @__PURE__ */ new Set();
|
|
27818
28674
|
for (const declarator of node.init.declarations ?? []) collectPatternNames(declarator.id, blockBindings);
|
|
27819
28675
|
return blockBindings;
|
|
27820
28676
|
}
|
|
27821
|
-
if ((isNodeOfType(node, "ForOfStatement") || isNodeOfType(node, "ForInStatement")) && isNodeOfType(node.left, "VariableDeclaration"))
|
|
27822
|
-
|
|
28677
|
+
if ((isNodeOfType(node, "ForOfStatement") || isNodeOfType(node, "ForInStatement")) && isNodeOfType(node.left, "VariableDeclaration")) {
|
|
28678
|
+
const blockBindings = /* @__PURE__ */ new Set();
|
|
28679
|
+
for (const declarator of node.left.declarations ?? []) collectPatternNames(declarator.id, blockBindings);
|
|
28680
|
+
return blockBindings;
|
|
28681
|
+
}
|
|
28682
|
+
return null;
|
|
27823
28683
|
};
|
|
27824
28684
|
const walkComponentRespectingShadows = (node, shadowedStateNames, visit, isComponentBodyRoot = false) => {
|
|
27825
28685
|
if (!node || typeof node !== "object") return;
|
|
27826
28686
|
let nextShadowedStateNames = shadowedStateNames;
|
|
27827
|
-
const localBindings = isComponentBodyRoot ?
|
|
27828
|
-
if (localBindings.size > 0) {
|
|
28687
|
+
const localBindings = isComponentBodyRoot ? null : isFunctionLike$1(node) ? collectFunctionLocalBindings(node) : collectBlockScopedBindings(node);
|
|
28688
|
+
if (localBindings && localBindings.size > 0) {
|
|
27829
28689
|
const merged = new Set(shadowedStateNames);
|
|
27830
28690
|
for (const localName of localBindings) merged.add(localName);
|
|
27831
28691
|
nextShadowedStateNames = merged;
|
|
@@ -27851,11 +28711,10 @@ const noDirectStateMutation = defineRule({
|
|
|
27851
28711
|
const bindings = collectUseStateBindings(componentBody);
|
|
27852
28712
|
if (bindings.length === 0) return;
|
|
27853
28713
|
const stateValueToSetter = new Map(bindings.map((binding) => [binding.valueName, binding.setterName]));
|
|
27854
|
-
const callbackRefSetterNames = collectCallbackRefSetterNames(componentBody);
|
|
27855
28714
|
const setterValueObservations = collectSetterValueObservations(componentBody, new Set(bindings.map((binding) => binding.setterName)));
|
|
27856
28715
|
const plainObjectStateValueNames = /* @__PURE__ */ new Set();
|
|
27857
28716
|
for (const binding of bindings) {
|
|
27858
|
-
if (callbackRefSetterNames.has(binding.setterName)) continue;
|
|
28717
|
+
if (setterValueObservations.callbackRefSetterNames.has(binding.setterName)) continue;
|
|
27859
28718
|
if (!isNodeOfType(binding.declarator.init, "CallExpression")) continue;
|
|
27860
28719
|
const initializerArgument = binding.declarator.init.arguments?.[0];
|
|
27861
28720
|
if (!initializerMarksPlainState(initializerArgument)) continue;
|
|
@@ -27916,7 +28775,7 @@ const noDisabledZoom = defineRule({
|
|
|
27916
28775
|
category: "Accessibility",
|
|
27917
28776
|
recommendation: "Remove `user-scalable=no` and `maximum-scale` from the viewport meta tag. If the layout breaks at 200% zoom, fix the layout instead.",
|
|
27918
28777
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
27919
|
-
if (
|
|
28778
|
+
if (resolveJsxElementType(node) !== "meta") return;
|
|
27920
28779
|
const nameAttr = findJsxAttribute(node.attributes ?? [], "name");
|
|
27921
28780
|
if (!nameAttr?.value) return;
|
|
27922
28781
|
if ((isNodeOfType(nameAttr.value, "Literal") ? nameAttr.value.value : null) !== "viewport") return;
|
|
@@ -27997,20 +28856,31 @@ const noDocumentStartViewTransition = defineRule({
|
|
|
27997
28856
|
} })
|
|
27998
28857
|
});
|
|
27999
28858
|
//#endregion
|
|
28859
|
+
//#region src/plugin/utils/get-static-property-name.ts
|
|
28860
|
+
const getStaticPropertyName = (memberExpression) => {
|
|
28861
|
+
const property = memberExpression.property;
|
|
28862
|
+
if (!memberExpression.computed && isNodeOfType(property, "Identifier")) return property.name;
|
|
28863
|
+
if (memberExpression.computed && isNodeOfType(property, "Literal")) return typeof property.value === "string" ? property.value : null;
|
|
28864
|
+
if (memberExpression.computed && isNodeOfType(property, "TemplateLiteral") && property.expressions.length === 0) return property.quasis[0]?.value.cooked ?? property.quasis[0]?.value.raw ?? null;
|
|
28865
|
+
return null;
|
|
28866
|
+
};
|
|
28867
|
+
//#endregion
|
|
28000
28868
|
//#region src/plugin/rules/js-performance/no-document-write.ts
|
|
28001
28869
|
const MESSAGE$24 = "`document.write()` blocks parsing, is ignored (or wipes the page) after load, and is flagged by browsers as a performance anti-pattern. Build DOM nodes or set `innerHTML`/`textContent` on a target element instead.";
|
|
28002
28870
|
const WRITE_METHODS = new Set(["write", "writeln"]);
|
|
28871
|
+
const isGlobalDocumentReference = (node, context) => node.name === "document" && context.scopes.isGlobalReference(node);
|
|
28003
28872
|
const noDocumentWrite = defineRule({
|
|
28004
28873
|
id: "no-document-write",
|
|
28005
28874
|
title: "document.write/writeln",
|
|
28006
28875
|
severity: "warn",
|
|
28007
28876
|
recommendation: "Don't use `document.write()`/`document.writeln()`. Append DOM nodes or set `innerHTML`/`textContent` on a specific element instead.",
|
|
28008
28877
|
create: (context) => ({ CallExpression(node) {
|
|
28009
|
-
const callee = node.callee;
|
|
28010
|
-
if (!isNodeOfType(callee, "MemberExpression")
|
|
28878
|
+
const callee = stripParenExpression(node.callee);
|
|
28879
|
+
if (!isNodeOfType(callee, "MemberExpression")) return;
|
|
28011
28880
|
const receiver = stripParenExpression(callee.object);
|
|
28012
|
-
if (!isNodeOfType(receiver, "Identifier") || receiver
|
|
28013
|
-
|
|
28881
|
+
if (!isNodeOfType(receiver, "Identifier") || !isGlobalDocumentReference(receiver, context)) return;
|
|
28882
|
+
const methodName = getStaticPropertyName(callee);
|
|
28883
|
+
if (methodName === null || !WRITE_METHODS.has(methodName)) return;
|
|
28014
28884
|
context.report({
|
|
28015
28885
|
node,
|
|
28016
28886
|
message: MESSAGE$24
|
|
@@ -28295,7 +29165,7 @@ const findTopLevelEffectCalls = (componentBody) => {
|
|
|
28295
29165
|
if (!isNodeOfType(componentBody, "BlockStatement")) return effectCalls;
|
|
28296
29166
|
for (const statement of componentBody.body ?? []) {
|
|
28297
29167
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
28298
|
-
const expression = statement
|
|
29168
|
+
const expression = unwrapDiscardedExpression(statement);
|
|
28299
29169
|
if (!isNodeOfType(expression, "CallExpression")) continue;
|
|
28300
29170
|
if (!isHookCall$2(expression, EFFECT_HOOK_NAMES$1)) continue;
|
|
28301
29171
|
effectCalls.push(expression);
|
|
@@ -28817,6 +29687,14 @@ const noEffectWithFreshDeps = defineRule({
|
|
|
28817
29687
|
//#endregion
|
|
28818
29688
|
//#region src/plugin/rules/security/no-eval.ts
|
|
28819
29689
|
const SANDBOX_SURFACE_PATH_PATTERN = /(?:^|[/-])sandbox(?:$|[/-])|(?:^|\/)[\w.]+-sandbox(?:\/|\.[cm]?[jt]sx?$)/i;
|
|
29690
|
+
const getExecutableGlobalName = (node, context) => {
|
|
29691
|
+
const expression = stripParenExpression(node);
|
|
29692
|
+
if (isNodeOfType(expression, "Identifier")) return context.scopes.isGlobalReference(expression) ? expression.name : null;
|
|
29693
|
+
if (!isNodeOfType(expression, "MemberExpression")) return null;
|
|
29694
|
+
const receiver = stripParenExpression(expression.object);
|
|
29695
|
+
if (!isNodeOfType(receiver, "Identifier") || receiver.name !== "globalThis" || !context.scopes.isGlobalReference(receiver)) return null;
|
|
29696
|
+
return getStaticPropertyName(expression);
|
|
29697
|
+
};
|
|
28820
29698
|
const noEval = defineRule({
|
|
28821
29699
|
id: "no-eval",
|
|
28822
29700
|
title: "eval() runs untrusted code strings",
|
|
@@ -28826,20 +29704,21 @@ const noEval = defineRule({
|
|
|
28826
29704
|
if (SANDBOX_SURFACE_PATH_PATTERN.test(normalizeFilename(context.filename ?? ""))) return {};
|
|
28827
29705
|
return {
|
|
28828
29706
|
CallExpression(node) {
|
|
28829
|
-
|
|
29707
|
+
const executableGlobalName = getExecutableGlobalName(node.callee, context);
|
|
29708
|
+
if (executableGlobalName === "eval") {
|
|
28830
29709
|
context.report({
|
|
28831
29710
|
node,
|
|
28832
29711
|
message: "eval() is a code-injection vulnerability: it runs any string as code."
|
|
28833
29712
|
});
|
|
28834
29713
|
return;
|
|
28835
29714
|
}
|
|
28836
|
-
if (
|
|
29715
|
+
if ((executableGlobalName === "setTimeout" || executableGlobalName === "setInterval") && isNodeOfType(node.arguments?.[0], "Literal") && typeof node.arguments[0].value === "string") context.report({
|
|
28837
29716
|
node,
|
|
28838
|
-
message: `Passing a string to ${
|
|
29717
|
+
message: `Passing a string to ${executableGlobalName}() is a code-injection vulnerability, since it runs that string as code.`
|
|
28839
29718
|
});
|
|
28840
29719
|
},
|
|
28841
29720
|
NewExpression(node) {
|
|
28842
|
-
if (
|
|
29721
|
+
if (getExecutableGlobalName(node.callee, context) === "Function") {
|
|
28843
29722
|
const onlyArgument = node.arguments.length === 1 ? node.arguments[0] : void 0;
|
|
28844
29723
|
if (isNodeOfType(onlyArgument, "Literal") && typeof onlyArgument.value === "string" && onlyArgument.value.trim() === "return this") return;
|
|
28845
29724
|
context.report({
|
|
@@ -30098,7 +30977,11 @@ const noFetchInEffect = defineRule({
|
|
|
30098
30977
|
severity: "warn",
|
|
30099
30978
|
recommendation: "Use a data-fetching layer or Server Component so fetches do not race, double-fire, or leak from `useEffect`.",
|
|
30100
30979
|
create: (context) => ({ CallExpression(node) {
|
|
30101
|
-
if (!
|
|
30980
|
+
if (!isReactApiCall(node, EFFECT_HOOK_NAMES$1, context.scopes, {
|
|
30981
|
+
allowGlobalReactNamespace: true,
|
|
30982
|
+
allowUnboundBareCalls: true,
|
|
30983
|
+
resolveNamedAliases: true
|
|
30984
|
+
})) return;
|
|
30102
30985
|
const callback = getEffectCallback(node);
|
|
30103
30986
|
if (!callback) return;
|
|
30104
30987
|
const analysisFunctions = collectEffectAnalysisFunctions(callback, context);
|
|
@@ -31064,7 +31947,7 @@ const noImgLazyWithHighFetchpriority = defineRule({
|
|
|
31064
31947
|
severity: "warn",
|
|
31065
31948
|
recommendation: "Don't combine `loading=\"lazy\"` with `fetchPriority=\"high\"`. A high-priority image (usually the LCP) should load eagerly; a lazy image is by definition not high priority.",
|
|
31066
31949
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
31067
|
-
if (
|
|
31950
|
+
if (resolveJsxElementType(node) !== "img") return;
|
|
31068
31951
|
const loadingAttribute = hasJsxPropIgnoreCase(node.attributes, "loading");
|
|
31069
31952
|
if (!loadingAttribute || getJsxPropStringValue(loadingAttribute)?.toLowerCase() !== "lazy") return;
|
|
31070
31953
|
const fetchPriorityAttribute = hasJsxPropIgnoreCase(node.attributes, "fetchPriority");
|
|
@@ -31305,7 +32188,7 @@ const noIndeterminateAttribute = defineRule({
|
|
|
31305
32188
|
if (classifyReactNativeFileTarget(context) === "react-native") return {};
|
|
31306
32189
|
return {
|
|
31307
32190
|
JSXOpeningElement(node) {
|
|
31308
|
-
if (
|
|
32191
|
+
if (resolveJsxElementType(node) !== "input") return;
|
|
31309
32192
|
let typeAttribute = null;
|
|
31310
32193
|
let typeAttributeIndex = null;
|
|
31311
32194
|
let indeterminateAttribute = null;
|
|
@@ -31484,11 +32367,12 @@ const isMemoCall = (node) => {
|
|
|
31484
32367
|
};
|
|
31485
32368
|
const isDefaultEquivalentComparator = (comparator) => isNodeOfType(comparator, "Identifier") && (comparator.name === "undefined" || comparator.name === "shallowEqual");
|
|
31486
32369
|
const hasCustomComparator = (node) => isNodeOfType(node, "CallExpression") && (node.arguments?.length ?? 0) >= 2 && !isDefaultEquivalentComparator(node.arguments?.[1]);
|
|
31487
|
-
const isInlineReference = (node) => {
|
|
31488
|
-
|
|
31489
|
-
if (isNodeOfType(
|
|
31490
|
-
if (isNodeOfType(
|
|
31491
|
-
if (isNodeOfType(
|
|
32370
|
+
const isInlineReference = (node, scopes) => {
|
|
32371
|
+
const referenceNode = unwrapObjectIntegrityExpression(node, scopes);
|
|
32372
|
+
if (isNodeOfType(referenceNode, "ArrowFunctionExpression") || isNodeOfType(referenceNode, "FunctionExpression") || isNodeOfType(referenceNode, "CallExpression") && isNodeOfType(referenceNode.callee, "MemberExpression") && isNodeOfType(referenceNode.callee.property, "Identifier") && referenceNode.callee.property.name === "bind") return "functions";
|
|
32373
|
+
if (isNodeOfType(referenceNode, "ObjectExpression")) return "objects";
|
|
32374
|
+
if (isNodeOfType(referenceNode, "ArrayExpression")) return "Arrays";
|
|
32375
|
+
if (isNodeOfType(referenceNode, "JSXElement") || isNodeOfType(referenceNode, "JSXFragment")) return "JSX";
|
|
31492
32376
|
return null;
|
|
31493
32377
|
};
|
|
31494
32378
|
const noInlinePropOnMemoComponent = defineRule({
|
|
@@ -31518,7 +32402,7 @@ const noInlinePropOnMemoComponent = defineRule({
|
|
|
31518
32402
|
let elementName = null;
|
|
31519
32403
|
if (isNodeOfType(openingElement.name, "JSXIdentifier")) elementName = openingElement.name.name;
|
|
31520
32404
|
if (!elementName || !memoizedComponentNames.has(elementName)) return;
|
|
31521
|
-
const propType = isInlineReference(node.value.expression);
|
|
32405
|
+
const propType = isInlineReference(node.value.expression, context.scopes);
|
|
31522
32406
|
if (propType) context.report({
|
|
31523
32407
|
node: node.value.expression,
|
|
31524
32408
|
message: `This redraws ${elementName} on every render because the prop is ${propType} built right here, so memo() can't skip it. Move it to a stable value with useMemo, useCallback, or module scope`
|
|
@@ -32377,11 +33261,13 @@ const noManyBooleanProps = defineRule({
|
|
|
32377
33261
|
};
|
|
32378
33262
|
const checkComponent = (functionNode, param, body, componentName, reportNode) => {
|
|
32379
33263
|
if (!param) return;
|
|
33264
|
+
const propsBinding = resolveFirstArgumentBinding(param);
|
|
33265
|
+
if (!propsBinding) return;
|
|
32380
33266
|
if (!functionContainsReactRenderOutput(functionNode, context.scopes)) return;
|
|
32381
|
-
if (isNodeOfType(
|
|
33267
|
+
if (isNodeOfType(propsBinding, "ObjectPattern")) {
|
|
32382
33268
|
const callbackUsedNames = collectCallbackUsedNames(body, param, context.scopes);
|
|
32383
33269
|
const booleanLikePropNames = [];
|
|
32384
|
-
for (const property of
|
|
33270
|
+
for (const property of propsBinding.properties ?? []) {
|
|
32385
33271
|
if (!isNodeOfType(property, "Property")) continue;
|
|
32386
33272
|
const keyName = isNodeOfType(property.key, "Identifier") ? property.key.name : null;
|
|
32387
33273
|
if (!keyName) continue;
|
|
@@ -32392,7 +33278,7 @@ const noManyBooleanProps = defineRule({
|
|
|
32392
33278
|
reportIfMany(booleanLikePropNames, componentName, reportNode);
|
|
32393
33279
|
return;
|
|
32394
33280
|
}
|
|
32395
|
-
if (isNodeOfType(
|
|
33281
|
+
if (isNodeOfType(propsBinding, "Identifier")) reportIfMany([...collectBooleanLikePropsFromBody(body, propsBinding.name)], componentName, reportNode);
|
|
32396
33282
|
};
|
|
32397
33283
|
return {
|
|
32398
33284
|
FunctionDeclaration(node) {
|
|
@@ -32514,7 +33400,7 @@ const noMirrorPropEffect = defineRule({
|
|
|
32514
33400
|
if (mirrorBindings.length === 0) return;
|
|
32515
33401
|
for (const statement of componentBody.body ?? []) {
|
|
32516
33402
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
32517
|
-
const effectCall = statement
|
|
33403
|
+
const effectCall = unwrapDiscardedExpression(statement);
|
|
32518
33404
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
32519
33405
|
if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) continue;
|
|
32520
33406
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
@@ -32528,7 +33414,7 @@ const noMirrorPropEffect = defineRule({
|
|
|
32528
33414
|
const bodyStatements = getCallbackStatements(callback);
|
|
32529
33415
|
if (bodyStatements.length !== 1) continue;
|
|
32530
33416
|
const onlyStatement = bodyStatements[0];
|
|
32531
|
-
const expression =
|
|
33417
|
+
const expression = unwrapDiscardedExpression(onlyStatement);
|
|
32532
33418
|
if (!isNodeOfType(expression, "CallExpression")) continue;
|
|
32533
33419
|
if (!isNodeOfType(expression.callee, "Identifier")) continue;
|
|
32534
33420
|
if (!isSetterIdentifier(expression.callee.name)) continue;
|
|
@@ -32573,25 +33459,60 @@ const resolveSettings$16 = (settings) => {
|
|
|
32573
33459
|
};
|
|
32574
33460
|
const isHocCall = (call, scopes) => {
|
|
32575
33461
|
if (!isNodeOfType(call, "CallExpression")) return false;
|
|
32576
|
-
|
|
32577
|
-
|
|
33462
|
+
if (isReactApiCall(call, REACT_HOC_NAMES, scopes, {
|
|
33463
|
+
allowGlobalReactNamespace: true,
|
|
33464
|
+
allowUnboundBareCalls: true
|
|
33465
|
+
})) return true;
|
|
33466
|
+
if (isReactHocMemberReference(call.callee, scopes)) return true;
|
|
32578
33467
|
if (!isNodeOfType(call.callee, "Identifier")) return false;
|
|
32579
33468
|
const symbol = scopes.symbolFor(call.callee);
|
|
32580
33469
|
if (!symbol) return false;
|
|
32581
|
-
return symbolMapsToHoc(symbol);
|
|
33470
|
+
return symbolMapsToHoc(symbol, scopes, /* @__PURE__ */ new Set());
|
|
33471
|
+
};
|
|
33472
|
+
const isReactImportEquals = (symbol) => {
|
|
33473
|
+
if (symbol.kind !== "ts-import-equals" || !isNodeOfType(symbol.declarationNode, "TSImportEqualsDeclaration")) return false;
|
|
33474
|
+
const moduleReference = symbol.declarationNode.moduleReference;
|
|
33475
|
+
return Boolean(isNodeOfType(moduleReference, "TSExternalModuleReference") && isNodeOfType(moduleReference.expression, "Literal") && typeof moduleReference.expression.value === "string" && REACT_RUNTIME_MODULE_SOURCES.has(moduleReference.expression.value));
|
|
32582
33476
|
};
|
|
32583
|
-
const
|
|
32584
|
-
if (
|
|
32585
|
-
|
|
33477
|
+
const isRequireReactCall$1 = (node, scopes) => {
|
|
33478
|
+
if (!isNodeOfType(node, "CallExpression") || !isNodeOfType(node.callee, "Identifier") || node.callee.name !== "require" || !scopes.isGlobalReference(node.callee)) return false;
|
|
33479
|
+
const moduleSpecifier = node.arguments[0];
|
|
33480
|
+
return Boolean(moduleSpecifier && isNodeOfType(moduleSpecifier, "Literal") && typeof moduleSpecifier.value === "string" && REACT_RUNTIME_MODULE_SOURCES.has(moduleSpecifier.value));
|
|
33481
|
+
};
|
|
33482
|
+
const isReactNamespaceExpression = (node, scopes) => {
|
|
33483
|
+
if (isRequireReactCall$1(node, scopes)) return true;
|
|
33484
|
+
if (!isNodeOfType(node, "Identifier")) return false;
|
|
33485
|
+
const symbol = scopes.symbolFor(node);
|
|
33486
|
+
if (!symbol) return node.name === "React" && scopes.isGlobalReference(node);
|
|
33487
|
+
if (symbol.initializer && isRequireReactCall$1(symbol.initializer, scopes)) return true;
|
|
33488
|
+
if (isReactImportEquals(symbol)) return true;
|
|
33489
|
+
return isReactNamespaceImport(node, scopes);
|
|
33490
|
+
};
|
|
33491
|
+
const isReactHocMemberReference = (node, scopes) => Boolean(isNodeOfType(node, "MemberExpression") && !node.computed && isNodeOfType(node.property, "Identifier") && REACT_HOC_NAMES.has(node.property.name) && isReactNamespaceExpression(node.object, scopes));
|
|
33492
|
+
const getDestructuredPropertyName$1 = (symbol) => {
|
|
33493
|
+
const property = symbol.bindingIdentifier.parent;
|
|
33494
|
+
if (!property || !isNodeOfType(property, "Property") || !property.parent || !isNodeOfType(property.parent, "ObjectPattern")) return null;
|
|
33495
|
+
if (isNodeOfType(property.key, "Identifier") && !property.computed) return property.key.name;
|
|
33496
|
+
if (isNodeOfType(property.key, "Literal") && typeof property.key.value === "string") return property.key.value;
|
|
33497
|
+
return null;
|
|
33498
|
+
};
|
|
33499
|
+
const symbolMapsToHoc = (symbol, scopes, visitedSymbolIds) => {
|
|
33500
|
+
if (visitedSymbolIds.has(symbol.id)) return false;
|
|
33501
|
+
visitedSymbolIds.add(symbol.id);
|
|
33502
|
+
if (symbol.kind === "import") {
|
|
33503
|
+
const importedName = getImportedName(symbol.declarationNode);
|
|
33504
|
+
return Boolean(isImportedFromReact(symbol) && importedName && REACT_HOC_NAMES.has(importedName));
|
|
32586
33505
|
}
|
|
32587
33506
|
const init = symbol.initializer;
|
|
32588
33507
|
if (!init) return false;
|
|
32589
|
-
|
|
32590
|
-
|
|
32591
|
-
|
|
33508
|
+
const destructuredPropertyName = getDestructuredPropertyName$1(symbol);
|
|
33509
|
+
if (destructuredPropertyName && REACT_HOC_NAMES.has(destructuredPropertyName) && isReactNamespaceExpression(init, scopes)) return true;
|
|
33510
|
+
if (isReactHocMemberReference(init, scopes)) return true;
|
|
33511
|
+
if (isNodeOfType(init, "Identifier")) {
|
|
33512
|
+
const initializedFromSymbol = scopes.symbolFor(init);
|
|
33513
|
+
if (initializedFromSymbol) return symbolMapsToHoc(initializedFromSymbol, scopes, visitedSymbolIds);
|
|
33514
|
+
return REACT_HOC_NAMES.has(init.name) && scopes.isGlobalReference(init);
|
|
32592
33515
|
}
|
|
32593
|
-
if (isNodeOfType(init, "Identifier") && REACT_HOC_NAMES.has(init.name)) return true;
|
|
32594
|
-
if (REACT_HOC_NAMES.has(symbol.name) && isNodeOfType(init, "Identifier") && init.name === "React") return true;
|
|
32595
33516
|
return false;
|
|
32596
33517
|
};
|
|
32597
33518
|
const isTrivialPassthroughChild = (child) => {
|
|
@@ -32649,26 +33570,14 @@ const isHocComponent = (call, scopes) => {
|
|
|
32649
33570
|
};
|
|
32650
33571
|
const containsJsx = (root) => {
|
|
32651
33572
|
let found = false;
|
|
32652
|
-
|
|
32653
|
-
if (found) return;
|
|
33573
|
+
walkAst(root, (node) => {
|
|
33574
|
+
if (found) return false;
|
|
32654
33575
|
if (node.type === "JSXElement" || node.type === "JSXFragment") {
|
|
32655
33576
|
found = true;
|
|
32656
|
-
return;
|
|
32657
|
-
}
|
|
32658
|
-
if (node !== root) {
|
|
32659
|
-
if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression" || node.type === "ClassDeclaration" || node.type === "ClassExpression") return;
|
|
32660
|
-
}
|
|
32661
|
-
const record = node;
|
|
32662
|
-
for (const key of Object.keys(record)) {
|
|
32663
|
-
if (key === "parent") continue;
|
|
32664
|
-
const child = record[key];
|
|
32665
|
-
if (Array.isArray(child)) {
|
|
32666
|
-
for (const item of child) if (isAstNode(item)) visit(item);
|
|
32667
|
-
} else if (isAstNode(child)) visit(child);
|
|
32668
|
-
if (found) return;
|
|
33577
|
+
return false;
|
|
32669
33578
|
}
|
|
32670
|
-
|
|
32671
|
-
|
|
33579
|
+
if (node !== root && (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression" || node.type === "ClassDeclaration" || node.type === "ClassExpression")) return false;
|
|
33580
|
+
});
|
|
32672
33581
|
return found;
|
|
32673
33582
|
};
|
|
32674
33583
|
const expressionContainsJsx = (expression) => {
|
|
@@ -32692,7 +33601,7 @@ const unwrapTsCast = (expression) => {
|
|
|
32692
33601
|
while (current.type === "TSAsExpression" || current.type === "TSSatisfiesExpression" || current.type === "TSNonNullExpression") current = stripParenExpression(current.expression);
|
|
32693
33602
|
return current;
|
|
32694
33603
|
};
|
|
32695
|
-
const collectReExportedNames = (program) => {
|
|
33604
|
+
const collectReExportedNames = (program, scopes) => {
|
|
32696
33605
|
const names = /* @__PURE__ */ new Set();
|
|
32697
33606
|
if (!isNodeOfType(program, "Program")) return names;
|
|
32698
33607
|
for (const statement of program.body) {
|
|
@@ -32716,8 +33625,7 @@ const collectReExportedNames = (program) => {
|
|
|
32716
33625
|
if (!declarator.init) continue;
|
|
32717
33626
|
const init = unwrapTsCast(declarator.init);
|
|
32718
33627
|
if (isNodeOfType(init, "CallExpression")) {
|
|
32719
|
-
|
|
32720
|
-
if (calleeName && REACT_HOC_NAMES.has(calleeName)) {
|
|
33628
|
+
if (isHocCall(init, scopes)) {
|
|
32721
33629
|
const wrappedArg = init.arguments[0];
|
|
32722
33630
|
if (wrappedArg && isNodeOfType(wrappedArg, "Identifier")) names.add(wrappedArg.name);
|
|
32723
33631
|
}
|
|
@@ -32786,16 +33694,7 @@ const recordComponent = (context, name, reportNode, isStateless) => {
|
|
|
32786
33694
|
isStateless
|
|
32787
33695
|
});
|
|
32788
33696
|
};
|
|
32789
|
-
const walkChildren = (node, context) =>
|
|
32790
|
-
const record = node;
|
|
32791
|
-
for (const key of Object.keys(record)) {
|
|
32792
|
-
if (key === "parent") continue;
|
|
32793
|
-
const child = record[key];
|
|
32794
|
-
if (Array.isArray(child)) {
|
|
32795
|
-
for (const item of child) if (isAstNode(item)) walkComponentSearch(item, context);
|
|
32796
|
-
} else if (isAstNode(child)) walkComponentSearch(child, context);
|
|
32797
|
-
}
|
|
32798
|
-
};
|
|
33697
|
+
const walkChildren = (node, context) => forEachChildNode(node, context.visitChild);
|
|
32799
33698
|
const walkComponentSearch = (node, context) => {
|
|
32800
33699
|
if (isNodeOfType(node, "ClassDeclaration") || isNodeOfType(node, "ClassExpression")) {
|
|
32801
33700
|
if (isEs6Component(node)) {
|
|
@@ -32897,12 +33796,13 @@ const noMultiComp = defineRule({
|
|
|
32897
33796
|
components: [],
|
|
32898
33797
|
componentDepth: 0,
|
|
32899
33798
|
currentVarName: null,
|
|
32900
|
-
scopes: context.scopes
|
|
33799
|
+
scopes: context.scopes,
|
|
33800
|
+
visitChild: (child) => walkComponentSearch(child, visitContext)
|
|
32901
33801
|
};
|
|
32902
33802
|
for (const statement of node.body) walkComponentSearch(statement, visitContext);
|
|
32903
33803
|
const flagged = settings.ignoreStateless ? visitContext.components.filter((component) => !component.isStateless) : visitContext.components;
|
|
32904
33804
|
if (flagged.length <= 2) return;
|
|
32905
|
-
const reExportedNames = collectReExportedNames(node);
|
|
33805
|
+
const reExportedNames = collectReExportedNames(node, context.scopes);
|
|
32906
33806
|
const exportedCount = flagged.filter((component) => isExportedDeclaration(component.reportNode, reExportedNames)).length;
|
|
32907
33807
|
if (exportedCount >= flagged.length || flagged.length >= 4 && exportedCount >= Math.floor(flagged.length * .7) || flagged.length >= 8 && exportedCount >= Math.floor(flagged.length * .5)) return;
|
|
32908
33808
|
const isSmallFeatureModule = exportedCount > 0 && exportedCount <= 2 && exportedCount < flagged.length;
|
|
@@ -34294,19 +35194,6 @@ const DATA_SINK_METHOD_NAMES = new Set([
|
|
|
34294
35194
|
"deserialize"
|
|
34295
35195
|
]);
|
|
34296
35196
|
//#endregion
|
|
34297
|
-
//#region src/plugin/utils/get-call-method-name.ts
|
|
34298
|
-
/**
|
|
34299
|
-
* Returns the static method name of a call's callee when it's a
|
|
34300
|
-
* non-computed MemberExpression (`obj.method` → `"method"`), or
|
|
34301
|
-
* `null` otherwise. Used by `no-pass-data-to-parent` and
|
|
34302
|
-
* `no-pass-live-state-to-parent` to match the receiver-bound
|
|
34303
|
-
* method-call shape against an allow/block list.
|
|
34304
|
-
*/
|
|
34305
|
-
const getCallMethodName = (callee) => {
|
|
34306
|
-
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
34307
|
-
return null;
|
|
34308
|
-
};
|
|
34309
|
-
//#endregion
|
|
34310
35197
|
//#region src/plugin/rules/state-and-effects/no-pass-data-to-parent.ts
|
|
34311
35198
|
const isUseStateIdentifier = (identifier) => {
|
|
34312
35199
|
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
@@ -34551,6 +35438,14 @@ const isHandlerBagArgument = (analysis, argument) => {
|
|
|
34551
35438
|
};
|
|
34552
35439
|
const getFunctionalUpdaterDataRefs = (analysis, updater) => getDownstreamRefs(analysis, updater).filter((updaterRef) => !updaterRef.resolved?.defs.some((def) => def.type === "Parameter" && def.node === updater));
|
|
34553
35440
|
const HOOK_NAME_PATTERN$1 = /^use[A-Z0-9]/;
|
|
35441
|
+
const EXTERNAL_SUBSCRIPTION_HOOK_NAMES = new Set([
|
|
35442
|
+
"useIntersectionObserver",
|
|
35443
|
+
"useMatchMedia",
|
|
35444
|
+
"useMediaQuery",
|
|
35445
|
+
"useResizeObserver",
|
|
35446
|
+
"useVisibility",
|
|
35447
|
+
"useWindowSize"
|
|
35448
|
+
]);
|
|
34554
35449
|
const isParentWiredHookResultRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
34555
35450
|
const node = def.node;
|
|
34556
35451
|
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
@@ -34573,6 +35468,19 @@ const isParentWiredHookCalleeRef = (analysis, ref) => {
|
|
|
34573
35468
|
if (!parent || !isNodeOfType(parent, "CallExpression") || parent.callee !== identifier) return false;
|
|
34574
35469
|
return (parent.arguments ?? []).some((hookArgument) => getDownstreamRefs(analysis, hookArgument).some((downstreamRef) => isProp(analysis, downstreamRef)));
|
|
34575
35470
|
};
|
|
35471
|
+
const isExternalSubscriptionHookRef = (ref) => {
|
|
35472
|
+
const identifier = ref.identifier;
|
|
35473
|
+
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
35474
|
+
if (EXTERNAL_SUBSCRIPTION_HOOK_NAMES.has(identifier.name) && isCalleePosition(identifier)) return true;
|
|
35475
|
+
return Boolean(ref.resolved?.defs.some((def) => {
|
|
35476
|
+
const node = def.node;
|
|
35477
|
+
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
35478
|
+
const initializer = stripParenExpression(node.init);
|
|
35479
|
+
if (!isNodeOfType(initializer, "CallExpression")) return false;
|
|
35480
|
+
const callee = stripParenExpression(initializer.callee);
|
|
35481
|
+
return isNodeOfType(callee, "Identifier") && EXTERNAL_SUBSCRIPTION_HOOK_NAMES.has(callee.name);
|
|
35482
|
+
}));
|
|
35483
|
+
};
|
|
34576
35484
|
const isImportBindingRef = (ref) => Boolean(ref.resolved?.defs.some((def) => def.type === "ImportBinding"));
|
|
34577
35485
|
const isCalleePosition = (identifier) => {
|
|
34578
35486
|
const parent = identifier.parent;
|
|
@@ -34634,10 +35542,11 @@ const noPassDataToParent = defineRule({
|
|
|
34634
35542
|
if (argumentRef && resolveToFunction(argumentRef)) return [];
|
|
34635
35543
|
}
|
|
34636
35544
|
return getDownstreamRefs(analysis, argument);
|
|
34637
|
-
}).flatMap((argumentRef) => getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
|
|
35545
|
+
}).flatMap((argumentRef) => isExternallyDrivenState(analysis, argumentRef) ? [] : getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
|
|
34638
35546
|
if (calleeNode === identifier && isWrapperHookCallbackRef(analysis, ref)) argsUpstreamRefs.push(...getArgsUpstreamRefs(analysis, ref).filter(isLeafRef));
|
|
34639
35547
|
if (!argsUpstreamRefs.some((argRef) => {
|
|
34640
35548
|
if (isUseStateIdentifier(argRef.identifier)) return false;
|
|
35549
|
+
if (isExternalSubscriptionHookRef(argRef)) return false;
|
|
34641
35550
|
if (isProp(analysis, argRef)) return false;
|
|
34642
35551
|
if (isUseRefIdentifier(argRef.identifier)) return false;
|
|
34643
35552
|
if (isRefCurrent(argRef)) return false;
|
|
@@ -34932,6 +35841,30 @@ const guardsRenderShape = (comparison) => {
|
|
|
34932
35841
|
}
|
|
34933
35842
|
return false;
|
|
34934
35843
|
};
|
|
35844
|
+
const isPropsChildrenLength = (node, scopes) => {
|
|
35845
|
+
const unwrappedNode = stripParenExpression(node);
|
|
35846
|
+
return isNodeOfType(unwrappedNode, "MemberExpression") && !unwrappedNode.computed && isNodeOfType(unwrappedNode.property, "Identifier") && unwrappedNode.property.name === "length" && resolvesToPropsChildren(stripParenExpression(unwrappedNode.object), scopes);
|
|
35847
|
+
};
|
|
35848
|
+
const isLargeTextLengthComparison = (node, scopes) => {
|
|
35849
|
+
const unwrappedNode = stripParenExpression(node);
|
|
35850
|
+
if (!isNodeOfType(unwrappedNode, "BinaryExpression")) return false;
|
|
35851
|
+
const leftIsLength = isPropsChildrenLength(unwrappedNode.left, scopes);
|
|
35852
|
+
const rightIsLength = isPropsChildrenLength(unwrappedNode.right, scopes);
|
|
35853
|
+
const thresholdNode = leftIsLength ? unwrappedNode.right : unwrappedNode.left;
|
|
35854
|
+
if (!leftIsLength && !rightIsLength || !isNodeOfType(thresholdNode, "Literal")) return false;
|
|
35855
|
+
if (typeof thresholdNode.value !== "number" || thresholdNode.value < 1e3) return false;
|
|
35856
|
+
return leftIsLength ? unwrappedNode.operator === ">" || unwrappedNode.operator === ">=" : unwrappedNode.operator === "<" || unwrappedNode.operator === "<=";
|
|
35857
|
+
};
|
|
35858
|
+
const isLargeStringOptimizationGuard = (comparison, scopes) => {
|
|
35859
|
+
let current = findTransparentExpressionRoot(comparison);
|
|
35860
|
+
while (current.parent) {
|
|
35861
|
+
const parent = current.parent;
|
|
35862
|
+
if (!isNodeOfType(parent, "LogicalExpression") || parent.operator !== "&&") return false;
|
|
35863
|
+
if (isLargeTextLengthComparison(parent.left === current ? parent.right : parent.left, scopes)) return true;
|
|
35864
|
+
current = findTransparentExpressionRoot(parent);
|
|
35865
|
+
}
|
|
35866
|
+
return false;
|
|
35867
|
+
};
|
|
34935
35868
|
const noPolymorphicChildren = defineRule({
|
|
34936
35869
|
id: "no-polymorphic-children",
|
|
34937
35870
|
title: "Children type checked at runtime",
|
|
@@ -34945,6 +35878,7 @@ const noPolymorphicChildren = defineRule({
|
|
|
34945
35878
|
const isStringLiteral = (operand) => isNodeOfType(operand, "Literal") && operand.value === "string";
|
|
34946
35879
|
if (!isStringLiteral(node.left) && !isStringLiteral(node.right)) return;
|
|
34947
35880
|
if (!guardsRenderShape(node)) return;
|
|
35881
|
+
if (isLargeStringOptimizationGuard(node, context.scopes)) return;
|
|
34948
35882
|
context.report({
|
|
34949
35883
|
node,
|
|
34950
35884
|
message: "Your users hit inconsistent behavior because `typeof children === \"string\"` makes this component switch on what callers pass, so add clear subcomponents like `<Button.Text>` instead."
|
|
@@ -35096,7 +36030,7 @@ const noPreventDefault = defineRule({
|
|
|
35096
36030
|
const isServerActionsFramework = hasCapability(context.settings, "server-actions");
|
|
35097
36031
|
const formMessage = isServerActionsFramework ? FORM_MESSAGE_SERVER_CAPABLE : FORM_MESSAGE_GENERIC;
|
|
35098
36032
|
return { JSXOpeningElement(node) {
|
|
35099
|
-
const elementName =
|
|
36033
|
+
const elementName = resolveJsxElementType(node);
|
|
35100
36034
|
if (!elementName) return;
|
|
35101
36035
|
const targetEventProps = PREVENT_DEFAULT_ELEMENTS.get(elementName);
|
|
35102
36036
|
if (!targetEventProps) return;
|
|
@@ -35162,6 +36096,27 @@ const hasPreviousValueDep = (effectNode, depElements) => {
|
|
|
35162
36096
|
}
|
|
35163
36097
|
return false;
|
|
35164
36098
|
};
|
|
36099
|
+
const isStateLikeDependency = (analysis, element, isPropName) => {
|
|
36100
|
+
if (!isNodeOfType(element, "Identifier") || isPropName(element.name)) return false;
|
|
36101
|
+
if (!analysis) return true;
|
|
36102
|
+
const reference = getRef(analysis, element);
|
|
36103
|
+
if (!reference) return true;
|
|
36104
|
+
const upstreamReferences = getUpstreamRefs(analysis, reference);
|
|
36105
|
+
if (upstreamReferences.some((upstreamReference) => isState(analysis, upstreamReference))) return true;
|
|
36106
|
+
return !upstreamReferences.some((upstreamReference) => isProp(analysis, upstreamReference));
|
|
36107
|
+
};
|
|
36108
|
+
const getRefHeldPropCallbackName = (callExpression, isPropName) => {
|
|
36109
|
+
const callee = stripParenExpression(callExpression.callee);
|
|
36110
|
+
if (!isNodeOfType(callee, "MemberExpression") || !isNodeOfType(callee.property, "Identifier") || callee.property.name !== "current") return null;
|
|
36111
|
+
const receiver = stripParenExpression(callee.object);
|
|
36112
|
+
if (!isNodeOfType(receiver, "Identifier")) return null;
|
|
36113
|
+
const binding = findVariableInitializer(callExpression, receiver.name);
|
|
36114
|
+
if (!binding?.initializer || !isNodeOfType(binding.initializer, "CallExpression")) return null;
|
|
36115
|
+
if (getCalleeName$2(binding.initializer) !== "useRef") return null;
|
|
36116
|
+
const callbackArgument = binding.initializer.arguments?.[0];
|
|
36117
|
+
if (!callbackArgument || !isNodeOfType(callbackArgument, "Identifier")) return null;
|
|
36118
|
+
return isPropName(callbackArgument.name) ? callbackArgument.name : null;
|
|
36119
|
+
};
|
|
35165
36120
|
const noPropCallbackInEffect = defineRule({
|
|
35166
36121
|
id: "no-prop-callback-in-effect",
|
|
35167
36122
|
title: "Parent kept in sync with a callback effect",
|
|
@@ -35178,11 +36133,11 @@ const noPropCallbackInEffect = defineRule({
|
|
|
35178
36133
|
if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
|
|
35179
36134
|
const depsNode = node.arguments[1];
|
|
35180
36135
|
if (!isNodeOfType(depsNode, "ArrayExpression") || !depsNode.elements?.length) return;
|
|
35181
|
-
const
|
|
36136
|
+
const analysis = getProgramAnalysis(node);
|
|
36137
|
+
const stateLikeDeps = (depsNode.elements ?? []).filter((element) => element && isStateLikeDependency(analysis, element, propStackTracker.isPropName));
|
|
35182
36138
|
if (stateLikeDeps.length === 0) return;
|
|
35183
36139
|
if (isRefLatchGuardedEffect(callback.body)) return;
|
|
35184
36140
|
if (hasPreviousValueDep(node, depsNode.elements ?? [])) return;
|
|
35185
|
-
const analysis = getProgramAnalysis(node);
|
|
35186
36141
|
if (analysis) {
|
|
35187
36142
|
const stateLikeDepRefs = [];
|
|
35188
36143
|
for (const element of stateLikeDeps) {
|
|
@@ -35194,9 +36149,9 @@ const noPropCallbackInEffect = defineRule({
|
|
|
35194
36149
|
const reportedNodes = /* @__PURE__ */ new Set();
|
|
35195
36150
|
walkInsideStatementBlocks(callback.body, (child) => {
|
|
35196
36151
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
35197
|
-
|
|
35198
|
-
const calleeName =
|
|
35199
|
-
if (!
|
|
36152
|
+
const directCallee = stripParenExpression(child.callee);
|
|
36153
|
+
const calleeName = isNodeOfType(directCallee, "Identifier") && propStackTracker.isPropName(directCallee.name) && directCallee.name || getRefHeldPropCallbackName(child, propStackTracker.isPropName);
|
|
36154
|
+
if (!calleeName) return;
|
|
35200
36155
|
if (!isResultDiscardedCall(child)) return;
|
|
35201
36156
|
if (reportedNodes.has(child)) return;
|
|
35202
36157
|
reportedNodes.add(child);
|
|
@@ -36560,7 +37515,7 @@ const isNonSettlingSetterArgument = (setterCall, stateName) => {
|
|
|
36560
37515
|
return expressionReadsStateValue(argument, stateName);
|
|
36561
37516
|
};
|
|
36562
37517
|
const getUnconditionalSetterCall = (statement, setterNames) => {
|
|
36563
|
-
const expression =
|
|
37518
|
+
const expression = unwrapDiscardedExpression(statement);
|
|
36564
37519
|
if (!isNodeOfType(expression, "CallExpression")) return null;
|
|
36565
37520
|
if (!isNodeOfType(expression.callee, "Identifier")) return null;
|
|
36566
37521
|
if (!setterNames.has(expression.callee.name)) return null;
|
|
@@ -36909,7 +37864,7 @@ const noSelfUpdatingEffect = defineRule({
|
|
|
36909
37864
|
const setterNames = new Set(setterNameToStateName.keys());
|
|
36910
37865
|
for (const statement of functionBody.body ?? []) {
|
|
36911
37866
|
if (!isNodeOfType(statement, "ExpressionStatement")) continue;
|
|
36912
|
-
const effectCall = statement
|
|
37867
|
+
const effectCall = unwrapDiscardedExpression(statement);
|
|
36913
37868
|
if (!isNodeOfType(effectCall, "CallExpression")) continue;
|
|
36914
37869
|
if (!isHookCall$2(effectCall, EFFECT_HOOK_NAMES$1)) continue;
|
|
36915
37870
|
if ((effectCall.arguments?.length ?? 0) < 2) continue;
|
|
@@ -37539,9 +38494,10 @@ const noStringFalseOnBooleanAttribute = defineRule({
|
|
|
37539
38494
|
recommendation: "Use the boolean form on boolean attributes: `disabled` / `disabled={true}` / `disabled={false}`, not `disabled=\"false\"`. A non-empty string is truthy, so `=\"false\"` actually turns the attribute ON.",
|
|
37540
38495
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
37541
38496
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
37542
|
-
const
|
|
38497
|
+
const elementName = resolveJsxElementType(node);
|
|
38498
|
+
const firstCharacter = elementName.charCodeAt(0);
|
|
37543
38499
|
if (firstCharacter < 97 || firstCharacter > 122) return;
|
|
37544
|
-
if (
|
|
38500
|
+
if (elementName.includes("-")) return;
|
|
37545
38501
|
for (const attribute of node.attributes) {
|
|
37546
38502
|
if (!isNodeOfType(attribute, "JSXAttribute")) continue;
|
|
37547
38503
|
if (!isNodeOfType(attribute.name, "JSXIdentifier")) continue;
|
|
@@ -37929,8 +38885,7 @@ const noUncontrolledInput = defineRule({
|
|
|
37929
38885
|
const undefinedInitialStateNames = isNodeOfType(componentBody, "BlockStatement") ? collectUndefinedInitialStateNames(componentBody) : /* @__PURE__ */ new Set();
|
|
37930
38886
|
walkAst(componentBody, (child) => {
|
|
37931
38887
|
if (!isNodeOfType(child, "JSXOpeningElement")) return;
|
|
37932
|
-
|
|
37933
|
-
const tagName = child.name.name;
|
|
38888
|
+
const tagName = resolveJsxElementType(child);
|
|
37934
38889
|
if (!UNCONTROLLED_INPUT_TAGS.has(tagName)) return;
|
|
37935
38890
|
const attributes = child.attributes ?? [];
|
|
37936
38891
|
if (hasJsxSpreadAttribute(attributes)) return;
|
|
@@ -37991,7 +38946,7 @@ const noUndeferredThirdParty = defineRule({
|
|
|
37991
38946
|
severity: "warn",
|
|
37992
38947
|
recommendation: "Use `next/script` with `strategy=\"lazyOnload\"`, or add the `defer` attribute.",
|
|
37993
38948
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
37994
|
-
if (
|
|
38949
|
+
if (resolveJsxElementType(node) !== "script") return;
|
|
37995
38950
|
const attributes = node.attributes ?? [];
|
|
37996
38951
|
const srcAttribute = findJsxAttribute(attributes, "src");
|
|
37997
38952
|
if (!srcAttribute) return;
|
|
@@ -39206,7 +40161,7 @@ const noUnknownProperty = defineRule({
|
|
|
39206
40161
|
}
|
|
39207
40162
|
if (fileIsNonReactJsx) return;
|
|
39208
40163
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
39209
|
-
const elementType = node
|
|
40164
|
+
const elementType = resolveJsxElementType(node);
|
|
39210
40165
|
const firstCharacter = elementType.charCodeAt(0);
|
|
39211
40166
|
if (!(firstCharacter >= 97 && firstCharacter <= 122) || elementType === "fbt" || elementType === "fbs") return;
|
|
39212
40167
|
let isValidHtmlTag = isKnownDomTag(elementType);
|
|
@@ -39370,50 +40325,35 @@ const resolveSettings$8 = (settings) => {
|
|
|
39370
40325
|
};
|
|
39371
40326
|
const expressionContainsJsxOrCreateElement = (root) => {
|
|
39372
40327
|
let found = false;
|
|
39373
|
-
|
|
39374
|
-
if (found) return;
|
|
40328
|
+
walkAst(root, (node) => {
|
|
40329
|
+
if (found) return false;
|
|
40330
|
+
if (node !== root && NESTED_FUNCTION_TYPES.has(node.type)) return false;
|
|
39375
40331
|
if (node.type === "JSXElement" || node.type === "JSXFragment") {
|
|
39376
40332
|
found = true;
|
|
39377
|
-
return;
|
|
40333
|
+
return false;
|
|
39378
40334
|
}
|
|
39379
40335
|
if (isNodeOfType(node, "CallExpression") && isCreateElementCall(node)) {
|
|
39380
40336
|
found = true;
|
|
39381
|
-
return;
|
|
39382
|
-
}
|
|
39383
|
-
const record = node;
|
|
39384
|
-
for (const key of Object.keys(record)) {
|
|
39385
|
-
if (key === "parent") continue;
|
|
39386
|
-
const child = record[key];
|
|
39387
|
-
if (Array.isArray(child)) for (const item of child) {
|
|
39388
|
-
if (!isAstNode(item)) continue;
|
|
39389
|
-
if (NESTED_FUNCTION_TYPES.has(item.type)) continue;
|
|
39390
|
-
visit(item);
|
|
39391
|
-
if (found) return;
|
|
39392
|
-
}
|
|
39393
|
-
else if (isAstNode(child)) {
|
|
39394
|
-
if (NESTED_FUNCTION_TYPES.has(child.type)) continue;
|
|
39395
|
-
visit(child);
|
|
39396
|
-
}
|
|
39397
|
-
if (found) return;
|
|
40337
|
+
return false;
|
|
39398
40338
|
}
|
|
39399
|
-
};
|
|
39400
|
-
visit(root);
|
|
40339
|
+
});
|
|
39401
40340
|
return found;
|
|
39402
40341
|
};
|
|
40342
|
+
const functionContainsJsxOrCreateElement = (functionNode, scopes) => expressionContainsJsxOrCreateElement(functionNode) || functionReturnsMatchingExpression(functionNode, scopes, expressionContainsJsxOrCreateElement);
|
|
39403
40343
|
const isReactClassComponent = (classNode) => {
|
|
39404
40344
|
if (isEs6Component(classNode)) return true;
|
|
39405
40345
|
return expressionContainsJsxOrCreateElement(classNode);
|
|
39406
40346
|
};
|
|
39407
|
-
const findEnclosingComponent = (node) => {
|
|
40347
|
+
const findEnclosingComponent = (node, scopes) => {
|
|
39408
40348
|
let walker = node.parent;
|
|
39409
40349
|
while (walker) {
|
|
39410
40350
|
if (isFunctionLike$1(walker)) {
|
|
39411
40351
|
const componentName = inferFunctionLikeName(walker);
|
|
39412
|
-
if (componentName && isReactComponentName(componentName) &&
|
|
40352
|
+
if (componentName && isReactComponentName(componentName) && functionContainsJsxOrCreateElement(walker, scopes)) return {
|
|
39413
40353
|
component: walker,
|
|
39414
40354
|
name: componentName
|
|
39415
40355
|
};
|
|
39416
|
-
if (!componentName &&
|
|
40356
|
+
if (!componentName && functionContainsJsxOrCreateElement(walker, scopes) && walker.parent && isNodeOfType(walker.parent, "ExportDefaultDeclaration")) return {
|
|
39417
40357
|
component: walker,
|
|
39418
40358
|
name: null
|
|
39419
40359
|
};
|
|
@@ -39652,7 +40592,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
39652
40592
|
if (renderPropRegex.test(propInfo.propName)) return;
|
|
39653
40593
|
if (settings.allowAsProps) return;
|
|
39654
40594
|
}
|
|
39655
|
-
const enclosing = findEnclosingComponent(candidateNode);
|
|
40595
|
+
const enclosing = findEnclosingComponent(candidateNode, context.scopes);
|
|
39656
40596
|
if (!enclosing) return;
|
|
39657
40597
|
const gatedName = propInfo ? null : requiredInstantiationName;
|
|
39658
40598
|
queuedReports.push({
|
|
@@ -39663,7 +40603,7 @@ const noUnstableNestedComponents = defineRule({
|
|
|
39663
40603
|
});
|
|
39664
40604
|
};
|
|
39665
40605
|
const checkFunctionLike = (node) => {
|
|
39666
|
-
if (!
|
|
40606
|
+
if (!functionContainsJsxOrCreateElement(node, context.scopes)) return;
|
|
39667
40607
|
const inferredName = inferFunctionLikeName(node);
|
|
39668
40608
|
const propInfo = isComponentDeclaredInProp(node);
|
|
39669
40609
|
const isObjectCallback = isObjectCallbackCandidate(node);
|
|
@@ -40323,19 +41263,6 @@ const classifyExport = (name, reportNode, isFunction, initializer, state) => {
|
|
|
40323
41263
|
reportNode
|
|
40324
41264
|
};
|
|
40325
41265
|
};
|
|
40326
|
-
const collectRelevantNodes = (programRoot) => {
|
|
40327
|
-
const exportNodes = [];
|
|
40328
|
-
const componentCandidates = [];
|
|
40329
|
-
walkAst(programRoot, (child) => {
|
|
40330
|
-
const childType = child.type;
|
|
40331
|
-
if (childType === "ExportAllDeclaration" || childType === "ExportDefaultDeclaration" || childType === "ExportNamedDeclaration") exportNodes.push(child);
|
|
40332
|
-
else if (childType === "FunctionDeclaration" || childType === "VariableDeclarator" || childType === "ClassDeclaration") componentCandidates.push(child);
|
|
40333
|
-
});
|
|
40334
|
-
return {
|
|
40335
|
-
exportNodes,
|
|
40336
|
-
componentCandidates
|
|
40337
|
-
};
|
|
40338
|
-
};
|
|
40339
41266
|
const isEntryPointFile = (filename) => {
|
|
40340
41267
|
const lastSlash = Math.max(filename.lastIndexOf("/"), filename.lastIndexOf("\\"));
|
|
40341
41268
|
const basename = lastSlash === -1 ? filename : filename.slice(lastSlash + 1);
|
|
@@ -40374,197 +41301,212 @@ const onlyExportComponents = defineRule({
|
|
|
40374
41301
|
category: "Architecture",
|
|
40375
41302
|
create: (context) => {
|
|
40376
41303
|
const settings = resolveSettings$6(context.settings);
|
|
40377
|
-
|
|
40378
|
-
|
|
40379
|
-
|
|
40380
|
-
|
|
40381
|
-
|
|
40382
|
-
|
|
40383
|
-
|
|
40384
|
-
|
|
40385
|
-
|
|
40386
|
-
|
|
40387
|
-
|
|
40388
|
-
|
|
40389
|
-
|
|
40390
|
-
|
|
40391
|
-
|
|
40392
|
-
|
|
40393
|
-
|
|
40394
|
-
|
|
40395
|
-
|
|
40396
|
-
|
|
40397
|
-
|
|
40398
|
-
|
|
40399
|
-
|
|
41304
|
+
if (!isFileNameAllowed(normalizeFilename(context.filename ?? ""), settings.checkJS)) return {};
|
|
41305
|
+
const exportNodes = [];
|
|
41306
|
+
const componentCandidates = [];
|
|
41307
|
+
const pushExportNode = (node) => {
|
|
41308
|
+
exportNodes.push(node);
|
|
41309
|
+
};
|
|
41310
|
+
const pushComponentCandidate = (node) => {
|
|
41311
|
+
componentCandidates.push(node);
|
|
41312
|
+
};
|
|
41313
|
+
return {
|
|
41314
|
+
ExportAllDeclaration: pushExportNode,
|
|
41315
|
+
ExportDefaultDeclaration: pushExportNode,
|
|
41316
|
+
ExportNamedDeclaration: pushExportNode,
|
|
41317
|
+
FunctionDeclaration: pushComponentCandidate,
|
|
41318
|
+
VariableDeclarator: pushComponentCandidate,
|
|
41319
|
+
ClassDeclaration: pushComponentCandidate,
|
|
41320
|
+
"Program:exit"() {
|
|
41321
|
+
const localComponentNames = /* @__PURE__ */ new Set();
|
|
41322
|
+
const state = {
|
|
41323
|
+
customHocs: new Set([...DEFAULT_REACT_HOCS, ...settings.customHOCs]),
|
|
41324
|
+
allowExportNames: new Set(settings.allowExportNames),
|
|
41325
|
+
allowConstantExport: settings.allowConstantExport,
|
|
41326
|
+
localComponentNames,
|
|
41327
|
+
scopes: context.scopes
|
|
41328
|
+
};
|
|
41329
|
+
for (const child of componentCandidates) {
|
|
41330
|
+
if (isNodeOfType(child, "FunctionDeclaration") && child.id) {
|
|
41331
|
+
if (isReactComponentName(child.id.name) && !isInsideFunctionScope(child) && functionContainsReactRenderOutput(child, context.scopes)) localComponentNames.add(child.id.name);
|
|
41332
|
+
}
|
|
41333
|
+
if (isNodeOfType(child, "ClassDeclaration") && child.id) {
|
|
41334
|
+
if (isReactComponentName(child.id.name) && isEs6Component(child) && !isInsideFunctionScope(child)) localComponentNames.add(child.id.name);
|
|
41335
|
+
}
|
|
41336
|
+
if (isNodeOfType(child, "VariableDeclarator") && isNodeOfType(child.id, "Identifier")) {
|
|
41337
|
+
const initializer = child.init;
|
|
41338
|
+
if (isReactComponentName(child.id.name) && (canBeReactFunctionComponent(initializer, state) || (initializer ? isEs6Component(skipTsExpression(initializer)) : false)) && !isInsideFunctionScope(child)) {
|
|
41339
|
+
const expression = initializer ? skipTsExpression(initializer) : null;
|
|
41340
|
+
if (!(expression !== null && (isNodeOfType(expression, "ArrowFunctionExpression") || isNodeOfType(expression, "FunctionExpression"))) || functionContainsReactRenderOutput(expression, context.scopes)) localComponentNames.add(child.id.name);
|
|
41341
|
+
}
|
|
40400
41342
|
}
|
|
40401
41343
|
}
|
|
40402
|
-
|
|
40403
|
-
|
|
40404
|
-
|
|
40405
|
-
|
|
40406
|
-
|
|
40407
|
-
|
|
40408
|
-
|
|
40409
|
-
|
|
40410
|
-
|
|
40411
|
-
|
|
40412
|
-
|
|
40413
|
-
|
|
40414
|
-
|
|
40415
|
-
|
|
40416
|
-
|
|
40417
|
-
|
|
40418
|
-
|
|
40419
|
-
|
|
40420
|
-
|
|
40421
|
-
|
|
40422
|
-
|
|
40423
|
-
|
|
40424
|
-
|
|
40425
|
-
|
|
40426
|
-
|
|
40427
|
-
|
|
41344
|
+
const exports = [];
|
|
41345
|
+
let hasReactExport = false;
|
|
41346
|
+
let hasAnyExports = false;
|
|
41347
|
+
const isExportedNodeIds = /* @__PURE__ */ new WeakSet();
|
|
41348
|
+
for (const child of exportNodes) {
|
|
41349
|
+
if (isNodeOfType(child, "ExportAllDeclaration")) {
|
|
41350
|
+
if (child.exportKind === "type") continue;
|
|
41351
|
+
hasAnyExports = true;
|
|
41352
|
+
context.report({
|
|
41353
|
+
node: child,
|
|
41354
|
+
message: EXPORT_ALL_MESSAGE
|
|
41355
|
+
});
|
|
41356
|
+
continue;
|
|
41357
|
+
}
|
|
41358
|
+
if (isNodeOfType(child, "ExportDefaultDeclaration")) {
|
|
41359
|
+
hasAnyExports = true;
|
|
41360
|
+
const declaration = child.declaration;
|
|
41361
|
+
const stripped = skipTsExpression(declaration);
|
|
41362
|
+
if (isNodeOfType(stripped, "FunctionDeclaration") || isNodeOfType(stripped, "FunctionExpression")) {
|
|
41363
|
+
if (stripped.id) {
|
|
41364
|
+
const idNode = stripped.id;
|
|
41365
|
+
isExportedNodeIds.add(stripped);
|
|
41366
|
+
exports.push(classifyExport(idNode.name, idNode, true, null, state));
|
|
41367
|
+
} else {
|
|
41368
|
+
context.report({
|
|
41369
|
+
node: stripped,
|
|
41370
|
+
message: ANONYMOUS_MESSAGE
|
|
41371
|
+
});
|
|
41372
|
+
hasReactExport = true;
|
|
41373
|
+
}
|
|
41374
|
+
continue;
|
|
41375
|
+
}
|
|
41376
|
+
if (isNodeOfType(stripped, "ClassDeclaration") || isNodeOfType(stripped, "ClassExpression")) {
|
|
41377
|
+
if (stripped.id) {
|
|
41378
|
+
const idNode = stripped.id;
|
|
41379
|
+
isExportedNodeIds.add(stripped);
|
|
41380
|
+
if (isReactComponentName(idNode.name) && isEs6Component(stripped)) hasReactExport = true;
|
|
41381
|
+
else exports.push({
|
|
41382
|
+
kind: "non-component",
|
|
41383
|
+
reportNode: idNode
|
|
41384
|
+
});
|
|
41385
|
+
} else context.report({
|
|
40428
41386
|
node: stripped,
|
|
40429
41387
|
message: ANONYMOUS_MESSAGE
|
|
40430
41388
|
});
|
|
40431
|
-
|
|
41389
|
+
continue;
|
|
40432
41390
|
}
|
|
40433
|
-
|
|
40434
|
-
|
|
40435
|
-
|
|
40436
|
-
|
|
40437
|
-
|
|
40438
|
-
|
|
40439
|
-
|
|
40440
|
-
|
|
41391
|
+
if (isNodeOfType(stripped, "Identifier")) {
|
|
41392
|
+
exports.push(classifyExport(stripped.name, stripped, false, null, state));
|
|
41393
|
+
continue;
|
|
41394
|
+
}
|
|
41395
|
+
if (isNodeOfType(stripped, "CallExpression")) {
|
|
41396
|
+
if (isRouteFactoryCall(stripped)) {
|
|
41397
|
+
hasReactExport = true;
|
|
41398
|
+
continue;
|
|
41399
|
+
}
|
|
41400
|
+
const isHoc = isHocCallee(stripped.callee, state);
|
|
41401
|
+
const firstArg = stripped.arguments[0];
|
|
41402
|
+
const firstArgIsValid = Boolean(firstArg) && (() => {
|
|
41403
|
+
if (!firstArg) return false;
|
|
41404
|
+
const expression = skipTsExpression(firstArg);
|
|
41405
|
+
if (isNodeOfType(expression, "Identifier")) return true;
|
|
41406
|
+
if (isNodeOfType(expression, "FunctionExpression") && expression.id) return true;
|
|
41407
|
+
if (isNodeOfType(expression, "CallExpression") && isHocCallee(expression.callee, state)) return true;
|
|
41408
|
+
return false;
|
|
41409
|
+
})();
|
|
41410
|
+
if (isHoc && firstArgIsValid) hasReactExport = true;
|
|
41411
|
+
else if (!isHoc && isConfigOnlyFactoryCall(stripped)) exports.push({
|
|
40441
41412
|
kind: "non-component",
|
|
40442
|
-
reportNode:
|
|
41413
|
+
reportNode: stripped
|
|
41414
|
+
});
|
|
41415
|
+
else context.report({
|
|
41416
|
+
node: stripped,
|
|
41417
|
+
message: ANONYMOUS_MESSAGE
|
|
41418
|
+
});
|
|
41419
|
+
continue;
|
|
41420
|
+
}
|
|
41421
|
+
if (isNodeOfType(stripped, "ObjectExpression")) {
|
|
41422
|
+
context.report({
|
|
41423
|
+
node: stripped,
|
|
41424
|
+
message: objectExpressionBundlesComponents(stripped, state) ? NAMESPACE_OBJECT_MESSAGE : ANONYMOUS_MESSAGE
|
|
41425
|
+
});
|
|
41426
|
+
continue;
|
|
41427
|
+
}
|
|
41428
|
+
if (isNodeOfType(stripped, "ArrowFunctionExpression") || isNodeOfType(stripped, "Literal")) {
|
|
41429
|
+
context.report({
|
|
41430
|
+
node: stripped,
|
|
41431
|
+
message: ANONYMOUS_MESSAGE
|
|
40443
41432
|
});
|
|
40444
|
-
} else context.report({
|
|
40445
|
-
node: stripped,
|
|
40446
|
-
message: ANONYMOUS_MESSAGE
|
|
40447
|
-
});
|
|
40448
|
-
continue;
|
|
40449
|
-
}
|
|
40450
|
-
if (isNodeOfType(stripped, "Identifier")) {
|
|
40451
|
-
exports.push(classifyExport(stripped.name, stripped, false, null, state));
|
|
40452
|
-
continue;
|
|
40453
|
-
}
|
|
40454
|
-
if (isNodeOfType(stripped, "CallExpression")) {
|
|
40455
|
-
if (isRouteFactoryCall(stripped)) {
|
|
40456
|
-
hasReactExport = true;
|
|
40457
41433
|
continue;
|
|
40458
41434
|
}
|
|
40459
|
-
const isHoc = isHocCallee(stripped.callee, state);
|
|
40460
|
-
const firstArg = stripped.arguments[0];
|
|
40461
|
-
const firstArgIsValid = Boolean(firstArg) && (() => {
|
|
40462
|
-
if (!firstArg) return false;
|
|
40463
|
-
const expression = skipTsExpression(firstArg);
|
|
40464
|
-
if (isNodeOfType(expression, "Identifier")) return true;
|
|
40465
|
-
if (isNodeOfType(expression, "FunctionExpression") && expression.id) return true;
|
|
40466
|
-
if (isNodeOfType(expression, "CallExpression") && isHocCallee(expression.callee, state)) return true;
|
|
40467
|
-
return false;
|
|
40468
|
-
})();
|
|
40469
|
-
if (isHoc && firstArgIsValid) hasReactExport = true;
|
|
40470
|
-
else if (!isHoc && isConfigOnlyFactoryCall(stripped)) exports.push({
|
|
40471
|
-
kind: "non-component",
|
|
40472
|
-
reportNode: stripped
|
|
40473
|
-
});
|
|
40474
|
-
else context.report({
|
|
40475
|
-
node: stripped,
|
|
40476
|
-
message: ANONYMOUS_MESSAGE
|
|
40477
|
-
});
|
|
40478
|
-
continue;
|
|
40479
|
-
}
|
|
40480
|
-
if (isNodeOfType(stripped, "ObjectExpression")) {
|
|
40481
|
-
context.report({
|
|
40482
|
-
node: stripped,
|
|
40483
|
-
message: objectExpressionBundlesComponents(stripped, state) ? NAMESPACE_OBJECT_MESSAGE : ANONYMOUS_MESSAGE
|
|
40484
|
-
});
|
|
40485
|
-
continue;
|
|
40486
|
-
}
|
|
40487
|
-
if (isNodeOfType(stripped, "ArrowFunctionExpression") || isNodeOfType(stripped, "Literal")) {
|
|
40488
41435
|
context.report({
|
|
40489
|
-
node:
|
|
41436
|
+
node: child,
|
|
40490
41437
|
message: ANONYMOUS_MESSAGE
|
|
40491
41438
|
});
|
|
40492
41439
|
continue;
|
|
40493
41440
|
}
|
|
40494
|
-
|
|
40495
|
-
|
|
40496
|
-
|
|
40497
|
-
|
|
40498
|
-
|
|
40499
|
-
|
|
40500
|
-
|
|
40501
|
-
|
|
40502
|
-
|
|
40503
|
-
|
|
40504
|
-
|
|
40505
|
-
|
|
40506
|
-
|
|
40507
|
-
|
|
40508
|
-
|
|
40509
|
-
|
|
40510
|
-
|
|
40511
|
-
|
|
40512
|
-
|
|
40513
|
-
|
|
40514
|
-
}
|
|
40515
|
-
|
|
40516
|
-
|
|
40517
|
-
|
|
40518
|
-
|
|
40519
|
-
|
|
40520
|
-
|
|
40521
|
-
else if (declaration.type === "TSEnumDeclaration" || declaration.type === "TSInterfaceDeclaration" || declaration.type === "TSTypeAliasDeclaration") {
|
|
40522
|
-
if (declaration.type === "TSEnumDeclaration") exports.push({
|
|
40523
|
-
kind: "non-component",
|
|
40524
|
-
reportNode: declaration
|
|
40525
|
-
});
|
|
41441
|
+
if (isNodeOfType(child, "ExportNamedDeclaration")) {
|
|
41442
|
+
if (child.exportKind === "type") continue;
|
|
41443
|
+
hasAnyExports = true;
|
|
41444
|
+
if (child.declaration) {
|
|
41445
|
+
const declaration = child.declaration;
|
|
41446
|
+
if (isNodeOfType(declaration, "FunctionDeclaration") && declaration.id) {
|
|
41447
|
+
isExportedNodeIds.add(declaration);
|
|
41448
|
+
exports.push(classifyExport(declaration.id.name, declaration.id, true, null, state));
|
|
41449
|
+
} else if (isNodeOfType(declaration, "ClassDeclaration") && declaration.id) {
|
|
41450
|
+
isExportedNodeIds.add(declaration);
|
|
41451
|
+
if (isReactComponentName(declaration.id.name) && isEs6Component(declaration)) exports.push({ kind: "react-component" });
|
|
41452
|
+
else exports.push({
|
|
41453
|
+
kind: "non-component",
|
|
41454
|
+
reportNode: declaration.id
|
|
41455
|
+
});
|
|
41456
|
+
} else if (isNodeOfType(declaration, "VariableDeclaration")) for (const declarator of declaration.declarations) {
|
|
41457
|
+
if (!isNodeOfType(declarator.id, "Identifier")) continue;
|
|
41458
|
+
isExportedNodeIds.add(declarator);
|
|
41459
|
+
const isFunction = canBeReactFunctionComponent(declarator.init ?? null, state);
|
|
41460
|
+
exports.push(classifyExport(declarator.id.name, declarator.id, isFunction, declarator.init, state));
|
|
41461
|
+
}
|
|
41462
|
+
else if (declaration.type === "TSEnumDeclaration" || declaration.type === "TSInterfaceDeclaration" || declaration.type === "TSTypeAliasDeclaration") {
|
|
41463
|
+
if (declaration.type === "TSEnumDeclaration") exports.push({
|
|
41464
|
+
kind: "non-component",
|
|
41465
|
+
reportNode: declaration
|
|
41466
|
+
});
|
|
41467
|
+
}
|
|
40526
41468
|
}
|
|
40527
|
-
|
|
40528
|
-
|
|
40529
|
-
|
|
40530
|
-
|
|
40531
|
-
|
|
40532
|
-
|
|
40533
|
-
|
|
40534
|
-
|
|
40535
|
-
|
|
40536
|
-
|
|
40537
|
-
|
|
40538
|
-
|
|
40539
|
-
|
|
40540
|
-
|
|
40541
|
-
|
|
40542
|
-
|
|
40543
|
-
|
|
40544
|
-
|
|
40545
|
-
|
|
41469
|
+
const isReExportFromSource = Boolean(child.source);
|
|
41470
|
+
for (const specifier of child.specifiers ?? []) {
|
|
41471
|
+
if (!isNodeOfType(specifier, "ExportSpecifier")) continue;
|
|
41472
|
+
const exported = specifier.exported;
|
|
41473
|
+
const local = specifier.local;
|
|
41474
|
+
let exportedName = null;
|
|
41475
|
+
if (exported && isNodeOfType(exported, "Identifier")) exportedName = exported.name;
|
|
41476
|
+
const localName = local && isNodeOfType(local, "Identifier") ? local.name : null;
|
|
41477
|
+
const reportNode = specifier;
|
|
41478
|
+
let entry;
|
|
41479
|
+
if (exportedName === "default" && localName) entry = classifyExport(localName, reportNode, false, null, state);
|
|
41480
|
+
else if (exportedName) entry = classifyExport(exportedName, reportNode, false, null, state);
|
|
41481
|
+
else {
|
|
41482
|
+
entry = {
|
|
41483
|
+
kind: "non-component",
|
|
41484
|
+
reportNode
|
|
41485
|
+
};
|
|
41486
|
+
if (localName && isReactComponentName(localName)) exports.push({ kind: "react-component" });
|
|
41487
|
+
}
|
|
41488
|
+
if (isReExportFromSource && entry.kind !== "react-component") continue;
|
|
41489
|
+
exports.push(entry);
|
|
40546
41490
|
}
|
|
40547
|
-
if (isReExportFromSource && entry.kind !== "react-component") continue;
|
|
40548
|
-
exports.push(entry);
|
|
40549
41491
|
}
|
|
40550
41492
|
}
|
|
40551
|
-
|
|
40552
|
-
|
|
40553
|
-
for (const entry of exports) if (entry.kind === "namespace-object") context.report({
|
|
40554
|
-
node: entry.reportNode,
|
|
40555
|
-
message: NAMESPACE_OBJECT_MESSAGE
|
|
40556
|
-
});
|
|
40557
|
-
if (hasAnyExports && hasReactExport) for (const entry of exports) {
|
|
40558
|
-
if (entry.kind === "non-component") context.report({
|
|
40559
|
-
node: entry.reportNode,
|
|
40560
|
-
message: NAMED_EXPORT_MESSAGE
|
|
40561
|
-
});
|
|
40562
|
-
if (entry.kind === "react-context") context.report({
|
|
41493
|
+
for (const entry of exports) if (entry.kind === "react-component") hasReactExport = true;
|
|
41494
|
+
for (const entry of exports) if (entry.kind === "namespace-object") context.report({
|
|
40563
41495
|
node: entry.reportNode,
|
|
40564
|
-
message:
|
|
41496
|
+
message: NAMESPACE_OBJECT_MESSAGE
|
|
40565
41497
|
});
|
|
41498
|
+
if (hasAnyExports && hasReactExport) for (const entry of exports) {
|
|
41499
|
+
if (entry.kind === "non-component") context.report({
|
|
41500
|
+
node: entry.reportNode,
|
|
41501
|
+
message: NAMED_EXPORT_MESSAGE
|
|
41502
|
+
});
|
|
41503
|
+
if (entry.kind === "react-context") context.report({
|
|
41504
|
+
node: entry.reportNode,
|
|
41505
|
+
message: REACT_CONTEXT_MESSAGE
|
|
41506
|
+
});
|
|
41507
|
+
}
|
|
40566
41508
|
}
|
|
40567
|
-
}
|
|
41509
|
+
};
|
|
40568
41510
|
}
|
|
40569
41511
|
});
|
|
40570
41512
|
//#endregion
|
|
@@ -40666,7 +41608,12 @@ const postmessageOriginRisk = defineRule({
|
|
|
40666
41608
|
scan: (file) => {
|
|
40667
41609
|
if (!isProductionSourcePath(file.relativePath)) return [];
|
|
40668
41610
|
if (WORKER_FILE_PATH_PATTERN.test(file.relativePath)) return [];
|
|
40669
|
-
|
|
41611
|
+
if (!file.content.includes("addEventListener") && !file.content.includes("onmessage")) return [];
|
|
41612
|
+
const ast = parseSourceText({
|
|
41613
|
+
filename: file.absolutePath,
|
|
41614
|
+
sourceText: file.content,
|
|
41615
|
+
shouldAttachParentReferences: false
|
|
41616
|
+
});
|
|
40670
41617
|
if (ast === null) return [];
|
|
40671
41618
|
const findings = [];
|
|
40672
41619
|
walkAst(ast, (node) => {
|
|
@@ -40916,7 +41863,7 @@ const preactPreferOndblclick = defineRule({
|
|
|
40916
41863
|
recommendation: "Rename `onDoubleClick` to `onDblClick` because Preact core listens for the DOM `dblclick` event name and `onDoubleClick` never fires.",
|
|
40917
41864
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
40918
41865
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
40919
|
-
const tagName = node
|
|
41866
|
+
const tagName = resolveJsxElementType(node);
|
|
40920
41867
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
|
|
40921
41868
|
const onDoubleClickAttribute = findJsxAttribute(node.attributes, "onDoubleClick");
|
|
40922
41869
|
if (!onDoubleClickAttribute) return;
|
|
@@ -40936,7 +41883,7 @@ const COMPAT_EXEMPT_INPUT_TYPES = new Set([
|
|
|
40936
41883
|
]);
|
|
40937
41884
|
const isTextLikeInput = (openingElement) => {
|
|
40938
41885
|
if (!isNodeOfType(openingElement.name, "JSXIdentifier")) return false;
|
|
40939
|
-
const tagName = openingElement
|
|
41886
|
+
const tagName = resolveJsxElementType(openingElement);
|
|
40940
41887
|
if (tagName === "textarea") return true;
|
|
40941
41888
|
if (tagName !== "input") return false;
|
|
40942
41889
|
const typeAttribute = findJsxAttribute(openingElement.attributes, "type");
|
|
@@ -41093,8 +42040,9 @@ const CROSS_CUTTING_STATE_BOOLEAN_NAMES = new Set([
|
|
|
41093
42040
|
]);
|
|
41094
42041
|
const collectBooleanPropBindings = (param) => {
|
|
41095
42042
|
const bindings = /* @__PURE__ */ new Set();
|
|
41096
|
-
|
|
41097
|
-
|
|
42043
|
+
const propsBinding = resolveFirstArgumentBinding(param);
|
|
42044
|
+
if (!isNodeOfType(propsBinding, "ObjectPattern")) return bindings;
|
|
42045
|
+
for (const property of propsBinding.properties ?? []) {
|
|
41098
42046
|
if (!isNodeOfType(property, "Property")) continue;
|
|
41099
42047
|
if (property.computed) continue;
|
|
41100
42048
|
if (!isNodeOfType(property.key, "Identifier")) continue;
|
|
@@ -41355,7 +42303,7 @@ const preferHtmlDialog = defineRule({
|
|
|
41355
42303
|
},
|
|
41356
42304
|
JSXOpeningElement(node) {
|
|
41357
42305
|
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
41358
|
-
const tagName = node
|
|
42306
|
+
const tagName = resolveJsxElementType(node);
|
|
41359
42307
|
if (tagName === "dialog") return;
|
|
41360
42308
|
if (tagName.length === 0 || tagName[0] !== tagName[0].toLowerCase()) return;
|
|
41361
42309
|
if (!HTML_TAGS.has(tagName)) return;
|
|
@@ -42902,13 +43850,13 @@ const resolveTanstackQueryHookName = (callExpression) => {
|
|
|
42902
43850
|
}
|
|
42903
43851
|
return null;
|
|
42904
43852
|
};
|
|
42905
|
-
const resolveHookNameFromInitializer = (initializer) => {
|
|
43853
|
+
const resolveHookNameFromInitializer = (initializer, scopes) => {
|
|
42906
43854
|
if (isNodeOfType(initializer, "CallExpression")) return resolveTanstackQueryHookName(initializer);
|
|
42907
43855
|
if (!isNodeOfType(initializer, "Identifier")) return null;
|
|
42908
|
-
const
|
|
42909
|
-
if (
|
|
42910
|
-
if (!isNodeOfType(
|
|
42911
|
-
return resolveTanstackQueryHookName(
|
|
43856
|
+
const resolvedSymbol = resolveConstIdentifierAlias(initializer, scopes);
|
|
43857
|
+
if (resolvedSymbol?.kind !== "const" || !resolvedSymbol.initializer) return null;
|
|
43858
|
+
if (!isNodeOfType(resolvedSymbol.initializer, "CallExpression")) return null;
|
|
43859
|
+
return resolveTanstackQueryHookName(resolvedSymbol.initializer);
|
|
42912
43860
|
};
|
|
42913
43861
|
const queryNoRestDestructuring = defineRule({
|
|
42914
43862
|
id: "query-no-rest-destructuring",
|
|
@@ -42921,7 +43869,7 @@ const queryNoRestDestructuring = defineRule({
|
|
|
42921
43869
|
if (!isNodeOfType(node.id, "ObjectPattern")) return;
|
|
42922
43870
|
if (!node.init) return;
|
|
42923
43871
|
if (!node.id.properties?.some((property) => isNodeOfType(property, "RestElement"))) return;
|
|
42924
|
-
const hookName = resolveHookNameFromInitializer(node.init);
|
|
43872
|
+
const hookName = resolveHookNameFromInitializer(node.init, context.scopes);
|
|
42925
43873
|
if (!hookName) return;
|
|
42926
43874
|
context.report({
|
|
42927
43875
|
node: node.id,
|
|
@@ -43046,15 +43994,6 @@ const isStableHookWrapperArgument = (node) => {
|
|
|
43046
43994
|
const parent = node.parent;
|
|
43047
43995
|
return isNodeOfType(parent, "CallExpression") && isNodeOfType(parent.callee, "Identifier") && STABLE_HOOK_WRAPPERS.has(parent.callee.name) && Boolean(parent.arguments?.some((argument) => argument === node));
|
|
43048
43996
|
};
|
|
43049
|
-
const isImmediatelyInvokedFunction = (functionNode) => {
|
|
43050
|
-
let wrappedCallee = functionNode;
|
|
43051
|
-
let enclosing = functionNode.parent;
|
|
43052
|
-
while (enclosing && stripParenExpression(enclosing) === functionNode) {
|
|
43053
|
-
wrappedCallee = enclosing;
|
|
43054
|
-
enclosing = enclosing.parent ?? null;
|
|
43055
|
-
}
|
|
43056
|
-
return Boolean(enclosing && isNodeOfType(enclosing, "CallExpression") && enclosing.callee === wrappedCallee);
|
|
43057
|
-
};
|
|
43058
43997
|
const queryStableQueryClient = defineRule({
|
|
43059
43998
|
id: "query-stable-query-client",
|
|
43060
43999
|
title: "Unstable QueryClient in component",
|
|
@@ -43401,7 +44340,7 @@ const renderingAnimateSvgWrapper = defineRule({
|
|
|
43401
44340
|
severity: "warn",
|
|
43402
44341
|
recommendation: "Wrap the SVG in a motion element so animation props apply to a stable wrapper instead of the SVG node itself.",
|
|
43403
44342
|
create: (context) => ({ JSXOpeningElement(node) {
|
|
43404
|
-
if (
|
|
44343
|
+
if (resolveJsxElementType(node) !== "svg") return;
|
|
43405
44344
|
if (node.attributes?.some((attribute) => isNodeOfType(attribute, "JSXAttribute") && isNodeOfType(attribute.name, "JSXIdentifier") && MOTION_ANIMATE_PROPS.has(attribute.name.name))) context.report({
|
|
43406
44345
|
node,
|
|
43407
44346
|
message: "This is slow to render because you animate <svg> directly, so wrap it in a <div> or <motion.div> & animate that instead"
|
|
@@ -44694,14 +45633,10 @@ const rerenderLazyStateInit = defineRule({
|
|
|
44694
45633
|
//#endregion
|
|
44695
45634
|
//#region src/plugin/rules/performance/rerender-memo-before-early-return.ts
|
|
44696
45635
|
const isJsxExpression = (node) => Boolean(node && isJsxElementOrFragment(stripParenExpression(node)));
|
|
44697
|
-
const callbackReturnsJsx = (callback) => {
|
|
45636
|
+
const callbackReturnsJsx = (callback, scopes) => {
|
|
44698
45637
|
if (!callback) return false;
|
|
44699
45638
|
if (!isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return false;
|
|
44700
|
-
|
|
44701
|
-
if (isJsxExpression(body)) return true;
|
|
44702
|
-
if (!isNodeOfType(body, "BlockStatement")) return false;
|
|
44703
|
-
for (const stmt of body.body ?? []) if (isNodeOfType(stmt, "ReturnStatement") && isJsxExpression(stmt.argument)) return true;
|
|
44704
|
-
return false;
|
|
45639
|
+
return functionReturnsMatchingExpression(callback, scopes, isJsxExpression);
|
|
44705
45640
|
};
|
|
44706
45641
|
const returnArgumentUsesAnyName = (returnStatement, names) => {
|
|
44707
45642
|
if (!isNodeOfType(returnStatement, "ReturnStatement") || !returnStatement.argument) return false;
|
|
@@ -44779,7 +45714,7 @@ const rerenderMemoBeforeEarlyReturn = defineRule({
|
|
|
44779
45714
|
if (!isNodeOfType(stmt, "VariableDeclaration")) continue;
|
|
44780
45715
|
for (const declarator of stmt.declarations ?? []) {
|
|
44781
45716
|
const init = declarator.init;
|
|
44782
|
-
if (isNodeOfType(init, "CallExpression") && isHookCall$2(init, "useMemo") && callbackReturnsJsx(init.arguments?.[0])) {
|
|
45717
|
+
if (isNodeOfType(init, "CallExpression") && isHookCall$2(init, "useMemo") && callbackReturnsJsx(init.arguments?.[0], context.scopes)) {
|
|
44783
45718
|
memoNode = declarator;
|
|
44784
45719
|
callbackGuardTests = collectLeadingCallbackGuardTests(init.arguments?.[0]);
|
|
44785
45720
|
if (isNodeOfType(declarator.id, "Identifier")) memoConsumerNames.add(declarator.id.name);
|
|
@@ -44845,8 +45780,11 @@ const collectFromObjectPattern = (pattern, bindings) => {
|
|
|
44845
45780
|
const collectDefaultedEmptyBindings = (functionNode) => {
|
|
44846
45781
|
const bindings = /* @__PURE__ */ new Map();
|
|
44847
45782
|
const params = functionNode.params ?? [];
|
|
44848
|
-
for (const param of params)
|
|
44849
|
-
|
|
45783
|
+
for (const [parameterIndex, param] of params.entries()) {
|
|
45784
|
+
const parameterBinding = parameterIndex === 0 ? resolveFirstArgumentBinding(param) : param;
|
|
45785
|
+
if (parameterBinding) collectFromObjectPattern(parameterBinding, bindings);
|
|
45786
|
+
}
|
|
45787
|
+
const propsParam = resolveFirstArgumentBinding(params[0]);
|
|
44850
45788
|
const body = functionNode.body;
|
|
44851
45789
|
if (!propsParam || !isNodeOfType(propsParam, "Identifier") || !body) return bindings;
|
|
44852
45790
|
if (!isNodeOfType(body, "BlockStatement")) return bindings;
|
|
@@ -45352,8 +46290,8 @@ const rnAnimationReactionAsDerived = defineRule({
|
|
|
45352
46290
|
if (statements.length !== 1) return;
|
|
45353
46291
|
const onlyStatement = statements[0];
|
|
45354
46292
|
if (!isNodeOfType(onlyStatement, "ExpressionStatement")) return;
|
|
45355
|
-
singleAssignment = onlyStatement
|
|
45356
|
-
} else if (body) singleAssignment = body;
|
|
46293
|
+
singleAssignment = unwrapDiscardedExpression(onlyStatement);
|
|
46294
|
+
} else if (body) singleAssignment = unwrapDiscardedExpression(body);
|
|
45357
46295
|
if (!singleAssignment) return;
|
|
45358
46296
|
const isValueAssignment = isNodeOfType(singleAssignment, "AssignmentExpression") && isNodeOfType(singleAssignment.left, "MemberExpression") && isNodeOfType(singleAssignment.left.object, "Identifier") && isNodeOfType(singleAssignment.left.property, "Identifier") && singleAssignment.left.property.name === "value";
|
|
45359
46297
|
const isSetCall = isNodeOfType(singleAssignment, "CallExpression") && isNodeOfType(singleAssignment.callee, "MemberExpression") && isNodeOfType(singleAssignment.callee.property, "Identifier") && singleAssignment.callee.property.name === "set" && (singleAssignment.arguments?.length ?? 0) === 1;
|
|
@@ -52273,7 +53211,8 @@ const serverCacheWithObjectLiteral = defineRule({
|
|
|
52273
53211
|
if (!isNodeOfType(node.callee, "Identifier")) return;
|
|
52274
53212
|
if (!cachedFunctionNames.has(node.callee.name)) return;
|
|
52275
53213
|
const firstArg = node.arguments?.[0];
|
|
52276
|
-
if (!isNodeOfType(firstArg, "
|
|
53214
|
+
if (!firstArg || isNodeOfType(firstArg, "SpreadElement")) return;
|
|
53215
|
+
if (!isNodeOfType(unwrapObjectIntegrityExpression(firstArg, context.scopes, OBJECT_FREEZE_OR_SEAL_METHOD_NAMES), "ObjectExpression")) return;
|
|
52277
53216
|
context.report({
|
|
52278
53217
|
node,
|
|
52279
53218
|
message: `Passing a new object to React.cache() each render misses the cache, so it refetches every request.`
|
|
@@ -52960,10 +53899,6 @@ const isInvalidStyleExpression = (expression) => {
|
|
|
52960
53899
|
}
|
|
52961
53900
|
return false;
|
|
52962
53901
|
};
|
|
52963
|
-
const getJsxOpeningElementName = (node) => {
|
|
52964
|
-
if (isNodeOfType(node.name, "JSXIdentifier")) return node.name.name;
|
|
52965
|
-
return null;
|
|
52966
|
-
};
|
|
52967
53902
|
const stylePropObject = defineRule({
|
|
52968
53903
|
id: "style-prop-object",
|
|
52969
53904
|
title: "Style prop is not an object",
|
|
@@ -52975,7 +53910,8 @@ const stylePropObject = defineRule({
|
|
|
52975
53910
|
const allowSet = new Set(allow);
|
|
52976
53911
|
return {
|
|
52977
53912
|
JSXOpeningElement(node) {
|
|
52978
|
-
|
|
53913
|
+
if (!isNodeOfType(node.name, "JSXIdentifier")) return;
|
|
53914
|
+
const elementName = resolveJsxElementType(node);
|
|
52979
53915
|
if (elementName && allowSet.has(elementName)) return;
|
|
52980
53916
|
if (elementName) {
|
|
52981
53917
|
const firstCharCode = elementName.charCodeAt(0);
|
|
@@ -53680,7 +54616,7 @@ const tanstackStartNoAnchorElement = defineRule({
|
|
|
53680
54616
|
create: (context) => {
|
|
53681
54617
|
if (!isInProjectDirectory(context, "routes")) return {};
|
|
53682
54618
|
return { JSXOpeningElement(node) {
|
|
53683
|
-
if (
|
|
54619
|
+
if (resolveJsxElementType(node) !== "a") return;
|
|
53684
54620
|
const attributes = node.attributes ?? [];
|
|
53685
54621
|
const hrefAttribute = findJsxAttribute(attributes, "href");
|
|
53686
54622
|
if (!hrefAttribute?.value) return;
|
|
@@ -54301,8 +55237,7 @@ const voidDomElementsNoChildren = defineRule({
|
|
|
54301
55237
|
create: (context) => ({
|
|
54302
55238
|
JSXElement(node) {
|
|
54303
55239
|
const openingElement = node.openingElement;
|
|
54304
|
-
|
|
54305
|
-
const tagName = openingElement.name.name;
|
|
55240
|
+
const tagName = resolveJsxElementType(openingElement);
|
|
54306
55241
|
if (!VOID_DOM_ELEMENTS.has(tagName)) return;
|
|
54307
55242
|
const hasChildrenContent = node.children.some(isMeaningfulJsxChild);
|
|
54308
55243
|
const hasChildrenLikeProp = findChildrenLikePropName(openingElement.attributes);
|
|
@@ -54367,12 +55302,6 @@ const webhookSignatureRisk = defineRule({
|
|
|
54367
55302
|
//#region src/plugin/rules/zod/utils/zod-ast.ts
|
|
54368
55303
|
const ZOD_MODULE = "zod";
|
|
54369
55304
|
const ZOD_MODULE_SOURCES = [ZOD_MODULE];
|
|
54370
|
-
const getStaticPropertyName = (member) => {
|
|
54371
|
-
const property = member.property;
|
|
54372
|
-
if (!member.computed && isNodeOfType(property, "Identifier")) return property.name;
|
|
54373
|
-
if (member.computed && isNodeOfType(property, "Literal") && typeof property.value === "string") return property.value;
|
|
54374
|
-
return null;
|
|
54375
|
-
};
|
|
54376
55305
|
const importInfoCache = /* @__PURE__ */ new WeakMap();
|
|
54377
55306
|
const getImportInfoForIdentifier = (identifier) => {
|
|
54378
55307
|
if (importInfoCache.has(identifier)) return importInfoCache.get(identifier) ?? null;
|
|
@@ -59553,7 +60482,10 @@ const appendNode = (builder, block, node) => {
|
|
|
59553
60482
|
};
|
|
59554
60483
|
const mapDescendantsToBlock = (builder, node, block) => {
|
|
59555
60484
|
builder.nodeBlock.set(node, block);
|
|
59556
|
-
if (isFunctionLike$1(node))
|
|
60485
|
+
if (isFunctionLike$1(node)) {
|
|
60486
|
+
builder.nestedFunctions.push(node);
|
|
60487
|
+
return;
|
|
60488
|
+
}
|
|
59557
60489
|
const record = node;
|
|
59558
60490
|
for (const key of Object.keys(record)) {
|
|
59559
60491
|
if (key === "parent") continue;
|
|
@@ -59787,11 +60719,12 @@ const buildStatement = (builder, statement, current) => {
|
|
|
59787
60719
|
mapDescendantsToBlock(builder, statement, current);
|
|
59788
60720
|
return current;
|
|
59789
60721
|
};
|
|
59790
|
-
const buildFunctionCfg = (functionNode, body) => {
|
|
60722
|
+
const buildFunctionCfg = (functionNode, body, nestedFunctionSink) => {
|
|
59791
60723
|
const builder = {
|
|
59792
60724
|
blocks: [],
|
|
59793
60725
|
entry: null,
|
|
59794
60726
|
exit: null,
|
|
60727
|
+
nestedFunctions: nestedFunctionSink,
|
|
59795
60728
|
nodeBlock: /* @__PURE__ */ new Map(),
|
|
59796
60729
|
loopStack: [],
|
|
59797
60730
|
switchStack: [],
|
|
@@ -59809,13 +60742,12 @@ const buildFunctionCfg = (functionNode, body) => {
|
|
|
59809
60742
|
bodyEnd = entry;
|
|
59810
60743
|
}
|
|
59811
60744
|
addEdge(bodyEnd, exit, "uncond");
|
|
59812
|
-
const blockOf = (node) => builder.nodeBlock.get(node) ?? null;
|
|
59813
60745
|
return {
|
|
59814
60746
|
owner: functionNode,
|
|
59815
60747
|
entry,
|
|
59816
60748
|
exit,
|
|
59817
60749
|
blocks: builder.blocks,
|
|
59818
|
-
blockOf
|
|
60750
|
+
blockOf: (node) => builder.nodeBlock.get(node) ?? null
|
|
59819
60751
|
};
|
|
59820
60752
|
};
|
|
59821
60753
|
const computeUnconditionalSet = (cfg) => {
|
|
@@ -59852,8 +60784,9 @@ const computeUnconditionalSet = (cfg) => {
|
|
|
59852
60784
|
const analyzeControlFlow = (program) => {
|
|
59853
60785
|
nextBlockId = 0;
|
|
59854
60786
|
const functionCfgs = /* @__PURE__ */ new Map();
|
|
60787
|
+
const pendingFunctions = [];
|
|
59855
60788
|
const buildFor = (functionNode, body) => {
|
|
59856
|
-
const cfg = buildFunctionCfg(functionNode, body);
|
|
60789
|
+
const cfg = buildFunctionCfg(functionNode, body, pendingFunctions);
|
|
59857
60790
|
functionCfgs.set(functionNode, {
|
|
59858
60791
|
cfg,
|
|
59859
60792
|
unconditionalSet: computeUnconditionalSet(cfg)
|
|
@@ -59863,21 +60796,18 @@ const analyzeControlFlow = (program) => {
|
|
|
59863
60796
|
type: "BlockStatement",
|
|
59864
60797
|
body: program.body
|
|
59865
60798
|
});
|
|
59866
|
-
|
|
59867
|
-
|
|
59868
|
-
|
|
59869
|
-
|
|
59870
|
-
|
|
59871
|
-
|
|
59872
|
-
|
|
59873
|
-
|
|
59874
|
-
|
|
59875
|
-
|
|
59876
|
-
|
|
59877
|
-
} else if (isAstNode(child)) visit(child);
|
|
59878
|
-
}
|
|
60799
|
+
for (let functionIndex = 0; functionIndex < pendingFunctions.length; functionIndex += 1) {
|
|
60800
|
+
const functionNode = pendingFunctions[functionIndex];
|
|
60801
|
+
if (!isFunctionLike$1(functionNode) || !functionNode.body || functionCfgs.has(functionNode)) continue;
|
|
60802
|
+
buildFor(functionNode, functionNode.body);
|
|
60803
|
+
}
|
|
60804
|
+
const getFunctionEntry = (functionNode) => {
|
|
60805
|
+
const existingEntry = functionCfgs.get(functionNode);
|
|
60806
|
+
if (existingEntry) return existingEntry;
|
|
60807
|
+
if (!isFunctionLike$1(functionNode) || !functionNode.body) return null;
|
|
60808
|
+
buildFor(functionNode, functionNode.body);
|
|
60809
|
+
return functionCfgs.get(functionNode) ?? null;
|
|
59879
60810
|
};
|
|
59880
|
-
visit(program);
|
|
59881
60811
|
const enclosingFunction = (node) => {
|
|
59882
60812
|
let current = node;
|
|
59883
60813
|
while (current) {
|
|
@@ -59888,12 +60818,12 @@ const analyzeControlFlow = (program) => {
|
|
|
59888
60818
|
return null;
|
|
59889
60819
|
};
|
|
59890
60820
|
const cfgFor = (functionLike) => {
|
|
59891
|
-
return
|
|
60821
|
+
return getFunctionEntry(functionLike)?.cfg ?? null;
|
|
59892
60822
|
};
|
|
59893
60823
|
const isUnconditionalFromEntry = (node) => {
|
|
59894
60824
|
const owner = enclosingFunction(node);
|
|
59895
60825
|
if (!owner) return true;
|
|
59896
|
-
const entry =
|
|
60826
|
+
const entry = getFunctionEntry(owner);
|
|
59897
60827
|
if (!entry) return true;
|
|
59898
60828
|
const block = entry.cfg.blockOf(node);
|
|
59899
60829
|
if (!block) return true;
|
|
@@ -59967,17 +60897,14 @@ const wrapWithSemanticContext = (rule) => ({
|
|
|
59967
60897
|
}
|
|
59968
60898
|
};
|
|
59969
60899
|
const visitors = rule.create(enrichedContext);
|
|
59970
|
-
const
|
|
59971
|
-
|
|
59972
|
-
|
|
59973
|
-
|
|
59974
|
-
|
|
59975
|
-
|
|
59976
|
-
|
|
59977
|
-
|
|
59978
|
-
if (innerProgramHandler) innerProgramHandler(node);
|
|
59979
|
-
});
|
|
59980
|
-
return passthroughVisitors;
|
|
60900
|
+
const innerProgramHandler = visitors.Program;
|
|
60901
|
+
return {
|
|
60902
|
+
...visitors,
|
|
60903
|
+
Program: ((node) => {
|
|
60904
|
+
programRoot = node;
|
|
60905
|
+
if (innerProgramHandler) innerProgramHandler(node);
|
|
60906
|
+
})
|
|
60907
|
+
};
|
|
59981
60908
|
}
|
|
59982
60909
|
});
|
|
59983
60910
|
//#endregion
|