oxlint-plugin-react-doctor 0.7.9-dev.1a30f2c → 0.7.9-dev.21043e0
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.d.ts +46 -0
- package/dist/index.js +1854 -302
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { KEYS } from "eslint-visitor-keys";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import { parseSync, visitorKeys } from "oxc-parser";
|
|
3
4
|
import * as fs from "node:fs";
|
|
4
5
|
import { readFileSync } from "node:fs";
|
|
5
|
-
import { parseSync, visitorKeys } from "oxc-parser";
|
|
6
6
|
import { analyze } from "eslint-scope";
|
|
7
7
|
//#region src/plugin/utils/is-node-of-type.ts
|
|
8
8
|
const isNodeOfType = (node, type) => node !== null && typeof node === "object" && "type" in node && node.type === type;
|
|
@@ -327,7 +327,15 @@ const defineRule = (rule) => {
|
|
|
327
327
|
let lastContentLineIndex;
|
|
328
328
|
const buildLineStartOffsets = (content) => {
|
|
329
329
|
const lineStartOffsets = [0];
|
|
330
|
-
for (let
|
|
330
|
+
for (let characterIndex = 0; characterIndex < content.length; characterIndex += 1) {
|
|
331
|
+
const character = content[characterIndex];
|
|
332
|
+
if (character === "\r" && content[characterIndex + 1] === "\n") {
|
|
333
|
+
characterIndex += 1;
|
|
334
|
+
lineStartOffsets.push(characterIndex + 1);
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
if (character === "\r" || character === "\n" || character === "\u2028" || character === "\u2029") lineStartOffsets.push(characterIndex + 1);
|
|
338
|
+
}
|
|
331
339
|
return lineStartOffsets;
|
|
332
340
|
};
|
|
333
341
|
const getLineStartOffsets = (content) => {
|
|
@@ -4342,7 +4350,8 @@ const SECRET_VALUE_PATTERNS = [
|
|
|
4342
4350
|
];
|
|
4343
4351
|
const JWT_LITERAL_VALUE_PATTERN = /\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{16,}\b/;
|
|
4344
4352
|
const PUBLIC_ENV_SECRET_NAME_PATTERN = /\b(?:NEXT_PUBLIC|VITE|REACT_APP|EXPO_PUBLIC)_[A-Z0-9_]*(?:SECRET|TOKEN|PASSWORD|PRIVATE|DATABASE_URL|SERVICE_ROLE|AWS_ACCESS_KEY|AWS_SECRET)[A-Z0-9_]*\b/i;
|
|
4345
|
-
const FULL_ENV_LEAK_CONTEXT_PATTERN = /\b(?:process
|
|
4353
|
+
const FULL_ENV_LEAK_CONTEXT_PATTERN = /\b(?:process\s*\.\s*env|import\s*\.\s*meta\s*\.\s*env|window\.__[A-Z0-9_]*ENV[A-Z0-9_]*__|__[A-Z0-9_]*ENV[A-Z0-9_]*__)\b/;
|
|
4354
|
+
const FULL_ENV_LEAK_COMMENT_TRIVIA_PATTERN = /\b(?:(?:process|window)\s*(?:\/[*/]|<!--|-->|\.\s*(?:\/[*/]|<!--|-->))|import\s*(?:\/[*/]|<!--|-->|\.\s*(?:\/[*/]|<!--|-->|meta\s*(?:\/[*/]|<!--|-->|\.\s*(?:\/[*/]|<!--|-->)))))/;
|
|
4346
4355
|
const FULL_ENV_LEAK_SECRET_NAME_PATTERN = /\b(?:DATABASE_URL|AWS_SECRET_ACCESS_KEY|AWS_ACCESS_KEY_ID|MAILGUN_API_KEY|SALESFORCE_CLIENT_SECRET|OKTA_CLIENT_SECRET|SESSION_SECRET|COOKIE_SECRET|PRIVATE_KEY|SERVICE_ROLE)\b/;
|
|
4347
4356
|
const TRUSTED_PUBLIC_SECRET_NAME_PATTERN = /(?:SENTRY_DSN|PUBLIC_KEY|PUBLISHABLE|ANON_KEY|POSTHOG_(?:PROJECT_)?TOKEN|POSTHOG_KEY|TLDRAW_LICENSE_KEY|CLERK_PUBLISHABLE_KEY|ALGOLIA_SEARCH_KEY|GC_API_KEY|GOOGLE_MAPS_API_KEY|MAPBOX_TOKEN|MIXPANEL_TOKEN|FACEBOOK_CLIENT_TOKEN|(?:NEXT_PUBLIC|VITE|REACT_APP|EXPO_PUBLIC)_(?:DISABLE|ENABLE|ALLOW|REQUIRE)_)|(?:TOKEN|SECRET|PASSWORD|PRIVATE)_(?:KIND|TYPE|URL|URI|ENDPOINT|HEADER|NAME)$/i;
|
|
4348
4357
|
const PUBLIC_CLIENT_KEY_PATTERNS = [
|
|
@@ -4500,6 +4509,172 @@ const findSuspiciousPublicEnvSecretNamePattern = (content) => {
|
|
|
4500
4509
|
//#region src/plugin/rules/security-scan/utils/has-full-env-leak-shape.ts
|
|
4501
4510
|
const hasFullEnvLeakShape = (content) => FULL_ENV_LEAK_CONTEXT_PATTERN.test(content) && FULL_ENV_LEAK_SECRET_NAME_PATTERN.test(content);
|
|
4502
4511
|
//#endregion
|
|
4512
|
+
//#region src/plugin/utils/attach-parent-references.ts
|
|
4513
|
+
const attachParentReferences = (root) => {
|
|
4514
|
+
const visit = (node, parent) => {
|
|
4515
|
+
const writableNode = node;
|
|
4516
|
+
writableNode.parent = parent;
|
|
4517
|
+
const nodeRecord = node;
|
|
4518
|
+
for (const key of Object.keys(nodeRecord)) {
|
|
4519
|
+
if (key === "parent") continue;
|
|
4520
|
+
const child = nodeRecord[key];
|
|
4521
|
+
if (Array.isArray(child)) {
|
|
4522
|
+
for (const item of child) if (isAstNode(item)) visit(item, node);
|
|
4523
|
+
} else if (isAstNode(child)) visit(child, node);
|
|
4524
|
+
}
|
|
4525
|
+
};
|
|
4526
|
+
visit(root, null);
|
|
4527
|
+
};
|
|
4528
|
+
//#endregion
|
|
4529
|
+
//#region src/plugin/utils/cross-file-probe-recorder.ts
|
|
4530
|
+
let activeProbeTrace = null;
|
|
4531
|
+
const recordExistenceProbe = (absolutePath) => {
|
|
4532
|
+
activeProbeTrace?.existencePaths.add(absolutePath);
|
|
4533
|
+
};
|
|
4534
|
+
const recordContentProbe = (absolutePath) => {
|
|
4535
|
+
activeProbeTrace?.contentPaths.add(absolutePath);
|
|
4536
|
+
};
|
|
4537
|
+
const isProbeRecorderActive = () => activeProbeTrace !== null;
|
|
4538
|
+
const collectCrossFileProbes = (collect) => {
|
|
4539
|
+
const previousTrace = activeProbeTrace;
|
|
4540
|
+
const trace = {
|
|
4541
|
+
existencePaths: /* @__PURE__ */ new Set(),
|
|
4542
|
+
contentPaths: /* @__PURE__ */ new Set()
|
|
4543
|
+
};
|
|
4544
|
+
activeProbeTrace = trace;
|
|
4545
|
+
try {
|
|
4546
|
+
collect();
|
|
4547
|
+
} finally {
|
|
4548
|
+
activeProbeTrace = previousTrace;
|
|
4549
|
+
}
|
|
4550
|
+
return trace;
|
|
4551
|
+
};
|
|
4552
|
+
//#endregion
|
|
4553
|
+
//#region src/plugin/utils/parse-source-file.ts
|
|
4554
|
+
const FILENAME_TO_LANG = {
|
|
4555
|
+
".ts": "ts",
|
|
4556
|
+
".tsx": "tsx",
|
|
4557
|
+
".js": "js",
|
|
4558
|
+
".jsx": "jsx",
|
|
4559
|
+
".mjs": "js",
|
|
4560
|
+
".cjs": "js",
|
|
4561
|
+
".mts": "ts",
|
|
4562
|
+
".cts": "ts"
|
|
4563
|
+
};
|
|
4564
|
+
const resolveLang = (filename) => {
|
|
4565
|
+
return FILENAME_TO_LANG[path.extname(filename).toLowerCase()] ?? "tsx";
|
|
4566
|
+
};
|
|
4567
|
+
const parseSourceText = ({ filename, sourceText, shouldAttachParentReferences = true }) => {
|
|
4568
|
+
try {
|
|
4569
|
+
const result = parseSync(filename, sourceText, {
|
|
4570
|
+
astType: "ts",
|
|
4571
|
+
lang: resolveLang(filename)
|
|
4572
|
+
});
|
|
4573
|
+
if (result.errors.some((parseError) => parseError.severity === "Error")) return null;
|
|
4574
|
+
const parsedProgram = result.program;
|
|
4575
|
+
if (shouldAttachParentReferences) attachParentReferences(parsedProgram);
|
|
4576
|
+
return parsedProgram;
|
|
4577
|
+
} catch {
|
|
4578
|
+
return null;
|
|
4579
|
+
}
|
|
4580
|
+
};
|
|
4581
|
+
const parseCache = /* @__PURE__ */ new Map();
|
|
4582
|
+
const parseSourceFile = (absoluteFilePath) => {
|
|
4583
|
+
if (!(absoluteFilePath.endsWith(".d.ts") || absoluteFilePath.endsWith(".d.mts") || absoluteFilePath.endsWith(".d.cts"))) recordContentProbe(absoluteFilePath);
|
|
4584
|
+
let fileStat;
|
|
4585
|
+
try {
|
|
4586
|
+
fileStat = fs.statSync(absoluteFilePath);
|
|
4587
|
+
} catch {
|
|
4588
|
+
return null;
|
|
4589
|
+
}
|
|
4590
|
+
if (!fileStat.isFile()) return null;
|
|
4591
|
+
if (fileStat.size > 2e6) return null;
|
|
4592
|
+
const cached = parseCache.get(absoluteFilePath);
|
|
4593
|
+
if (cached && cached.mtimeMs === fileStat.mtimeMs && cached.size === fileStat.size) return cached.program;
|
|
4594
|
+
if (absoluteFilePath.endsWith(".d.ts") || absoluteFilePath.endsWith(".d.mts") || absoluteFilePath.endsWith(".d.cts")) {
|
|
4595
|
+
parseCache.set(absoluteFilePath, {
|
|
4596
|
+
mtimeMs: fileStat.mtimeMs,
|
|
4597
|
+
size: fileStat.size,
|
|
4598
|
+
program: null
|
|
4599
|
+
});
|
|
4600
|
+
return null;
|
|
4601
|
+
}
|
|
4602
|
+
let sourceText;
|
|
4603
|
+
try {
|
|
4604
|
+
sourceText = fs.readFileSync(absoluteFilePath, "utf8");
|
|
4605
|
+
} catch {
|
|
4606
|
+
parseCache.set(absoluteFilePath, {
|
|
4607
|
+
mtimeMs: fileStat.mtimeMs,
|
|
4608
|
+
size: fileStat.size,
|
|
4609
|
+
program: null
|
|
4610
|
+
});
|
|
4611
|
+
return null;
|
|
4612
|
+
}
|
|
4613
|
+
const parsedProgram = parseSourceText({
|
|
4614
|
+
filename: absoluteFilePath,
|
|
4615
|
+
sourceText
|
|
4616
|
+
});
|
|
4617
|
+
parseCache.set(absoluteFilePath, {
|
|
4618
|
+
mtimeMs: fileStat.mtimeMs,
|
|
4619
|
+
size: fileStat.size,
|
|
4620
|
+
program: parsedProgram
|
|
4621
|
+
});
|
|
4622
|
+
return parsedProgram;
|
|
4623
|
+
};
|
|
4624
|
+
//#endregion
|
|
4625
|
+
//#region src/plugin/rules/security-scan/utils/mask-source-comments.ts
|
|
4626
|
+
const SOURCE_FILE_EXTENSION_PATTERN$1 = /\.(?:[cm]?[jt]sx?)$/i;
|
|
4627
|
+
const POSSIBLE_SOURCE_COMMENT_PATTERN = /\/\/|\/\*|<!--/;
|
|
4628
|
+
const LINE_TERMINATORS = new Set([
|
|
4629
|
+
"\r",
|
|
4630
|
+
"\n",
|
|
4631
|
+
"\u2028",
|
|
4632
|
+
"\u2029"
|
|
4633
|
+
]);
|
|
4634
|
+
const hasPossibleAnnexBClosingComment = (content) => {
|
|
4635
|
+
let searchIndex = 0;
|
|
4636
|
+
while (searchIndex < content.length) {
|
|
4637
|
+
const closingCommentIndex = content.indexOf("-->", searchIndex);
|
|
4638
|
+
if (closingCommentIndex === -1) return false;
|
|
4639
|
+
let prefixIndex = closingCommentIndex - 1;
|
|
4640
|
+
while (prefixIndex >= 0 && !LINE_TERMINATORS.has(content[prefixIndex] ?? "")) {
|
|
4641
|
+
if (content[prefixIndex]?.trim() !== "") break;
|
|
4642
|
+
prefixIndex -= 1;
|
|
4643
|
+
}
|
|
4644
|
+
if (prefixIndex < 0 || LINE_TERMINATORS.has(content[prefixIndex] ?? "")) return true;
|
|
4645
|
+
searchIndex = closingCommentIndex + 3;
|
|
4646
|
+
}
|
|
4647
|
+
return false;
|
|
4648
|
+
};
|
|
4649
|
+
const maskSourceComments = (relativePath, content) => {
|
|
4650
|
+
if (!SOURCE_FILE_EXTENSION_PATTERN$1.test(relativePath)) return content;
|
|
4651
|
+
if (!content.startsWith("#!") && !POSSIBLE_SOURCE_COMMENT_PATTERN.test(content) && !hasPossibleAnnexBClosingComment(content)) return content;
|
|
4652
|
+
try {
|
|
4653
|
+
const result = parseSync(relativePath, content, {
|
|
4654
|
+
astType: "ts",
|
|
4655
|
+
lang: resolveLang(relativePath)
|
|
4656
|
+
});
|
|
4657
|
+
if (result.errors.some((parseError) => parseError.severity === "Error")) return void 0;
|
|
4658
|
+
const firstLineTerminatorIndex = content.search(/[\r\n\u2028\u2029]/);
|
|
4659
|
+
const ignoredRanges = [...content.startsWith("#!") ? [{
|
|
4660
|
+
start: 0,
|
|
4661
|
+
end: firstLineTerminatorIndex === -1 ? content.length : firstLineTerminatorIndex
|
|
4662
|
+
}] : [], ...result.comments];
|
|
4663
|
+
if (ignoredRanges.length === 0) return content;
|
|
4664
|
+
const contentParts = [];
|
|
4665
|
+
let previousEnd = 0;
|
|
4666
|
+
for (const ignoredRange of ignoredRanges) {
|
|
4667
|
+
contentParts.push(content.slice(previousEnd, ignoredRange.start));
|
|
4668
|
+
contentParts.push(content.slice(ignoredRange.start, ignoredRange.end).replace(/[^\r\n\u2028\u2029]/g, " "));
|
|
4669
|
+
previousEnd = ignoredRange.end;
|
|
4670
|
+
}
|
|
4671
|
+
contentParts.push(content.slice(previousEnd));
|
|
4672
|
+
return contentParts.join("");
|
|
4673
|
+
} catch {
|
|
4674
|
+
return;
|
|
4675
|
+
}
|
|
4676
|
+
};
|
|
4677
|
+
//#endregion
|
|
4503
4678
|
//#region src/plugin/rules/security-scan/utils/scan-artifact-leak.ts
|
|
4504
4679
|
const scanArtifactLeak = (file, findLeakPattern, message) => {
|
|
4505
4680
|
if (DOCUMENTATION_CONTEXT_PATTERN.test(file.relativePath)) return [];
|
|
@@ -4515,12 +4690,39 @@ const scanArtifactLeak = (file, findLeakPattern, message) => {
|
|
|
4515
4690
|
};
|
|
4516
4691
|
//#endregion
|
|
4517
4692
|
//#region src/plugin/rules/security-scan/artifact-env-leak.ts
|
|
4693
|
+
const ARTIFACT_ENV_LEAK_MESSAGE = "A browser artifact contains server-secret environment names or a full environment dump shape.";
|
|
4694
|
+
const findArtifactEnvLeakPattern = (content) => findSuspiciousPublicEnvSecretNamePattern(content) ?? (hasFullEnvLeakShape(content) ? FULL_ENV_LEAK_SECRET_NAME_PATTERN : void 0);
|
|
4518
4695
|
const artifactEnvLeak = defineRule({
|
|
4519
4696
|
id: "artifact-env-leak",
|
|
4520
4697
|
title: "Server env leaked to browser artifact",
|
|
4521
4698
|
severity: "error",
|
|
4522
4699
|
recommendation: "Treat public env prefixes as publication, not secrecy; keep secret env vars server-only and rebuild after rotating leaked keys.",
|
|
4523
|
-
scan: (file) =>
|
|
4700
|
+
scan: (file) => {
|
|
4701
|
+
let isRawCandidateExact = false;
|
|
4702
|
+
const findRawCandidatePattern = (content) => {
|
|
4703
|
+
const suspiciousPublicNamePattern = findSuspiciousPublicEnvSecretNamePattern(content);
|
|
4704
|
+
if (suspiciousPublicNamePattern) {
|
|
4705
|
+
isRawCandidateExact = true;
|
|
4706
|
+
return suspiciousPublicNamePattern;
|
|
4707
|
+
}
|
|
4708
|
+
if (!FULL_ENV_LEAK_SECRET_NAME_PATTERN.test(content)) return void 0;
|
|
4709
|
+
if (FULL_ENV_LEAK_CONTEXT_PATTERN.test(content)) {
|
|
4710
|
+
isRawCandidateExact = true;
|
|
4711
|
+
return FULL_ENV_LEAK_SECRET_NAME_PATTERN;
|
|
4712
|
+
}
|
|
4713
|
+
return FULL_ENV_LEAK_COMMENT_TRIVIA_PATTERN.test(content) ? FULL_ENV_LEAK_SECRET_NAME_PATTERN : void 0;
|
|
4714
|
+
};
|
|
4715
|
+
const rawCandidateFindings = scanArtifactLeak(file, findRawCandidatePattern, ARTIFACT_ENV_LEAK_MESSAGE);
|
|
4716
|
+
if (rawCandidateFindings.length === 0) return rawCandidateFindings;
|
|
4717
|
+
const rawFindings = isRawCandidateExact ? rawCandidateFindings : scanArtifactLeak(file, findArtifactEnvLeakPattern, ARTIFACT_ENV_LEAK_MESSAGE);
|
|
4718
|
+
const executableContent = maskSourceComments(file.relativePath, file.content);
|
|
4719
|
+
if (executableContent === void 0) return rawCandidateFindings;
|
|
4720
|
+
if (executableContent === file.content) return rawFindings;
|
|
4721
|
+
return scanArtifactLeak({
|
|
4722
|
+
...file,
|
|
4723
|
+
content: executableContent
|
|
4724
|
+
}, findArtifactEnvLeakPattern, ARTIFACT_ENV_LEAK_MESSAGE);
|
|
4725
|
+
}
|
|
4524
4726
|
});
|
|
4525
4727
|
//#endregion
|
|
4526
4728
|
//#region src/plugin/rules/security-scan/artifact-secret-leak.ts
|
|
@@ -6534,6 +6736,28 @@ const asyncParallel = defineRule({
|
|
|
6534
6736
|
}
|
|
6535
6737
|
});
|
|
6536
6738
|
//#endregion
|
|
6739
|
+
//#region src/plugin/utils/collect-function-return-statements.ts
|
|
6740
|
+
const collectFunctionReturnStatements = (functionNode) => {
|
|
6741
|
+
if (!isFunctionLike$1(functionNode) || !isNodeOfType(functionNode.body, "BlockStatement")) return [];
|
|
6742
|
+
const returnStatements = [];
|
|
6743
|
+
walkAst(functionNode.body, (node) => {
|
|
6744
|
+
if (node !== functionNode.body && (isFunctionLike$1(node) || isNodeOfType(node, "ClassDeclaration") || isNodeOfType(node, "ClassExpression"))) return false;
|
|
6745
|
+
if (isNodeOfType(node, "ReturnStatement")) returnStatements.push(node);
|
|
6746
|
+
});
|
|
6747
|
+
return returnStatements;
|
|
6748
|
+
};
|
|
6749
|
+
//#endregion
|
|
6750
|
+
//#region src/plugin/utils/is-nullish-expression.ts
|
|
6751
|
+
const isNullishExpression = (expression) => isNodeOfType(expression, "Literal") && expression.value === null || isNodeOfType(expression, "Identifier") && expression.name === "undefined" || isNodeOfType(expression, "UnaryExpression") && expression.operator === "void";
|
|
6752
|
+
//#endregion
|
|
6753
|
+
//#region src/plugin/utils/strip-this-parameter.ts
|
|
6754
|
+
const stripThisParameter = (parameters) => {
|
|
6755
|
+
const firstParameter = parameters[0];
|
|
6756
|
+
if (!firstParameter) return parameters;
|
|
6757
|
+
if (isNodeOfType(firstParameter, "Identifier") && firstParameter.name === "this") return parameters.slice(1);
|
|
6758
|
+
return parameters;
|
|
6759
|
+
};
|
|
6760
|
+
//#endregion
|
|
6537
6761
|
//#region src/plugin/rules/security/auth-token-in-web-storage.ts
|
|
6538
6762
|
const MESSAGE$60 = "Storing an auth token in `localStorage`/`sessionStorage` exposes it to any XSS on the page: JavaScript can read web storage and exfiltrate the token. Keep tokens in an `HttpOnly`, `Secure`, `SameSite` cookie instead.";
|
|
6539
6763
|
const STORAGE_NAMES = new Set(["localStorage", "sessionStorage"]);
|
|
@@ -6545,12 +6769,8 @@ const STORAGE_GLOBALS = new Set([
|
|
|
6545
6769
|
const SENSITIVE_KEY_PATTERN = /token|jwt|secret|password|passwd|credential|api[-_]?key|bearer|private[-_]?key/i;
|
|
6546
6770
|
const NON_AUTH_TOKEN_PATTERN = /csrf|xsrf|device|fcm|apns|push|design|tokeniz|syntax|css|theme|color/i;
|
|
6547
6771
|
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;
|
|
6548
|
-
const PRODUCT_API_KEY_RECORDS_PATTERN = /(?:^|[._:-])(?:created|saved|integration|mailing)[-_]?api[-_]?keys$/i;
|
|
6549
|
-
const PRODUCT_API_KEY_COLLECTION_PATTERN = /[._:-](?:created|generated|saved)[-_]?api[-_]?keys$/i;
|
|
6550
6772
|
const isAuthCredentialKey = (key) => {
|
|
6551
|
-
if (PRODUCT_API_KEY_RECORDS_PATTERN.test(key)) return false;
|
|
6552
6773
|
if (!SENSITIVE_KEY_PATTERN.test(key)) return false;
|
|
6553
|
-
if (PRODUCT_API_KEY_COLLECTION_PATTERN.test(key)) return false;
|
|
6554
6774
|
if (NON_AUTH_TOKEN_PATTERN.test(key) && !STRONG_AUTH_KEY_PATTERN.test(key)) return false;
|
|
6555
6775
|
return true;
|
|
6556
6776
|
};
|
|
@@ -6559,11 +6779,63 @@ const isDirectWebStorageObject = (node) => {
|
|
|
6559
6779
|
if (isNodeOfType(node, "MemberExpression") && !node.computed && isNodeOfType(node.object, "Identifier") && STORAGE_GLOBALS.has(node.object.name) && isNodeOfType(node.property, "Identifier")) return STORAGE_NAMES.has(node.property.name);
|
|
6560
6780
|
return false;
|
|
6561
6781
|
};
|
|
6562
|
-
const
|
|
6563
|
-
if (
|
|
6564
|
-
|
|
6565
|
-
const binding = findVariableInitializer(
|
|
6566
|
-
|
|
6782
|
+
const immutableInitializer = (identifier, visitedIdentifiers = /* @__PURE__ */ new Set()) => {
|
|
6783
|
+
if (visitedIdentifiers.has(identifier)) return null;
|
|
6784
|
+
visitedIdentifiers.add(identifier);
|
|
6785
|
+
const binding = findVariableInitializer(identifier, identifier.name);
|
|
6786
|
+
if (!binding?.initializer) return null;
|
|
6787
|
+
if (isNodeOfType(binding.initializer, "FunctionDeclaration")) return binding.initializer;
|
|
6788
|
+
const declarator = binding.bindingIdentifier.parent;
|
|
6789
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator")) return null;
|
|
6790
|
+
const declaration = declarator.parent;
|
|
6791
|
+
if (!declaration || !isNodeOfType(declaration, "VariableDeclaration")) return null;
|
|
6792
|
+
if (declaration.kind !== "const") return null;
|
|
6793
|
+
const initializer = stripParenExpression(binding.initializer);
|
|
6794
|
+
if (!isNodeOfType(initializer, "Identifier")) return initializer;
|
|
6795
|
+
return immutableInitializer(initializer, visitedIdentifiers) ?? initializer;
|
|
6796
|
+
};
|
|
6797
|
+
const isWebStorageFactoryResult = (node, visitedNodes) => {
|
|
6798
|
+
const expression = stripParenExpression(node);
|
|
6799
|
+
if (isWebStorageObject(expression, new Set(visitedNodes))) return true;
|
|
6800
|
+
if (isNodeOfType(expression, "ConditionalExpression")) {
|
|
6801
|
+
const consequent = stripParenExpression(expression.consequent);
|
|
6802
|
+
const alternate = stripParenExpression(expression.alternate);
|
|
6803
|
+
return (isNullishExpression(consequent) || isWebStorageFactoryResult(consequent, visitedNodes)) && (isNullishExpression(alternate) || isWebStorageFactoryResult(alternate, visitedNodes)) && (!isNullishExpression(consequent) || !isNullishExpression(alternate));
|
|
6804
|
+
}
|
|
6805
|
+
if (isNodeOfType(expression, "LogicalExpression")) {
|
|
6806
|
+
if (expression.operator === "&&") return isWebStorageFactoryResult(expression.right, visitedNodes);
|
|
6807
|
+
const left = stripParenExpression(expression.left);
|
|
6808
|
+
const right = stripParenExpression(expression.right);
|
|
6809
|
+
const isLeftStorage = isWebStorageFactoryResult(left, visitedNodes);
|
|
6810
|
+
const isRightStorage = isWebStorageFactoryResult(right, visitedNodes);
|
|
6811
|
+
return (isLeftStorage || isNullishExpression(left)) && (isRightStorage || isNullishExpression(right)) && (isLeftStorage || isRightStorage);
|
|
6812
|
+
}
|
|
6813
|
+
return false;
|
|
6814
|
+
};
|
|
6815
|
+
const isWebStorageObject = (node, visitedNodes = /* @__PURE__ */ new Set()) => {
|
|
6816
|
+
const expression = stripParenExpression(node);
|
|
6817
|
+
if (visitedNodes.has(expression)) return false;
|
|
6818
|
+
visitedNodes.add(expression);
|
|
6819
|
+
if (isDirectWebStorageObject(expression)) return true;
|
|
6820
|
+
if (isNodeOfType(expression, "Identifier")) {
|
|
6821
|
+
const initializer = immutableInitializer(expression);
|
|
6822
|
+
return initializer ? isWebStorageObject(initializer, new Set(visitedNodes)) : false;
|
|
6823
|
+
}
|
|
6824
|
+
if (!isNodeOfType(expression, "CallExpression")) return false;
|
|
6825
|
+
const callee = stripParenExpression(expression.callee);
|
|
6826
|
+
if (!isNodeOfType(callee, "Identifier")) return false;
|
|
6827
|
+
const factory = immutableInitializer(callee);
|
|
6828
|
+
if (!isFunctionLike$1(factory)) return false;
|
|
6829
|
+
if (isNodeOfType(factory, "ArrowFunctionExpression") && !isNodeOfType(factory.body, "BlockStatement")) return isWebStorageFactoryResult(factory.body, visitedNodes);
|
|
6830
|
+
let didReturnWebStorage = false;
|
|
6831
|
+
for (const returnStatement of collectFunctionReturnStatements(factory)) {
|
|
6832
|
+
if (!returnStatement.argument) continue;
|
|
6833
|
+
const strippedReturn = stripParenExpression(returnStatement.argument);
|
|
6834
|
+
if (isNullishExpression(strippedReturn)) continue;
|
|
6835
|
+
if (!isWebStorageFactoryResult(strippedReturn, visitedNodes)) return false;
|
|
6836
|
+
didReturnWebStorage = true;
|
|
6837
|
+
}
|
|
6838
|
+
return didReturnWebStorage;
|
|
6567
6839
|
};
|
|
6568
6840
|
const resolveStaticKeyString = (node) => {
|
|
6569
6841
|
if (isNodeOfType(node, "Literal") && typeof node.value === "string") return node.value;
|
|
@@ -6583,6 +6855,56 @@ const staticMemberName = (member) => {
|
|
|
6583
6855
|
if (member.computed && isNodeOfType(member.property, "Literal") && typeof member.property.value === "string") return member.property.value;
|
|
6584
6856
|
return null;
|
|
6585
6857
|
};
|
|
6858
|
+
const parameterIndex = (expression, parameterSymbolIds, scopes, canUnwrapSerialization, visitedNodes = /* @__PURE__ */ new Set()) => {
|
|
6859
|
+
const strippedExpression = stripParenExpression(expression);
|
|
6860
|
+
if (visitedNodes.has(strippedExpression)) return null;
|
|
6861
|
+
visitedNodes.add(strippedExpression);
|
|
6862
|
+
if (isNodeOfType(strippedExpression, "Identifier")) {
|
|
6863
|
+
const directSymbolId = scopes.symbolFor(strippedExpression)?.id;
|
|
6864
|
+
const directIndex = directSymbolId === void 0 ? -1 : parameterSymbolIds.indexOf(directSymbolId);
|
|
6865
|
+
if (directIndex !== -1) return directIndex;
|
|
6866
|
+
const initializer = immutableInitializer(strippedExpression);
|
|
6867
|
+
return initializer ? parameterIndex(initializer, parameterSymbolIds, scopes, canUnwrapSerialization, visitedNodes) : null;
|
|
6868
|
+
}
|
|
6869
|
+
if (canUnwrapSerialization && isNodeOfType(strippedExpression, "CallExpression") && isNodeOfType(strippedExpression.callee, "MemberExpression") && !strippedExpression.callee.computed && isNodeOfType(strippedExpression.callee.object, "Identifier") && strippedExpression.callee.object.name === "JSON" && isNodeOfType(strippedExpression.callee.property, "Identifier") && strippedExpression.callee.property.name === "stringify") {
|
|
6870
|
+
const serializedArgument = strippedExpression.arguments[0];
|
|
6871
|
+
return serializedArgument ? parameterIndex(serializedArgument, parameterSymbolIds, scopes, true, visitedNodes) : null;
|
|
6872
|
+
}
|
|
6873
|
+
return null;
|
|
6874
|
+
};
|
|
6875
|
+
const storageHelperSinkCache = /* @__PURE__ */ new WeakMap();
|
|
6876
|
+
const findStorageHelperSinks = (functionNode, scopes) => {
|
|
6877
|
+
const cachedSinks = storageHelperSinkCache.get(functionNode);
|
|
6878
|
+
if (cachedSinks) return cachedSinks;
|
|
6879
|
+
if (!isNodeOfType(functionNode, "FunctionDeclaration") && !isNodeOfType(functionNode, "FunctionExpression") && !isNodeOfType(functionNode, "ArrowFunctionExpression")) {
|
|
6880
|
+
storageHelperSinkCache.set(functionNode, []);
|
|
6881
|
+
return [];
|
|
6882
|
+
}
|
|
6883
|
+
const parameterSymbolIds = stripThisParameter(functionNode.params).map((parameter) => {
|
|
6884
|
+
const strippedParameter = stripParenExpression(parameter);
|
|
6885
|
+
const identifier = isNodeOfType(strippedParameter, "Identifier") ? strippedParameter : isNodeOfType(strippedParameter, "AssignmentPattern") && isNodeOfType(strippedParameter.left, "Identifier") ? strippedParameter.left : null;
|
|
6886
|
+
return identifier ? scopes.symbolFor(identifier)?.id ?? null : null;
|
|
6887
|
+
});
|
|
6888
|
+
const helperSinks = [];
|
|
6889
|
+
walkAst(functionNode.body, (child) => {
|
|
6890
|
+
if (child !== functionNode.body && isFunctionLike$1(child)) return false;
|
|
6891
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
6892
|
+
const callee = stripParenExpression(child.callee);
|
|
6893
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier") || callee.property.name !== "setItem" || !isWebStorageObject(callee.object)) return;
|
|
6894
|
+
const keyExpression = child.arguments[0];
|
|
6895
|
+
const valueExpression = child.arguments[1];
|
|
6896
|
+
if (!keyExpression || !valueExpression) return;
|
|
6897
|
+
const keyParameterIndex = parameterIndex(keyExpression, parameterSymbolIds, scopes, false);
|
|
6898
|
+
const valueParameterIndex = parameterIndex(valueExpression, parameterSymbolIds, scopes, true);
|
|
6899
|
+
if (keyParameterIndex === null || valueParameterIndex === null) return;
|
|
6900
|
+
helperSinks.push({
|
|
6901
|
+
keyParameterIndex,
|
|
6902
|
+
valueParameterIndex
|
|
6903
|
+
});
|
|
6904
|
+
});
|
|
6905
|
+
storageHelperSinkCache.set(functionNode, helperSinks);
|
|
6906
|
+
return helperSinks;
|
|
6907
|
+
};
|
|
6586
6908
|
const authTokenInWebStorage = defineRule({
|
|
6587
6909
|
id: "auth-token-in-web-storage",
|
|
6588
6910
|
title: "Auth token in web storage",
|
|
@@ -6590,14 +6912,23 @@ const authTokenInWebStorage = defineRule({
|
|
|
6590
6912
|
recommendation: "Don't persist auth tokens (JWTs, access/refresh tokens, secrets) in `localStorage`/`sessionStorage`; they're readable by any XSS. Use an `HttpOnly` cookie set by the server.",
|
|
6591
6913
|
create: skipNonProductionFiles((context) => ({
|
|
6592
6914
|
CallExpression(node) {
|
|
6593
|
-
const callee = node.callee;
|
|
6594
|
-
|
|
6595
|
-
if (!isNodeOfType(callee.property, "Identifier")
|
|
6596
|
-
|
|
6597
|
-
|
|
6598
|
-
if (
|
|
6599
|
-
|
|
6600
|
-
|
|
6915
|
+
const callee = stripParenExpression(node.callee);
|
|
6916
|
+
const keyArguments = [];
|
|
6917
|
+
if (isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && callee.property.name === "setItem" && isWebStorageObject(stripParenExpression(callee.object))) {
|
|
6918
|
+
const keyArgument = node.arguments[0];
|
|
6919
|
+
if (keyArgument) keyArguments.push(keyArgument);
|
|
6920
|
+
} else if (isNodeOfType(callee, "Identifier")) {
|
|
6921
|
+
const helperFunction = immutableInitializer(callee);
|
|
6922
|
+
const helperSinks = helperFunction ? findStorageHelperSinks(helperFunction, context.scopes) : [];
|
|
6923
|
+
for (const helperSink of helperSinks) {
|
|
6924
|
+
const keyArgument = node.arguments[helperSink.keyParameterIndex];
|
|
6925
|
+
if (keyArgument && node.arguments[helperSink.valueParameterIndex]) keyArguments.push(keyArgument);
|
|
6926
|
+
}
|
|
6927
|
+
}
|
|
6928
|
+
if (!keyArguments.some((keyArgument) => {
|
|
6929
|
+
const keyString = resolveStaticKeyString(keyArgument);
|
|
6930
|
+
return keyString !== null && isAuthCredentialKey(keyString);
|
|
6931
|
+
})) return;
|
|
6601
6932
|
context.report({
|
|
6602
6933
|
node,
|
|
6603
6934
|
message: MESSAGE$60
|
|
@@ -6845,9 +7176,6 @@ const isCreateElementCall = (node) => {
|
|
|
6845
7176
|
return false;
|
|
6846
7177
|
};
|
|
6847
7178
|
//#endregion
|
|
6848
|
-
//#region src/plugin/utils/is-nullish-expression.ts
|
|
6849
|
-
const isNullishExpression = (expression) => isNodeOfType(expression, "Literal") && expression.value === null || isNodeOfType(expression, "Identifier") && expression.name === "undefined" || isNodeOfType(expression, "UnaryExpression") && expression.operator === "void";
|
|
6850
|
-
//#endregion
|
|
6851
7179
|
//#region src/plugin/rules/react-builtins/button-has-type.ts
|
|
6852
7180
|
const MISSING_MESSAGE$2 = "Your users can submit the form by accident because a `<button>` with no `type` defaults to submit.";
|
|
6853
7181
|
const INVALID_MESSAGE = "This button has an invalid `type`, so the browser may treat it like a submit button.";
|
|
@@ -7309,6 +7637,7 @@ const areExpressionsStructurallyEqual = (a, b) => {
|
|
|
7309
7637
|
if (a.type !== b.type) return false;
|
|
7310
7638
|
if (isNodeOfType(a, "ThisExpression")) return true;
|
|
7311
7639
|
if (isNodeOfType(a, "Identifier") && isNodeOfType(b, "Identifier")) return a.name === b.name;
|
|
7640
|
+
if (isNodeOfType(a, "PrivateIdentifier") && isNodeOfType(b, "PrivateIdentifier")) return a.name === b.name;
|
|
7312
7641
|
if (isNodeOfType(a, "Literal") && isNodeOfType(b, "Literal")) return a.value === b.value;
|
|
7313
7642
|
if (isNodeOfType(a, "MemberExpression") && isNodeOfType(b, "MemberExpression")) {
|
|
7314
7643
|
if (a.computed !== b.computed) return false;
|
|
@@ -8364,17 +8693,6 @@ const createMethodMutationAnalysis = (context) => {
|
|
|
8364
8693
|
//#region src/plugin/utils/is-member-property.ts
|
|
8365
8694
|
const isMemberProperty = (node, propertyName) => Boolean(node && isNodeOfType(node, "MemberExpression") && isNodeOfType(node.property, "Identifier") && node.property.name === propertyName);
|
|
8366
8695
|
//#endregion
|
|
8367
|
-
//#region src/plugin/utils/collect-function-return-statements.ts
|
|
8368
|
-
const collectFunctionReturnStatements = (functionNode) => {
|
|
8369
|
-
if (!isFunctionLike$1(functionNode) || !isNodeOfType(functionNode.body, "BlockStatement")) return [];
|
|
8370
|
-
const returnStatements = [];
|
|
8371
|
-
walkAst(functionNode.body, (node) => {
|
|
8372
|
-
if (node !== functionNode.body && (isFunctionLike$1(node) || isNodeOfType(node, "ClassDeclaration") || isNodeOfType(node, "ClassExpression"))) return false;
|
|
8373
|
-
if (isNodeOfType(node, "ReturnStatement")) returnStatements.push(node);
|
|
8374
|
-
});
|
|
8375
|
-
return returnStatements;
|
|
8376
|
-
};
|
|
8377
|
-
//#endregion
|
|
8378
8696
|
//#region src/plugin/utils/statement-always-exits.ts
|
|
8379
8697
|
const statementAlwaysExits = (statement) => {
|
|
8380
8698
|
if (isNodeOfType(statement, "ReturnStatement") || isNodeOfType(statement, "ThrowStatement")) return true;
|
|
@@ -8641,7 +8959,7 @@ const isReactNamespaceImport = (identifier, scopes) => {
|
|
|
8641
8959
|
if (!symbol || !isImportedFromReact(symbol)) return false;
|
|
8642
8960
|
return isNodeOfType(symbol.declarationNode, "ImportDefaultSpecifier") || isNodeOfType(symbol.declarationNode, "ImportNamespaceSpecifier") || getImportedName(symbol.declarationNode) === "default";
|
|
8643
8961
|
};
|
|
8644
|
-
const isReactNamespaceReceiver = (receiver, scopes, options) => {
|
|
8962
|
+
const isReactNamespaceReceiver$1 = (receiver, scopes, options) => {
|
|
8645
8963
|
if (!isNodeOfType(receiver, "Identifier")) return false;
|
|
8646
8964
|
if (isReactNamespaceImport(receiver, scopes)) return true;
|
|
8647
8965
|
return Boolean(options.allowGlobalReactNamespace && receiver.name === "React" && scopes.isGlobalReference(receiver));
|
|
@@ -8654,7 +8972,7 @@ const isDestructuredReactApiBinding = (identifier, apiNames, scopes, options) =>
|
|
|
8654
8972
|
for (const property of pattern.properties) {
|
|
8655
8973
|
if (!isNodeOfType(property, "Property") || property.value !== symbol.bindingIdentifier) continue;
|
|
8656
8974
|
const propertyName = getStaticPropertyKeyName(property);
|
|
8657
|
-
return Boolean(propertyName && includesApiName(apiNames, propertyName) && isReactNamespaceReceiver(stripParenExpression(symbol.initializer), scopes, options));
|
|
8975
|
+
return Boolean(propertyName && includesApiName(apiNames, propertyName) && isReactNamespaceReceiver$1(stripParenExpression(symbol.initializer), scopes, options));
|
|
8658
8976
|
}
|
|
8659
8977
|
return false;
|
|
8660
8978
|
};
|
|
@@ -8678,7 +8996,7 @@ const isReactApiCallee = (rawCallee, apiNames, scopes, options, visitedSymbolIds
|
|
|
8678
8996
|
return Boolean(options.allowUnboundBareCalls && includesApiName(apiNames, callee.name) && scopes.isGlobalReference(callee));
|
|
8679
8997
|
}
|
|
8680
8998
|
if (!isNodeOfType(callee, "MemberExpression") || !includesApiName(apiNames, getStaticPropertyName(callee) ?? "")) return false;
|
|
8681
|
-
return isReactNamespaceReceiver(stripParenExpression(callee.object), scopes, options);
|
|
8999
|
+
return isReactNamespaceReceiver$1(stripParenExpression(callee.object), scopes, options);
|
|
8682
9000
|
};
|
|
8683
9001
|
//#endregion
|
|
8684
9002
|
//#region src/plugin/utils/is-proven-browser-api-receiver.ts
|
|
@@ -9083,119 +9401,6 @@ const findReExportTargetsForName = (programRoot, exportedName) => {
|
|
|
9083
9401
|
return exportAllTargets;
|
|
9084
9402
|
};
|
|
9085
9403
|
//#endregion
|
|
9086
|
-
//#region src/plugin/utils/attach-parent-references.ts
|
|
9087
|
-
const attachParentReferences = (root) => {
|
|
9088
|
-
const visit = (node, parent) => {
|
|
9089
|
-
const writableNode = node;
|
|
9090
|
-
writableNode.parent = parent;
|
|
9091
|
-
const nodeRecord = node;
|
|
9092
|
-
for (const key of Object.keys(nodeRecord)) {
|
|
9093
|
-
if (key === "parent") continue;
|
|
9094
|
-
const child = nodeRecord[key];
|
|
9095
|
-
if (Array.isArray(child)) {
|
|
9096
|
-
for (const item of child) if (isAstNode(item)) visit(item, node);
|
|
9097
|
-
} else if (isAstNode(child)) visit(child, node);
|
|
9098
|
-
}
|
|
9099
|
-
};
|
|
9100
|
-
visit(root, null);
|
|
9101
|
-
};
|
|
9102
|
-
//#endregion
|
|
9103
|
-
//#region src/plugin/utils/cross-file-probe-recorder.ts
|
|
9104
|
-
let activeProbeTrace = null;
|
|
9105
|
-
const recordExistenceProbe = (absolutePath) => {
|
|
9106
|
-
activeProbeTrace?.existencePaths.add(absolutePath);
|
|
9107
|
-
};
|
|
9108
|
-
const recordContentProbe = (absolutePath) => {
|
|
9109
|
-
activeProbeTrace?.contentPaths.add(absolutePath);
|
|
9110
|
-
};
|
|
9111
|
-
const isProbeRecorderActive = () => activeProbeTrace !== null;
|
|
9112
|
-
const collectCrossFileProbes = (collect) => {
|
|
9113
|
-
const previousTrace = activeProbeTrace;
|
|
9114
|
-
const trace = {
|
|
9115
|
-
existencePaths: /* @__PURE__ */ new Set(),
|
|
9116
|
-
contentPaths: /* @__PURE__ */ new Set()
|
|
9117
|
-
};
|
|
9118
|
-
activeProbeTrace = trace;
|
|
9119
|
-
try {
|
|
9120
|
-
collect();
|
|
9121
|
-
} finally {
|
|
9122
|
-
activeProbeTrace = previousTrace;
|
|
9123
|
-
}
|
|
9124
|
-
return trace;
|
|
9125
|
-
};
|
|
9126
|
-
//#endregion
|
|
9127
|
-
//#region src/plugin/utils/parse-source-file.ts
|
|
9128
|
-
const FILENAME_TO_LANG = {
|
|
9129
|
-
".ts": "ts",
|
|
9130
|
-
".tsx": "tsx",
|
|
9131
|
-
".js": "js",
|
|
9132
|
-
".jsx": "jsx",
|
|
9133
|
-
".mjs": "js",
|
|
9134
|
-
".cjs": "js",
|
|
9135
|
-
".mts": "ts",
|
|
9136
|
-
".cts": "ts"
|
|
9137
|
-
};
|
|
9138
|
-
const resolveLang = (filename) => {
|
|
9139
|
-
return FILENAME_TO_LANG[path.extname(filename).toLowerCase()] ?? "tsx";
|
|
9140
|
-
};
|
|
9141
|
-
const parseSourceText = ({ filename, sourceText, shouldAttachParentReferences = true }) => {
|
|
9142
|
-
try {
|
|
9143
|
-
const result = parseSync(filename, sourceText, {
|
|
9144
|
-
astType: "ts",
|
|
9145
|
-
lang: resolveLang(filename)
|
|
9146
|
-
});
|
|
9147
|
-
if (result.errors.some((parseError) => parseError.severity === "Error")) return null;
|
|
9148
|
-
const parsedProgram = result.program;
|
|
9149
|
-
if (shouldAttachParentReferences) attachParentReferences(parsedProgram);
|
|
9150
|
-
return parsedProgram;
|
|
9151
|
-
} catch {
|
|
9152
|
-
return null;
|
|
9153
|
-
}
|
|
9154
|
-
};
|
|
9155
|
-
const parseCache = /* @__PURE__ */ new Map();
|
|
9156
|
-
const parseSourceFile = (absoluteFilePath) => {
|
|
9157
|
-
if (!(absoluteFilePath.endsWith(".d.ts") || absoluteFilePath.endsWith(".d.mts") || absoluteFilePath.endsWith(".d.cts"))) recordContentProbe(absoluteFilePath);
|
|
9158
|
-
let fileStat;
|
|
9159
|
-
try {
|
|
9160
|
-
fileStat = fs.statSync(absoluteFilePath);
|
|
9161
|
-
} catch {
|
|
9162
|
-
return null;
|
|
9163
|
-
}
|
|
9164
|
-
if (!fileStat.isFile()) return null;
|
|
9165
|
-
if (fileStat.size > 2e6) return null;
|
|
9166
|
-
const cached = parseCache.get(absoluteFilePath);
|
|
9167
|
-
if (cached && cached.mtimeMs === fileStat.mtimeMs && cached.size === fileStat.size) return cached.program;
|
|
9168
|
-
if (absoluteFilePath.endsWith(".d.ts") || absoluteFilePath.endsWith(".d.mts") || absoluteFilePath.endsWith(".d.cts")) {
|
|
9169
|
-
parseCache.set(absoluteFilePath, {
|
|
9170
|
-
mtimeMs: fileStat.mtimeMs,
|
|
9171
|
-
size: fileStat.size,
|
|
9172
|
-
program: null
|
|
9173
|
-
});
|
|
9174
|
-
return null;
|
|
9175
|
-
}
|
|
9176
|
-
let sourceText;
|
|
9177
|
-
try {
|
|
9178
|
-
sourceText = fs.readFileSync(absoluteFilePath, "utf8");
|
|
9179
|
-
} catch {
|
|
9180
|
-
parseCache.set(absoluteFilePath, {
|
|
9181
|
-
mtimeMs: fileStat.mtimeMs,
|
|
9182
|
-
size: fileStat.size,
|
|
9183
|
-
program: null
|
|
9184
|
-
});
|
|
9185
|
-
return null;
|
|
9186
|
-
}
|
|
9187
|
-
const parsedProgram = parseSourceText({
|
|
9188
|
-
filename: absoluteFilePath,
|
|
9189
|
-
sourceText
|
|
9190
|
-
});
|
|
9191
|
-
parseCache.set(absoluteFilePath, {
|
|
9192
|
-
mtimeMs: fileStat.mtimeMs,
|
|
9193
|
-
size: fileStat.size,
|
|
9194
|
-
program: parsedProgram
|
|
9195
|
-
});
|
|
9196
|
-
return parsedProgram;
|
|
9197
|
-
};
|
|
9198
|
-
//#endregion
|
|
9199
9404
|
//#region src/plugin/utils/resolve-relative-import-path.ts
|
|
9200
9405
|
const MODULE_FILE_EXTENSIONS = [
|
|
9201
9406
|
".ts",
|
|
@@ -14662,6 +14867,29 @@ const getReleaseVerbName = (node) => {
|
|
|
14662
14867
|
}
|
|
14663
14868
|
return null;
|
|
14664
14869
|
};
|
|
14870
|
+
const isRetainedAbortControllerRefRelease = (releaseReceiver, usage, context) => {
|
|
14871
|
+
const releaseFunction = findEnclosingFunction$1(releaseReceiver);
|
|
14872
|
+
const usageFunction = findEnclosingFunction$1(usage.node);
|
|
14873
|
+
if (!releaseFunction || !usageFunction || !isFunctionLike$1(usageFunction) || !isReturnedEffectCleanupFunction(releaseFunction) || !hasReactRefCurrentOrigin(releaseReceiver, context.scopes)) return false;
|
|
14874
|
+
const controllerKey = getListenerAbortControllerKey(usage, context);
|
|
14875
|
+
const refCurrentKey = resolveExpressionKey(releaseReceiver, context);
|
|
14876
|
+
if (controllerKey === null || refCurrentKey === null) return false;
|
|
14877
|
+
const usageFunctionBody = usageFunction.body;
|
|
14878
|
+
const previousAbortCalls = [];
|
|
14879
|
+
const ownershipAssignments = [];
|
|
14880
|
+
walkAst(usageFunctionBody, (child) => {
|
|
14881
|
+
if (child !== usageFunctionBody && isFunctionLike$1(child)) return false;
|
|
14882
|
+
if (isNodeOfType(child, "AssignmentExpression") && resolveExpressionKey(child.left, context) === refCurrentKey && resolveExpressionKey(child.right, context) === controllerKey) {
|
|
14883
|
+
ownershipAssignments.push(child);
|
|
14884
|
+
return;
|
|
14885
|
+
}
|
|
14886
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
14887
|
+
const childCallee = isNodeOfType(child.callee, "ChainExpression") ? child.callee.expression : stripParenExpression(child.callee);
|
|
14888
|
+
if (isNodeOfType(childCallee, "MemberExpression") && !childCallee.computed && isNodeOfType(childCallee.property, "Identifier") && childCallee.property.name === "abort" && resolveExpressionKey(childCallee.object, context) === refCurrentKey) previousAbortCalls.push(child);
|
|
14889
|
+
});
|
|
14890
|
+
const safeOwnershipAssignments = ownershipAssignments.filter((assignment) => doMatchingNodesCoverEveryPathBeforeUsage(assignment, previousAbortCalls, usageFunction, context));
|
|
14891
|
+
return doMatchingNodesCoverEveryPathBeforeUsage(usage.node, safeOwnershipAssignments, usageFunction, context);
|
|
14892
|
+
};
|
|
14665
14893
|
const doesReleaseCallMatchUsage = (node, usage, context) => {
|
|
14666
14894
|
const callNode = isNodeOfType(node, "ChainExpression") ? node.expression : node;
|
|
14667
14895
|
if (!isNodeOfType(callNode, "CallExpression")) return false;
|
|
@@ -14681,6 +14909,7 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
|
|
|
14681
14909
|
if (usage.kind === "socket") return usage.handleKey !== null && releaseReceiverKey === usage.handleKey && (SOCKET_RELEASE_VERB_NAMES.has(releaseVerbName) || UNIVERSAL_RELEASE_VERB_NAMES.has(releaseVerbName));
|
|
14682
14910
|
if (usage.handleKey !== null && releaseReceiverKey === usage.handleKey && (releaseVerbName === "unsubscribe" || releaseVerbName === "unsub" || releaseVerbName === "close" || releaseVerbName === "unwatch" || releaseVerbName === "unlisten" || BOUND_RESOURCE_RELEASE_METHOD_NAMES.has(releaseVerbName))) return true;
|
|
14683
14911
|
if (releaseVerbName === "abort" && releaseReceiverKey === getListenerAbortControllerKey(usage, context)) return true;
|
|
14912
|
+
if (releaseVerbName === "abort" && isRetainedAbortControllerRefRelease(callee.object, usage, context)) return true;
|
|
14684
14913
|
if (usage.receiverKey === null || releaseReceiverKey !== usage.receiverKey) return false;
|
|
14685
14914
|
const pairedVerbNames = usage.registrationVerbName ? PAIRED_RELEASE_VERB_NAMES_BY_REGISTRATION_VERB.get(usage.registrationVerbName) : null;
|
|
14686
14915
|
if (!pairedVerbNames || !matchesPairedReleaseVerb(releaseVerbName, pairedVerbNames)) return false;
|
|
@@ -14714,9 +14943,12 @@ const doesReleaseCallMatchUsage = (node, usage, context) => {
|
|
|
14714
14943
|
return isNodeOfType(handlerArgument, "Literal") && handlerArgument.value === null;
|
|
14715
14944
|
}
|
|
14716
14945
|
if (releaseVerbName === "removeEventListener" || releaseVerbName === "removeListener" || releaseVerbName === "off") {
|
|
14717
|
-
const
|
|
14946
|
+
const usesUnaryListenerSignature = usage.registrationVerbName === "addListener" && isNodeOfType(usage.node, "CallExpression") && usage.node.arguments?.length === 1 && callNode.arguments?.length === 1;
|
|
14947
|
+
const releaseHandler = usesUnaryListenerSignature ? callNode.arguments?.[0] : callNode.arguments?.[1];
|
|
14718
14948
|
if (!releaseHandler) return releaseVerbName === "off";
|
|
14719
|
-
|
|
14949
|
+
const expectedHandlerKey = usesUnaryListenerSignature ? usage.eventKey : usage.handlerKey;
|
|
14950
|
+
const registrationHandler = isNodeOfType(usage.node, "CallExpression") ? usage.node.arguments?.[usesUnaryListenerSignature ? 0 : 1] : null;
|
|
14951
|
+
return expectedHandlerKey !== null && resolveExpressionKey(releaseHandler, context) === expectedHandlerKey || registrationHandler !== null && resolveStableValue(releaseHandler, context) === resolveStableValue(registrationHandler, context);
|
|
14720
14952
|
}
|
|
14721
14953
|
if (releaseVerbName === "unobserve" && usage.eventKey !== null) return releaseEventKey === usage.eventKey;
|
|
14722
14954
|
return true;
|
|
@@ -14729,8 +14961,7 @@ const isReturnedEffectCleanupFunction = (functionNode) => {
|
|
|
14729
14961
|
currentNode = parentNode;
|
|
14730
14962
|
parentNode = currentNode.parent;
|
|
14731
14963
|
}
|
|
14732
|
-
|
|
14733
|
-
const effectCallback = findEnclosingFunction$1(parentNode);
|
|
14964
|
+
const effectCallback = isNodeOfType(parentNode, "ReturnStatement") && parentNode.argument === currentNode ? findEnclosingFunction$1(parentNode) : isNodeOfType(parentNode, "ArrowFunctionExpression") && parentNode.body === currentNode ? parentNode : null;
|
|
14734
14965
|
const effectCall = effectCallback?.parent;
|
|
14735
14966
|
return Boolean(effectCallback && isNodeOfType(effectCall, "CallExpression") && isHookCall$2(effectCall, CLEANUP_EFFECT_HOOK_NAMES));
|
|
14736
14967
|
};
|
|
@@ -14742,6 +14973,25 @@ const isPotentiallyReachableFunction = (functionNode, context) => {
|
|
|
14742
14973
|
if (!symbol) return false;
|
|
14743
14974
|
return symbol.references.some((reference) => findEnclosingFunction$1(reference.identifier) !== functionNode);
|
|
14744
14975
|
};
|
|
14976
|
+
const isSelfReleasingListenerRelease = (releaseNode, releaseFunction, usage, context) => {
|
|
14977
|
+
if (usage.kind !== "subscribe" || usage.registrationVerbName !== "addEventListener" || usage.receiverKey === null || usage.eventKey === null || !isNodeOfType(usage.node, "CallExpression") || !isFunctionLike$1(releaseFunction) || releaseFunction.async || releaseFunction.generator || !isNodeOfType(releaseFunction.body, "BlockStatement") || !doMatchingNodesCoverEveryPathFromFunctionEntry(releaseFunction, [releaseNode], context)) return false;
|
|
14978
|
+
const registrationCapture = resolveEventListenerCapture(usage.node.arguments?.[2], { allowIndeterminateEntries: true });
|
|
14979
|
+
const releaseCall = isNodeOfType(releaseNode, "ChainExpression") ? releaseNode.expression : releaseNode;
|
|
14980
|
+
if (!isNodeOfType(releaseCall, "CallExpression")) return false;
|
|
14981
|
+
const releaseCapture = resolveEventListenerCapture(releaseCall.arguments?.[2], { allowIndeterminateEntries: true });
|
|
14982
|
+
if (registrationCapture === null || releaseCapture === null || registrationCapture !== releaseCapture) return false;
|
|
14983
|
+
const ownerFunction = findEnclosingFunction$1(releaseFunction);
|
|
14984
|
+
if (!ownerFunction || !isFunctionLike$1(ownerFunction)) return false;
|
|
14985
|
+
const triggerRegistrations = [];
|
|
14986
|
+
walkAst(ownerFunction.body, (child) => {
|
|
14987
|
+
if (child !== ownerFunction.body && isFunctionLike$1(child)) return false;
|
|
14988
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
14989
|
+
const registrationDetails = getCallRegistrationDetails(child, context);
|
|
14990
|
+
if (registrationDetails.registrationVerbName === "addEventListener" && registrationDetails.receiverKey === usage.receiverKey && resolveStableValue(child.arguments?.[1], context) === releaseFunction) triggerRegistrations.push(child);
|
|
14991
|
+
});
|
|
14992
|
+
if (triggerRegistrations.some((triggerRegistration) => triggerRegistration === usage.node)) return true;
|
|
14993
|
+
return doMatchingNodesCoverEveryPathAfterUsage(usage.node, triggerRegistrations, context) || doMatchingNodesCoverEveryPathBeforeUsage(usage.node, triggerRegistrations, ownerFunction, context);
|
|
14994
|
+
};
|
|
14745
14995
|
const isReleaseReachableForUsage = (releaseNode, usage, context) => {
|
|
14746
14996
|
if (!isNodeReachableWithinFunction(releaseNode, context)) return false;
|
|
14747
14997
|
const releaseFunction = findEnclosingFunction$1(releaseNode);
|
|
@@ -14749,6 +14999,7 @@ const isReleaseReachableForUsage = (releaseNode, usage, context) => {
|
|
|
14749
14999
|
if (releaseFunction === findEnclosingFunction$1(usage.node)) return true;
|
|
14750
15000
|
const usageFunction = findEnclosingFunction$1(usage.node);
|
|
14751
15001
|
if (usageFunction && isFunctionLike$1(usageFunction) && getAssignedReactRefSymbol(usageFunction, context) && isCleanupFunctionReferencedByReturn(usageFunction, releaseFunction, context)) return isReactRefCallbackCleanupOwnedByEffect(usageFunction, releaseFunction, usage, context);
|
|
15002
|
+
if (isSelfReleasingListenerRelease(releaseNode, releaseFunction, usage, context)) return true;
|
|
14752
15003
|
return isPotentiallyReachableFunction(releaseFunction, context);
|
|
14753
15004
|
};
|
|
14754
15005
|
const fileContainsReleaseForUsage = (usage, context) => {
|
|
@@ -15016,6 +15267,11 @@ const doesResourceResultEscape = (resourceNode, allowReturnedResourceEscape, all
|
|
|
15016
15267
|
parentNode = currentNode.parent;
|
|
15017
15268
|
continue;
|
|
15018
15269
|
}
|
|
15270
|
+
if (isNodeOfType(parentNode, "ConditionalExpression") && (parentNode.consequent === currentNode || parentNode.alternate === currentNode) || isNodeOfType(parentNode, "LogicalExpression") && (parentNode.right === currentNode || parentNode.left === currentNode && parentNode.operator !== "&&")) {
|
|
15271
|
+
currentNode = parentNode;
|
|
15272
|
+
parentNode = currentNode.parent;
|
|
15273
|
+
continue;
|
|
15274
|
+
}
|
|
15019
15275
|
if (isNodeOfType(parentNode, "VariableDeclarator") && parentNode.init === currentNode && isNodeOfType(parentNode.id, "Identifier") && isNodeOfType(parentNode.parent, "VariableDeclaration") && parentNode.parent.kind === "const") {
|
|
15020
15276
|
const ownerFunction = findEnclosingFunction$1(resourceNode);
|
|
15021
15277
|
const resourceSymbol = context.scopes.symbolFor(parentNode.id);
|
|
@@ -16877,7 +17133,7 @@ const collectCaptureDepKeys = (callback, scopes, declaredExactBindingKeys, allow
|
|
|
16877
17133
|
keys.add(depKey);
|
|
16878
17134
|
continue;
|
|
16879
17135
|
}
|
|
16880
|
-
const identitySourceKeys = resolveReactiveIdentitySourceKeys(symbol, scopes);
|
|
17136
|
+
const identitySourceKeys = resolvePureCalledFunctionSourceKeys(reference, symbol, scopes) ?? resolveRenderDerivedMutableSourceKeys(reference, symbol, scopes) ?? resolveReactiveIdentitySourceKeys(symbol, scopes);
|
|
16881
17137
|
if (identitySourceKeys) {
|
|
16882
17138
|
if (identitySourceKeys.size === 0) stableCapturedNames.add(depKey);
|
|
16883
17139
|
for (const identitySourceKey of identitySourceKeys) keys.add(identitySourceKey);
|
|
@@ -16960,6 +17216,161 @@ const resolveReactiveIdentitySourceKeys = (symbol, scopes) => {
|
|
|
16960
17216
|
if (symbol.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier || symbol.references.some((reference) => reference.flag !== "read")) return null;
|
|
16961
17217
|
return resolveIdentitySourceKeysFromExpression(symbol.initializer, scopes, new Set([symbol.id]));
|
|
16962
17218
|
};
|
|
17219
|
+
const isPureDerivedExpression = (expression) => {
|
|
17220
|
+
const candidate = unwrapExpression$3(expression);
|
|
17221
|
+
if (isNodeOfType(candidate, "Literal") || isNodeOfType(candidate, "Identifier")) return true;
|
|
17222
|
+
if (isNodeOfType(candidate, "MemberExpression")) return isPureDerivedExpression(candidate.object) && (!candidate.computed || isPureDerivedExpression(candidate.property));
|
|
17223
|
+
if (isNodeOfType(candidate, "BinaryExpression") || isNodeOfType(candidate, "LogicalExpression")) return isPureDerivedExpression(candidate.left) && isPureDerivedExpression(candidate.right);
|
|
17224
|
+
if (isNodeOfType(candidate, "UnaryExpression")) return candidate.operator !== "delete" && isPureDerivedExpression(candidate.argument);
|
|
17225
|
+
if (isNodeOfType(candidate, "ConditionalExpression")) return isPureDerivedExpression(candidate.test) && isPureDerivedExpression(candidate.consequent) && isPureDerivedExpression(candidate.alternate);
|
|
17226
|
+
if (isNodeOfType(candidate, "TemplateLiteral")) return candidate.expressions.every((nestedExpression) => isPureDerivedExpression(nestedExpression));
|
|
17227
|
+
return false;
|
|
17228
|
+
};
|
|
17229
|
+
const isPureDerivedStatement = (statement) => {
|
|
17230
|
+
if (isNodeOfType(statement, "BlockStatement")) return statement.body.every((nestedStatement) => isPureDerivedStatement(nestedStatement));
|
|
17231
|
+
if (isNodeOfType(statement, "ReturnStatement")) return !statement.argument || isPureDerivedExpression(statement.argument);
|
|
17232
|
+
if (isNodeOfType(statement, "IfStatement")) return isPureDerivedExpression(statement.test) && isPureDerivedStatement(statement.consequent) && (!statement.alternate || isPureDerivedStatement(statement.alternate));
|
|
17233
|
+
return false;
|
|
17234
|
+
};
|
|
17235
|
+
const isPureDerivedFunction = (functionNode) => {
|
|
17236
|
+
if (!isNodeOfType(functionNode, "FunctionDeclaration") && !isNodeOfType(functionNode, "FunctionExpression") && !isNodeOfType(functionNode, "ArrowFunctionExpression")) return false;
|
|
17237
|
+
if (functionNode.async || functionNode.generator) return false;
|
|
17238
|
+
return isNodeOfType(functionNode.body, "BlockStatement") ? isPureDerivedStatement(functionNode.body) : isPureDerivedExpression(functionNode.body);
|
|
17239
|
+
};
|
|
17240
|
+
const resolvePureCalledFunctionSourceKeys = (reference, symbol, scopes) => {
|
|
17241
|
+
if (symbol.references.some((symbolReference) => symbolReference.flag !== "read")) return null;
|
|
17242
|
+
const referenceRoot = findTransparentExpressionRoot(reference.identifier);
|
|
17243
|
+
const callExpression = referenceRoot.parent;
|
|
17244
|
+
if (!isNodeOfType(callExpression, "CallExpression") || callExpression.callee !== referenceRoot) return null;
|
|
17245
|
+
const functionNode = getFunctionValueNode(symbol);
|
|
17246
|
+
if (!functionNode || !isPureDerivedFunction(functionNode)) return null;
|
|
17247
|
+
const sourceKeys = /* @__PURE__ */ new Set();
|
|
17248
|
+
for (const capturedReference of closureCaptures(functionNode, scopes)) {
|
|
17249
|
+
const capturedSymbol = capturedReference.resolvedSymbol;
|
|
17250
|
+
if (!capturedSymbol || capturedSymbol.id === symbol.id) continue;
|
|
17251
|
+
if (isOutsideAllFunctions(capturedSymbol) || symbolHasStableValue(capturedSymbol, scopes)) continue;
|
|
17252
|
+
const capturedKey = computeDepKey(capturedReference);
|
|
17253
|
+
if (!capturedKey) return null;
|
|
17254
|
+
if (capturedKey === capturedSymbol.name) {
|
|
17255
|
+
const nestedSourceKeys = resolveReactiveIdentitySourceKeys(capturedSymbol, scopes);
|
|
17256
|
+
if (nestedSourceKeys) {
|
|
17257
|
+
for (const nestedSourceKey of nestedSourceKeys) sourceKeys.add(nestedSourceKey);
|
|
17258
|
+
continue;
|
|
17259
|
+
}
|
|
17260
|
+
}
|
|
17261
|
+
sourceKeys.add(capturedKey);
|
|
17262
|
+
}
|
|
17263
|
+
return sourceKeys.size > 0 ? sourceKeys : null;
|
|
17264
|
+
};
|
|
17265
|
+
const mergeDerivedExpressionSourceKeys = (expressions, scopes, visitedSymbolIds) => {
|
|
17266
|
+
const sourceKeys = /* @__PURE__ */ new Set();
|
|
17267
|
+
for (const expression of expressions) {
|
|
17268
|
+
const expressionSourceKeys = resolveDerivedExpressionSourceKeys(expression, scopes, visitedSymbolIds);
|
|
17269
|
+
if (!expressionSourceKeys) return null;
|
|
17270
|
+
for (const expressionSourceKey of expressionSourceKeys) sourceKeys.add(expressionSourceKey);
|
|
17271
|
+
}
|
|
17272
|
+
return sourceKeys;
|
|
17273
|
+
};
|
|
17274
|
+
const resolveDerivedExpressionSourceKeys = (expression, scopes, visitedSymbolIds) => {
|
|
17275
|
+
const candidate = unwrapExpression$3(expression);
|
|
17276
|
+
if (isNodeOfType(candidate, "Literal")) return /* @__PURE__ */ new Set();
|
|
17277
|
+
if (isNodeOfType(candidate, "Identifier")) {
|
|
17278
|
+
if (scopes.isGlobalReference(candidate)) return /* @__PURE__ */ new Set();
|
|
17279
|
+
const sourceSymbol = scopes.symbolFor(candidate);
|
|
17280
|
+
if (!sourceSymbol) return null;
|
|
17281
|
+
if (isOutsideAllFunctions(sourceSymbol) || symbolHasStableValue(sourceSymbol, scopes)) return /* @__PURE__ */ new Set();
|
|
17282
|
+
if (sourceSymbol.kind === "const" && sourceSymbol.initializer && isNodeOfType(sourceSymbol.declarationNode, "VariableDeclarator") && sourceSymbol.declarationNode.id === sourceSymbol.bindingIdentifier && sourceSymbol.references.every((sourceReference) => sourceReference.flag === "read") && !visitedSymbolIds.has(sourceSymbol.id)) {
|
|
17283
|
+
visitedSymbolIds.add(sourceSymbol.id);
|
|
17284
|
+
const sourceKeys = resolveDerivedExpressionSourceKeys(sourceSymbol.initializer, scopes, visitedSymbolIds);
|
|
17285
|
+
visitedSymbolIds.delete(sourceSymbol.id);
|
|
17286
|
+
if (sourceKeys) return sourceKeys;
|
|
17287
|
+
}
|
|
17288
|
+
return new Set([sourceSymbol.name]);
|
|
17289
|
+
}
|
|
17290
|
+
if (isNodeOfType(candidate, "MemberExpression")) {
|
|
17291
|
+
if (hasComputedMemberExpression(candidate)) return null;
|
|
17292
|
+
const sourceKey = stringifyMemberChain(candidate);
|
|
17293
|
+
const rootIdentifier = getMemberRootIdentifier(candidate);
|
|
17294
|
+
const rootSymbol = rootIdentifier ? scopes.symbolFor(rootIdentifier) : null;
|
|
17295
|
+
if (!sourceKey || !rootSymbol) return null;
|
|
17296
|
+
if (isOutsideAllFunctions(rootSymbol) || symbolHasStableValue(rootSymbol, scopes)) return /* @__PURE__ */ new Set();
|
|
17297
|
+
return new Set([sourceKey]);
|
|
17298
|
+
}
|
|
17299
|
+
if (isNodeOfType(candidate, "BinaryExpression") || isNodeOfType(candidate, "LogicalExpression")) return mergeDerivedExpressionSourceKeys([candidate.left, candidate.right], scopes, visitedSymbolIds);
|
|
17300
|
+
if (isNodeOfType(candidate, "UnaryExpression") && candidate.operator !== "delete") return resolveDerivedExpressionSourceKeys(candidate.argument, scopes, visitedSymbolIds);
|
|
17301
|
+
if (isNodeOfType(candidate, "ConditionalExpression")) return mergeDerivedExpressionSourceKeys([
|
|
17302
|
+
candidate.test,
|
|
17303
|
+
candidate.consequent,
|
|
17304
|
+
candidate.alternate
|
|
17305
|
+
], scopes, visitedSymbolIds);
|
|
17306
|
+
if (isNodeOfType(candidate, "TemplateLiteral")) return mergeDerivedExpressionSourceKeys(candidate.expressions, scopes, visitedSymbolIds);
|
|
17307
|
+
if (isNodeOfType(candidate, "NewExpression")) {
|
|
17308
|
+
const callee = unwrapExpression$3(candidate.callee);
|
|
17309
|
+
if (!isNodeOfType(callee, "Identifier") || callee.name !== "Error" || !scopes.isGlobalReference(callee)) return null;
|
|
17310
|
+
const argumentsToAnalyze = [];
|
|
17311
|
+
for (const argument of candidate.arguments) {
|
|
17312
|
+
if (!isAstNode(argument) || isNodeOfType(argument, "SpreadElement")) return null;
|
|
17313
|
+
argumentsToAnalyze.push(argument);
|
|
17314
|
+
}
|
|
17315
|
+
return mergeDerivedExpressionSourceKeys(argumentsToAnalyze, scopes, visitedSymbolIds);
|
|
17316
|
+
}
|
|
17317
|
+
return null;
|
|
17318
|
+
};
|
|
17319
|
+
const resolveWriteControlSourceKeys = (assignment, boundaryFunction, scopes) => {
|
|
17320
|
+
const sourceKeys = /* @__PURE__ */ new Set();
|
|
17321
|
+
let currentNode = assignment;
|
|
17322
|
+
while (currentNode.parent && currentNode.parent !== boundaryFunction) {
|
|
17323
|
+
const parentNode = currentNode.parent;
|
|
17324
|
+
if (isNodeOfType(parentNode, "IfStatement")) {
|
|
17325
|
+
if (parentNode.test === currentNode) return null;
|
|
17326
|
+
const testSourceKeys = resolveDerivedExpressionSourceKeys(parentNode.test, scopes, /* @__PURE__ */ new Set());
|
|
17327
|
+
if (!testSourceKeys) return null;
|
|
17328
|
+
for (const testSourceKey of testSourceKeys) sourceKeys.add(testSourceKey);
|
|
17329
|
+
} else if (!isNodeOfType(parentNode, "ExpressionStatement") && !isNodeOfType(parentNode, "BlockStatement")) return null;
|
|
17330
|
+
currentNode = parentNode;
|
|
17331
|
+
}
|
|
17332
|
+
return currentNode.parent === boundaryFunction ? sourceKeys : null;
|
|
17333
|
+
};
|
|
17334
|
+
const isReadOnlyInitialStateUse = (referenceNode, scopes) => {
|
|
17335
|
+
const referenceRoot = findTransparentExpressionRoot(referenceNode);
|
|
17336
|
+
const callExpression = referenceRoot.parent;
|
|
17337
|
+
return isNodeOfType(callExpression, "CallExpression") && callExpression.arguments.some((argument) => argument === referenceRoot) && isReactApiCall(callExpression, "useState", scopes, {
|
|
17338
|
+
allowGlobalReactNamespace: true,
|
|
17339
|
+
allowUnboundBareCalls: true,
|
|
17340
|
+
resolveNamedAliases: true
|
|
17341
|
+
});
|
|
17342
|
+
};
|
|
17343
|
+
const resolveRenderDerivedMutableSourceKeys = (capturedReference, symbol, scopes) => {
|
|
17344
|
+
if (symbol.kind !== "let" || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return null;
|
|
17345
|
+
const boundaryFunction = findEnclosingFunction$1(symbol.bindingIdentifier);
|
|
17346
|
+
if (!boundaryFunction) return null;
|
|
17347
|
+
const capturingFunction = findEnclosingFunction$1(capturedReference.identifier);
|
|
17348
|
+
if (!capturingFunction || capturingFunction === boundaryFunction) return null;
|
|
17349
|
+
const sourceKeys = /* @__PURE__ */ new Set();
|
|
17350
|
+
if (symbol.initializer) {
|
|
17351
|
+
const initializerSourceKeys = resolveDerivedExpressionSourceKeys(symbol.initializer, scopes, new Set([symbol.id]));
|
|
17352
|
+
if (!initializerSourceKeys) return null;
|
|
17353
|
+
for (const initializerSourceKey of initializerSourceKeys) sourceKeys.add(initializerSourceKey);
|
|
17354
|
+
}
|
|
17355
|
+
let writeCount = 0;
|
|
17356
|
+
for (const symbolReference of symbol.references) {
|
|
17357
|
+
if (symbolReference.flag === "read") {
|
|
17358
|
+
if (findEnclosingFunction$1(symbolReference.identifier) !== capturingFunction && !isReadOnlyInitialStateUse(symbolReference.identifier, scopes)) return null;
|
|
17359
|
+
continue;
|
|
17360
|
+
}
|
|
17361
|
+
if (symbolReference.flag !== "write") return null;
|
|
17362
|
+
const referenceRoot = findTransparentExpressionRoot(symbolReference.identifier);
|
|
17363
|
+
const assignment = referenceRoot.parent;
|
|
17364
|
+
if (!isNodeOfType(assignment, "AssignmentExpression") || assignment.operator !== "=" || assignment.left !== referenceRoot || findEnclosingFunction$1(referenceRoot) !== boundaryFunction) return null;
|
|
17365
|
+
const assignmentSourceKeys = resolveDerivedExpressionSourceKeys(assignment.right, scopes, new Set([symbol.id]));
|
|
17366
|
+
const controlSourceKeys = resolveWriteControlSourceKeys(assignment, boundaryFunction, scopes);
|
|
17367
|
+
if (!assignmentSourceKeys || !controlSourceKeys) return null;
|
|
17368
|
+
for (const assignmentSourceKey of assignmentSourceKeys) sourceKeys.add(assignmentSourceKey);
|
|
17369
|
+
for (const controlSourceKey of controlSourceKeys) sourceKeys.add(controlSourceKey);
|
|
17370
|
+
writeCount += 1;
|
|
17371
|
+
}
|
|
17372
|
+
return writeCount > 0 && sourceKeys.size > 0 ? sourceKeys : null;
|
|
17373
|
+
};
|
|
16963
17374
|
const isUseCallbackResultDep = (node, scopes) => {
|
|
16964
17375
|
const rootSymbol = getRootSymbol(node, scopes);
|
|
16965
17376
|
const initializer = rootSymbol?.initializer ? unwrapExpression$3(rootSymbol.initializer) : null;
|
|
@@ -27251,7 +27662,11 @@ const mouseEventsHaveKeyEvents = defineRule({
|
|
|
27251
27662
|
//#region src/plugin/utils/has-directive.ts
|
|
27252
27663
|
const hasDirective = (programNode, directive) => {
|
|
27253
27664
|
if (!isNodeOfType(programNode, "Program")) return false;
|
|
27254
|
-
|
|
27665
|
+
for (const statement of programNode.body) {
|
|
27666
|
+
if (!isNodeOfType(statement, "ExpressionStatement") || statement.directive === void 0) return false;
|
|
27667
|
+
if (statement.directive === directive) return true;
|
|
27668
|
+
}
|
|
27669
|
+
return false;
|
|
27255
27670
|
};
|
|
27256
27671
|
//#endregion
|
|
27257
27672
|
//#region src/plugin/rules/nextjs/nextjs-async-client-component.ts
|
|
@@ -29183,7 +29598,7 @@ const nextjsNoVercelOgImport = defineRule({
|
|
|
29183
29598
|
//#endregion
|
|
29184
29599
|
//#region src/plugin/rules/a11y/no-access-key.ts
|
|
29185
29600
|
const MESSAGE$39 = "Screen reader users can lose their shortcuts because `accessKey` clashes with them, so remove it.";
|
|
29186
|
-
const isUndefinedIdentifier = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
|
|
29601
|
+
const isUndefinedIdentifier$1 = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
|
|
29187
29602
|
const noAccessKey = defineRule({
|
|
29188
29603
|
id: "no-access-key",
|
|
29189
29604
|
title: "accessKey attribute used",
|
|
@@ -29208,7 +29623,7 @@ const noAccessKey = defineRule({
|
|
|
29208
29623
|
if (isNodeOfType(attributeValue, "JSXExpressionContainer")) {
|
|
29209
29624
|
const expression = attributeValue.expression;
|
|
29210
29625
|
if (!expression || expression.type === "JSXEmptyExpression") return;
|
|
29211
|
-
if (isUndefinedIdentifier(expression)) return;
|
|
29626
|
+
if (isUndefinedIdentifier$1(expression)) return;
|
|
29212
29627
|
context.report({
|
|
29213
29628
|
node: accessKey,
|
|
29214
29629
|
message: MESSAGE$39
|
|
@@ -29973,6 +30388,12 @@ const isReactNamespaceImportReference = (ref) => Boolean(ref?.resolved?.defs.som
|
|
|
29973
30388
|
const importDeclaration = declarationNode.parent;
|
|
29974
30389
|
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
|
|
29975
30390
|
}));
|
|
30391
|
+
const isReactNamespaceReceiver = (analysis, node) => {
|
|
30392
|
+
const receiver = stripParenExpression(node);
|
|
30393
|
+
if (!isNodeOfType(receiver, "Identifier")) return false;
|
|
30394
|
+
const namespaceReference = getRef(analysis, receiver);
|
|
30395
|
+
return namespaceReference?.resolved ? isReactNamespaceImportReference(namespaceReference) : receiver.name === "React";
|
|
30396
|
+
};
|
|
29976
30397
|
const isGenuineReactHookDeclarator = (analysis, declarator, hookName) => {
|
|
29977
30398
|
if (!isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.init, "CallExpression")) return false;
|
|
29978
30399
|
const callee = stripParenExpression(declarator.init.callee);
|
|
@@ -29981,24 +30402,20 @@ const isGenuineReactHookDeclarator = (analysis, declarator, hookName) => {
|
|
|
29981
30402
|
if (!reference?.resolved) return callee.name === hookName;
|
|
29982
30403
|
return isReactNamedImportReference(reference, hookName);
|
|
29983
30404
|
}
|
|
29984
|
-
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.
|
|
29985
|
-
|
|
29986
|
-
if (!namespaceReference?.resolved) return callee.object.name === "React";
|
|
29987
|
-
return isReactNamespaceImportReference(namespaceReference);
|
|
30405
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier") || callee.property.name !== hookName) return false;
|
|
30406
|
+
return isReactNamespaceReceiver(analysis, callee.object);
|
|
29988
30407
|
};
|
|
29989
30408
|
const isHookCallee$1 = (analysis, node, hookName) => {
|
|
29990
30409
|
if (!node) return false;
|
|
29991
30410
|
if (isNodeOfType(node, "Identifier")) {
|
|
29992
30411
|
if (node.name === hookName) return true;
|
|
29993
30412
|
if (isReactNamedImportReference(getRef(analysis, node), hookName)) return true;
|
|
29994
|
-
const
|
|
29995
|
-
|
|
30413
|
+
const receiverRoot = findTransparentExpressionRoot(node);
|
|
30414
|
+
const parent = receiverRoot.parent;
|
|
30415
|
+
if (parent && isNodeOfType(parent, "MemberExpression") && parent.object === receiverRoot && isReactNamespaceReceiver(analysis, node) && isNodeOfType(parent.property, "Identifier") && parent.property.name === hookName) return true;
|
|
29996
30416
|
return false;
|
|
29997
30417
|
}
|
|
29998
|
-
if (isNodeOfType(node, "MemberExpression"))
|
|
29999
|
-
const receiver = stripParenExpression(node.object);
|
|
30000
|
-
return isNodeOfType(receiver, "Identifier") && receiver.name === "React" && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
|
|
30001
|
-
}
|
|
30418
|
+
if (isNodeOfType(node, "MemberExpression")) return isReactNamespaceReceiver(analysis, node.object) && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
|
|
30002
30419
|
return false;
|
|
30003
30420
|
};
|
|
30004
30421
|
const isUseEffect = (node) => {
|
|
@@ -30422,7 +30839,88 @@ const isIndependentWriterIdentifier = (componentFunction, identifier, includeDef
|
|
|
30422
30839
|
if (HANDLER_BINDING_NAME_PATTERN.test(bindingName)) return true;
|
|
30423
30840
|
return isSetterWiredToJsxHandler(componentFunction, bindingName);
|
|
30424
30841
|
};
|
|
30425
|
-
const
|
|
30842
|
+
const isSynchronousFunction = (functionNode) => {
|
|
30843
|
+
const functionMetadata = functionNode;
|
|
30844
|
+
return functionMetadata.async !== true && functionMetadata.generator !== true;
|
|
30845
|
+
};
|
|
30846
|
+
const findBindingVariable = (analysis, bindingIdentifier) => {
|
|
30847
|
+
for (const scope of analysis.scopeManager.scopes) for (const variable of scope.variables) if (variable.identifiers.includes(bindingIdentifier)) return variable;
|
|
30848
|
+
return null;
|
|
30849
|
+
};
|
|
30850
|
+
const getImmutableFunctionVariable = (analysis, componentFunction, functionNode) => {
|
|
30851
|
+
if (!isSynchronousFunction(functionNode) || !isAstDescendant(functionNode, componentFunction)) return null;
|
|
30852
|
+
const bindingIdentifier = getFunctionBindingIdentifier$1(functionNode);
|
|
30853
|
+
if (!bindingIdentifier) return null;
|
|
30854
|
+
const variable = findBindingVariable(analysis, bindingIdentifier);
|
|
30855
|
+
if (!variable || variable.defs.length !== 1 || variable.references.some((reference) => reference.isWrite() && !reference.init)) return null;
|
|
30856
|
+
const definition = variable.defs[0];
|
|
30857
|
+
if (definition.type === "FunctionName") return definition.node === functionNode ? variable : null;
|
|
30858
|
+
if (definition.type !== "Variable") return null;
|
|
30859
|
+
const declarator = definition.node;
|
|
30860
|
+
if (!isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.parent, "VariableDeclaration") || declarator.parent.kind !== "const") return null;
|
|
30861
|
+
if (declarator.init === functionNode) return variable;
|
|
30862
|
+
if (isNodeOfType(declarator.init, "CallExpression") && declarator.init.arguments?.[0] === functionNode && isGenuineReactHookDeclarator(analysis, declarator, "useCallback")) return variable;
|
|
30863
|
+
return null;
|
|
30864
|
+
};
|
|
30865
|
+
const getJsxEventValueAttribute = (identifier) => {
|
|
30866
|
+
const expression = findTransparentExpressionRoot(identifier);
|
|
30867
|
+
const expressionContainer = expression.parent;
|
|
30868
|
+
if (!isNodeOfType(expressionContainer, "JSXExpressionContainer") || expressionContainer.expression !== expression) return null;
|
|
30869
|
+
const attribute = expressionContainer.parent;
|
|
30870
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) return null;
|
|
30871
|
+
const attributeName = getJsxAttributeName(attribute.name);
|
|
30872
|
+
return attributeName && isEventHandlerName(attributeName) ? attribute : null;
|
|
30873
|
+
};
|
|
30874
|
+
const getInlineJsxEventCallbackAttribute = (callExpression) => {
|
|
30875
|
+
const callbackFunction = findEnclosingFunction$1(callExpression);
|
|
30876
|
+
if (!callbackFunction || !isSynchronousFunction(callbackFunction)) return null;
|
|
30877
|
+
return getJsxEventValueAttribute(callbackFunction);
|
|
30878
|
+
};
|
|
30879
|
+
const isReactHookDependencyReference = (identifier) => {
|
|
30880
|
+
const expression = findTransparentExpressionRoot(identifier);
|
|
30881
|
+
const dependencyArray = expression.parent;
|
|
30882
|
+
if (!isNodeOfType(dependencyArray, "ArrayExpression") || !(dependencyArray.elements ?? []).includes(expression)) return false;
|
|
30883
|
+
const hookCall = dependencyArray.parent;
|
|
30884
|
+
if (!isNodeOfType(hookCall, "CallExpression") || hookCall.arguments?.[1] !== dependencyArray) return false;
|
|
30885
|
+
const callee = hookCall.callee;
|
|
30886
|
+
if (isNodeOfType(callee, "Identifier")) return /^use[A-Z0-9]/.test(callee.name);
|
|
30887
|
+
return Boolean(isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && /^use[A-Z0-9]/.test(callee.property.name));
|
|
30888
|
+
};
|
|
30889
|
+
const hasReachableJsxEventCallPath = (analysis, context, componentFunction, functionVariable, visitedVariables) => {
|
|
30890
|
+
if (visitedVariables.has(functionVariable)) return false;
|
|
30891
|
+
const nextVisitedVariables = new Set(visitedVariables).add(functionVariable);
|
|
30892
|
+
const callExpressions = [];
|
|
30893
|
+
let hasDirectJsxEventReference = false;
|
|
30894
|
+
for (const reference of functionVariable.references) {
|
|
30895
|
+
if (reference.init) continue;
|
|
30896
|
+
const identifier = reference.identifier;
|
|
30897
|
+
if (reference.isWrite()) return false;
|
|
30898
|
+
const jsxEventValueAttribute = getJsxEventValueAttribute(identifier);
|
|
30899
|
+
if (jsxEventValueAttribute) {
|
|
30900
|
+
if (isNodeReachableWithinFunction(jsxEventValueAttribute, context)) hasDirectJsxEventReference = true;
|
|
30901
|
+
continue;
|
|
30902
|
+
}
|
|
30903
|
+
if (isReactHookDependencyReference(identifier)) continue;
|
|
30904
|
+
const callExpression = getCallExpr(reference);
|
|
30905
|
+
if (!callExpression) return false;
|
|
30906
|
+
const jsxEventCallbackAttribute = getInlineJsxEventCallbackAttribute(callExpression);
|
|
30907
|
+
if (jsxEventCallbackAttribute) {
|
|
30908
|
+
if (isNodeReachableWithinFunction(callExpression, context) && isNodeReachableWithinFunction(jsxEventCallbackAttribute, context)) hasDirectJsxEventReference = true;
|
|
30909
|
+
continue;
|
|
30910
|
+
}
|
|
30911
|
+
callExpressions.push(callExpression);
|
|
30912
|
+
}
|
|
30913
|
+
if (hasDirectJsxEventReference) return true;
|
|
30914
|
+
for (const callExpression of callExpressions) {
|
|
30915
|
+
if (!isNodeReachableWithinFunction(callExpression, context)) continue;
|
|
30916
|
+
const callerFunction = findEnclosingFunction$1(callExpression);
|
|
30917
|
+
if (!callerFunction || callerFunction === componentFunction) continue;
|
|
30918
|
+
const callerVariable = getImmutableFunctionVariable(analysis, componentFunction, callerFunction);
|
|
30919
|
+
if (callerVariable && hasReachableJsxEventCallPath(analysis, context, componentFunction, callerVariable, nextVisitedVariables)) return true;
|
|
30920
|
+
}
|
|
30921
|
+
return false;
|
|
30922
|
+
};
|
|
30923
|
+
const hasUserInputSetterWriter = (analysis, context, setterRef, effectNode, includeDeferredWriters = false) => {
|
|
30426
30924
|
if (!setterRef.resolved) return false;
|
|
30427
30925
|
const componentFunction = findEnclosingFunction$1(effectNode);
|
|
30428
30926
|
if (!componentFunction) return false;
|
|
@@ -30431,6 +30929,11 @@ const hasUserInputSetterWriter = (setterRef, effectNode, includeDeferredWriters
|
|
|
30431
30929
|
const identifier = reference.identifier;
|
|
30432
30930
|
if (isAstDescendant(identifier, effectNode)) continue;
|
|
30433
30931
|
if (isIndependentWriterIdentifier(componentFunction, identifier, includeDeferredWriters)) return true;
|
|
30932
|
+
if (!isNodeReachableWithinFunction(identifier, context)) continue;
|
|
30933
|
+
const writerFunction = findEnclosingFunction$1(identifier);
|
|
30934
|
+
if (!writerFunction || writerFunction === componentFunction) continue;
|
|
30935
|
+
const writerVariable = getImmutableFunctionVariable(analysis, componentFunction, writerFunction);
|
|
30936
|
+
if (writerVariable && hasReachableJsxEventCallPath(analysis, context, componentFunction, writerVariable, /* @__PURE__ */ new Set())) return true;
|
|
30434
30937
|
}
|
|
30435
30938
|
return false;
|
|
30436
30939
|
};
|
|
@@ -31320,7 +31823,7 @@ const areInMutuallyExclusiveBranches = (leftNode, rightNode) => {
|
|
|
31320
31823
|
}
|
|
31321
31824
|
return false;
|
|
31322
31825
|
};
|
|
31323
|
-
const collectEffectStateWriteFacts = (analysis, effectNode, currentFilename) => {
|
|
31826
|
+
const collectEffectStateWriteFacts = (analysis, context, effectNode, currentFilename) => {
|
|
31324
31827
|
const frames = collectBoundedEffectExecutionFrames(analysis, effectNode, currentFilename);
|
|
31325
31828
|
if (frames.length === 0) return [];
|
|
31326
31829
|
const effectHasCleanup = hasCleanup(analysis, effectNode);
|
|
@@ -31350,7 +31853,7 @@ const collectEffectStateWriteFacts = (analysis, effectNode, currentFilename) =>
|
|
|
31350
31853
|
for (const returnedExpression of returnedExpressions) mergeEvidence(valueEvidence, collectValueEvidence(analysis, returnedExpression, updaterFrame, remainingValueCallFrames));
|
|
31351
31854
|
} else valueEvidence = collectValueEvidence(analysis, writtenValue, frame, remainingValueCallFrames);
|
|
31352
31855
|
const sourceReferences = [...valueEvidence.sourceReferences].filter((sourceReference) => getUseStateDecl(analysis, sourceReference) !== stateDeclarator);
|
|
31353
|
-
const hasIndependentWriter = hasUserInputSetterWriter(setterReference, effectNode, true);
|
|
31856
|
+
const hasIndependentWriter = hasUserInputSetterWriter(analysis, context, setterReference, effectNode, true);
|
|
31354
31857
|
const doesMatchStateInitializer = matchesStateInitializer(analysis, callExpression, stateDeclarator);
|
|
31355
31858
|
if (effectHasCleanup && (frame.isDeferred || valueEvidence.hasUnknownSource || valueEvidence.hasDeferredIntroducedValue || valueEvidence.readsExternalValue)) cleanupManagedStateDeclarators.add(stateDeclarator);
|
|
31356
31859
|
const isRenderKnownCopy = sourceReferences.length > 0 && !frame.isDeferred && !valueEvidence.hasUnknownSource && !valueEvidence.hasDeferredIntroducedValue && !valueEvidence.readsExternalValue && !hasIndependentWriter;
|
|
@@ -31391,7 +31894,7 @@ const noAdjustStateOnPropChange = defineRule({
|
|
|
31391
31894
|
const dependencyReferences = getEffectDepsRefs(analysis, node);
|
|
31392
31895
|
if (!dependencyReferences) return;
|
|
31393
31896
|
if (!dependencyReferences.flatMap((reference) => isState(analysis, reference) ? [] : getUpstreamRefs(analysis, reference)).some((reference) => isProp(analysis, reference))) return;
|
|
31394
|
-
for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
|
|
31897
|
+
for (const fact of collectEffectStateWriteFacts(analysis, context, node, context.filename)) {
|
|
31395
31898
|
if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
|
|
31396
31899
|
context.report({
|
|
31397
31900
|
node: fact.callExpression,
|
|
@@ -34078,6 +34581,7 @@ const noChainStateUpdates = defineRule({
|
|
|
34078
34581
|
id: "no-chain-state-updates",
|
|
34079
34582
|
title: "State updates chained through effects",
|
|
34080
34583
|
severity: "warn",
|
|
34584
|
+
disabledWhen: ["react:18"],
|
|
34081
34585
|
tags: ["test-noise"],
|
|
34082
34586
|
recommendation: "Set all the related state together in the event handler that starts it, instead of having one useEffect react to a state change and set more state. See https://react.dev/learn/you-might-not-need-an-effect#chains-of-computations",
|
|
34083
34587
|
create: (context) => ({ CallExpression(node) {
|
|
@@ -35964,7 +36468,7 @@ const noDerivedState = defineRule({
|
|
|
35964
36468
|
if (!isUseEffect(node)) return;
|
|
35965
36469
|
const analysis = getProgramAnalysis(node);
|
|
35966
36470
|
if (!analysis) return;
|
|
35967
|
-
for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
|
|
36471
|
+
for (const fact of collectEffectStateWriteFacts(analysis, context, node, context.filename)) {
|
|
35968
36472
|
if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
|
|
35969
36473
|
reportStateWrite(fact.callExpression, fact.stateDeclarator);
|
|
35970
36474
|
}
|
|
@@ -35984,7 +36488,7 @@ const noDerivedStateEffect = defineRule({
|
|
|
35984
36488
|
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
35985
36489
|
const analysis = getProgramAnalysis(node);
|
|
35986
36490
|
if (!analysis) return;
|
|
35987
|
-
if (!collectEffectStateWriteFacts(analysis, node, context.filename).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
|
|
36491
|
+
if (!collectEffectStateWriteFacts(analysis, context, node, context.filename).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
|
|
35988
36492
|
context.report({
|
|
35989
36493
|
node,
|
|
35990
36494
|
message: "You pay an extra render for state you can derive from other values."
|
|
@@ -36460,9 +36964,20 @@ const noDidMountSetState = defineRule({
|
|
|
36460
36964
|
}
|
|
36461
36965
|
});
|
|
36462
36966
|
//#endregion
|
|
36967
|
+
//#region src/plugin/utils/find-enclosing-class.ts
|
|
36968
|
+
const findEnclosingClass = (node) => {
|
|
36969
|
+
let ancestor = node.parent;
|
|
36970
|
+
while (ancestor) {
|
|
36971
|
+
if (isNodeOfType(ancestor, "ClassDeclaration") || isNodeOfType(ancestor, "ClassExpression")) return ancestor;
|
|
36972
|
+
ancestor = ancestor.parent ?? null;
|
|
36973
|
+
}
|
|
36974
|
+
return null;
|
|
36975
|
+
};
|
|
36976
|
+
//#endregion
|
|
36463
36977
|
//#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
|
|
36464
36978
|
const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
|
|
36465
36979
|
const MESSAGE$27 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
|
|
36980
|
+
const DIFFERENCE_OPERATORS = new Set(["!=", "!=="]);
|
|
36466
36981
|
const EQUALITY_OPERATORS = new Set([
|
|
36467
36982
|
"==",
|
|
36468
36983
|
"===",
|
|
@@ -36474,6 +36989,8 @@ const FUNCTION_NODE_TYPES = new Set([
|
|
|
36474
36989
|
"FunctionExpression",
|
|
36475
36990
|
"ArrowFunctionExpression"
|
|
36476
36991
|
]);
|
|
36992
|
+
const CLASS_NODE_TYPES = new Set(["ClassDeclaration", "ClassExpression"]);
|
|
36993
|
+
const callbackRefFieldNamesByClass = /* @__PURE__ */ new WeakMap();
|
|
36477
36994
|
const isLifecycleMethodFunction = (node) => {
|
|
36478
36995
|
if (!FUNCTION_NODE_TYPES.has(node.type)) return false;
|
|
36479
36996
|
const parent = node.parent;
|
|
@@ -36529,6 +37046,187 @@ const getStaticMemberName = (node) => {
|
|
|
36529
37046
|
if (!isNodeOfType(node, "MemberExpression") || node.computed === true) return null;
|
|
36530
37047
|
return isNodeOfType(node.property, "Identifier") ? node.property.name : null;
|
|
36531
37048
|
};
|
|
37049
|
+
const getMemberIdentity = (property) => {
|
|
37050
|
+
const propertyName = getPropertyKeyName$2(property);
|
|
37051
|
+
if (propertyName !== void 0) return isNodeOfType(property, "PrivateIdentifier") ? `#${propertyName}` : propertyName;
|
|
37052
|
+
return isNodeOfType(property, "Literal") && typeof property.value === "string" ? property.value : null;
|
|
37053
|
+
};
|
|
37054
|
+
const collectPreviousSourcePaths = (pattern, domain, members, previousSourcePaths) => {
|
|
37055
|
+
if (!pattern) return;
|
|
37056
|
+
const unwrappedPattern = stripParenExpression(pattern);
|
|
37057
|
+
if (isNodeOfType(unwrappedPattern, "Identifier")) {
|
|
37058
|
+
previousSourcePaths.set(unwrappedPattern.name, {
|
|
37059
|
+
domain,
|
|
37060
|
+
members: [...members],
|
|
37061
|
+
source: "previous"
|
|
37062
|
+
});
|
|
37063
|
+
return;
|
|
37064
|
+
}
|
|
37065
|
+
if (isNodeOfType(unwrappedPattern, "AssignmentPattern")) {
|
|
37066
|
+
collectPreviousSourcePaths(unwrappedPattern.left, domain, members, previousSourcePaths);
|
|
37067
|
+
return;
|
|
37068
|
+
}
|
|
37069
|
+
if (!isNodeOfType(unwrappedPattern, "ObjectPattern")) return;
|
|
37070
|
+
for (const property of unwrappedPattern.properties) {
|
|
37071
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
37072
|
+
const propertyName = getStaticPropertyKeyName(property, { allowComputedString: true });
|
|
37073
|
+
if (!propertyName) continue;
|
|
37074
|
+
collectPreviousSourcePaths(property.value, domain, [...members, propertyName], previousSourcePaths);
|
|
37075
|
+
}
|
|
37076
|
+
};
|
|
37077
|
+
const getStateSourcePath = (node, previousSourcePaths) => {
|
|
37078
|
+
let currentNode = stripParenExpression(node);
|
|
37079
|
+
const members = [];
|
|
37080
|
+
while (isNodeOfType(currentNode, "MemberExpression")) {
|
|
37081
|
+
const memberName = getStaticMemberName(currentNode);
|
|
37082
|
+
if (!memberName) return null;
|
|
37083
|
+
members.unshift(memberName);
|
|
37084
|
+
currentNode = stripParenExpression(currentNode.object);
|
|
37085
|
+
}
|
|
37086
|
+
if (isNodeOfType(currentNode, "ThisExpression")) {
|
|
37087
|
+
const [domain, ...pathMembers] = members;
|
|
37088
|
+
if (domain !== "props" && domain !== "state") return null;
|
|
37089
|
+
return {
|
|
37090
|
+
domain,
|
|
37091
|
+
members: pathMembers,
|
|
37092
|
+
source: "current"
|
|
37093
|
+
};
|
|
37094
|
+
}
|
|
37095
|
+
if (!isNodeOfType(currentNode, "Identifier")) return null;
|
|
37096
|
+
const previousSourcePath = previousSourcePaths.get(currentNode.name);
|
|
37097
|
+
return previousSourcePath ? {
|
|
37098
|
+
...previousSourcePath,
|
|
37099
|
+
members: [...previousSourcePath.members, ...members]
|
|
37100
|
+
} : null;
|
|
37101
|
+
};
|
|
37102
|
+
const haveMatchingStateSourcePaths = (left, right) => left.domain === right.domain && left.members.length === right.members.length && left.members.every((member, index) => member === right.members[index]);
|
|
37103
|
+
const collectConjunctiveStateSourceComparisons = (test, previousSourcePaths, comparisons) => {
|
|
37104
|
+
const expression = stripParenExpression(test);
|
|
37105
|
+
if (isNodeOfType(expression, "LogicalExpression") && expression.operator === "&&") {
|
|
37106
|
+
collectConjunctiveStateSourceComparisons(expression.left, previousSourcePaths, comparisons);
|
|
37107
|
+
collectConjunctiveStateSourceComparisons(expression.right, previousSourcePaths, comparisons);
|
|
37108
|
+
return;
|
|
37109
|
+
}
|
|
37110
|
+
if (!isNodeOfType(expression, "BinaryExpression") || !EQUALITY_OPERATORS.has(expression.operator)) return;
|
|
37111
|
+
const leftPath = getStateSourcePath(expression.left, previousSourcePaths);
|
|
37112
|
+
const rightPath = getStateSourcePath(expression.right, previousSourcePaths);
|
|
37113
|
+
if (Boolean(leftPath) === Boolean(rightPath)) return;
|
|
37114
|
+
const path = leftPath ?? rightPath;
|
|
37115
|
+
if (!path) return;
|
|
37116
|
+
comparisons.push({
|
|
37117
|
+
comparedValue: leftPath ? expression.right : expression.left,
|
|
37118
|
+
isDifference: DIFFERENCE_OPERATORS.has(expression.operator),
|
|
37119
|
+
path
|
|
37120
|
+
});
|
|
37121
|
+
};
|
|
37122
|
+
const isHistoricalToCurrentTransitionGuard = (test, previousSourcePaths) => {
|
|
37123
|
+
const expression = stripParenExpression(test);
|
|
37124
|
+
if (isNodeOfType(expression, "LogicalExpression") && expression.operator === "||") return isHistoricalToCurrentTransitionGuard(expression.left, previousSourcePaths) && isHistoricalToCurrentTransitionGuard(expression.right, previousSourcePaths);
|
|
37125
|
+
const comparisons = [];
|
|
37126
|
+
collectConjunctiveStateSourceComparisons(expression, previousSourcePaths, comparisons);
|
|
37127
|
+
return comparisons.some((comparison, index) => comparisons.slice(index + 1).some((candidate) => comparison.path.source !== candidate.path.source && comparison.isDifference !== candidate.isDifference && haveMatchingStateSourcePaths(comparison.path, candidate.path) && areExpressionsStructurallyEqual(comparison.comparedValue, candidate.comparedValue)));
|
|
37128
|
+
};
|
|
37129
|
+
const getThisFieldName = (node) => {
|
|
37130
|
+
const unwrappedNode = stripParenExpression(node);
|
|
37131
|
+
if (!isNodeOfType(unwrappedNode, "MemberExpression") || unwrappedNode.computed === true || !isNodeOfType(stripParenExpression(unwrappedNode.object), "ThisExpression")) return null;
|
|
37132
|
+
return getMemberIdentity(unwrappedNode.property);
|
|
37133
|
+
};
|
|
37134
|
+
const isUndefinedIdentifier = (node) => {
|
|
37135
|
+
const unwrappedNode = stripParenExpression(node);
|
|
37136
|
+
return isNodeOfType(unwrappedNode, "Identifier") && unwrappedNode.name === "undefined";
|
|
37137
|
+
};
|
|
37138
|
+
const isDirectRefParameterValue = (node, parameterSymbolId, scopes) => {
|
|
37139
|
+
const unwrappedNode = stripParenExpression(node);
|
|
37140
|
+
if (isNodeOfType(unwrappedNode, "Identifier")) return scopes.symbolFor(unwrappedNode)?.id === parameterSymbolId;
|
|
37141
|
+
if (!isNodeOfType(unwrappedNode, "LogicalExpression") || unwrappedNode.operator !== "??") return false;
|
|
37142
|
+
const left = stripParenExpression(unwrappedNode.left);
|
|
37143
|
+
return isNodeOfType(left, "Identifier") && scopes.symbolFor(left)?.id === parameterSymbolId && isUndefinedIdentifier(unwrappedNode.right);
|
|
37144
|
+
};
|
|
37145
|
+
const getCallbackRefAssignedFields = (callback, scopes) => {
|
|
37146
|
+
const firstParameter = (callback.params ?? [])[0];
|
|
37147
|
+
if (!firstParameter) return /* @__PURE__ */ new Set();
|
|
37148
|
+
const parameterIdentifier = isNodeOfType(firstParameter, "AssignmentPattern") ? firstParameter.left : firstParameter;
|
|
37149
|
+
if (!isNodeOfType(parameterIdentifier, "Identifier")) return /* @__PURE__ */ new Set();
|
|
37150
|
+
const parameterSymbolId = scopes.symbolFor(parameterIdentifier)?.id;
|
|
37151
|
+
if (parameterSymbolId === void 0) return /* @__PURE__ */ new Set();
|
|
37152
|
+
const body = callback.body;
|
|
37153
|
+
if (!body) return /* @__PURE__ */ new Set();
|
|
37154
|
+
const assignedFieldNames = /* @__PURE__ */ new Set();
|
|
37155
|
+
walkAst(body, (node) => {
|
|
37156
|
+
if (node !== body && (FUNCTION_NODE_TYPES.has(node.type) && !isImmediatelyInvokedFunction(node) || CLASS_NODE_TYPES.has(node.type))) return false;
|
|
37157
|
+
const assignmentTarget = isNodeOfType(node, "AssignmentExpression") && node.left || isNodeOfType(node, "UpdateExpression") && node.argument || isNodeOfType(node, "UnaryExpression") && node.operator === "delete" && node.argument || null;
|
|
37158
|
+
if (!assignmentTarget) return;
|
|
37159
|
+
const fieldName = getThisFieldName(assignmentTarget);
|
|
37160
|
+
if (!fieldName) return;
|
|
37161
|
+
if (isNodeOfType(node, "AssignmentExpression") && node.operator === "=" && isDirectRefParameterValue(node.right, parameterSymbolId, scopes)) {
|
|
37162
|
+
assignedFieldNames.add(fieldName);
|
|
37163
|
+
return;
|
|
37164
|
+
}
|
|
37165
|
+
assignedFieldNames.delete(fieldName);
|
|
37166
|
+
});
|
|
37167
|
+
return assignedFieldNames;
|
|
37168
|
+
};
|
|
37169
|
+
const getClassMemberCallback = (classNode, memberName) => {
|
|
37170
|
+
const classBody = classNode.body?.body ?? [];
|
|
37171
|
+
for (const member of classBody) {
|
|
37172
|
+
if (!isNodeOfType(member, "MethodDefinition") && !isNodeOfType(member, "PropertyDefinition")) continue;
|
|
37173
|
+
if (member.static === true) continue;
|
|
37174
|
+
const key = member.key;
|
|
37175
|
+
if (getMemberIdentity(key) !== memberName) continue;
|
|
37176
|
+
const value = member.value;
|
|
37177
|
+
return value && FUNCTION_NODE_TYPES.has(value.type) ? value : null;
|
|
37178
|
+
}
|
|
37179
|
+
return null;
|
|
37180
|
+
};
|
|
37181
|
+
const collectCallbackRefFieldsFromExpression = (expression, classNode, fieldNames, scopes) => {
|
|
37182
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
37183
|
+
if (FUNCTION_NODE_TYPES.has(unwrappedExpression.type)) {
|
|
37184
|
+
for (const fieldName of getCallbackRefAssignedFields(unwrappedExpression, scopes)) fieldNames.add(fieldName);
|
|
37185
|
+
return;
|
|
37186
|
+
}
|
|
37187
|
+
const handlerName = getThisFieldName(unwrappedExpression);
|
|
37188
|
+
if (handlerName) {
|
|
37189
|
+
const callback = getClassMemberCallback(classNode, handlerName);
|
|
37190
|
+
if (callback) for (const fieldName of getCallbackRefAssignedFields(callback, scopes)) fieldNames.add(fieldName);
|
|
37191
|
+
return;
|
|
37192
|
+
}
|
|
37193
|
+
if (isNodeOfType(unwrappedExpression, "ConditionalExpression")) {
|
|
37194
|
+
collectCallbackRefFieldsFromExpression(unwrappedExpression.consequent, classNode, fieldNames, scopes);
|
|
37195
|
+
collectCallbackRefFieldsFromExpression(unwrappedExpression.alternate, classNode, fieldNames, scopes);
|
|
37196
|
+
return;
|
|
37197
|
+
}
|
|
37198
|
+
if (isNodeOfType(unwrappedExpression, "LogicalExpression")) {
|
|
37199
|
+
if (unwrappedExpression.operator !== "&&") collectCallbackRefFieldsFromExpression(unwrappedExpression.left, classNode, fieldNames, scopes);
|
|
37200
|
+
collectCallbackRefFieldsFromExpression(unwrappedExpression.right, classNode, fieldNames, scopes);
|
|
37201
|
+
}
|
|
37202
|
+
};
|
|
37203
|
+
const getCallbackRefFieldNames = (classNode, scopes) => {
|
|
37204
|
+
if (!classNode) return /* @__PURE__ */ new Set();
|
|
37205
|
+
const cachedFieldNames = callbackRefFieldNamesByClass.get(classNode);
|
|
37206
|
+
if (cachedFieldNames) return cachedFieldNames;
|
|
37207
|
+
const fieldNames = /* @__PURE__ */ new Set();
|
|
37208
|
+
const classBody = classNode.body;
|
|
37209
|
+
if (classBody) walkAst(classBody, (node) => {
|
|
37210
|
+
if (node !== classBody && CLASS_NODE_TYPES.has(node.type)) return false;
|
|
37211
|
+
if (!isNodeOfType(node, "JSXAttribute") || !isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "ref" || !node.value || !isNodeOfType(node.value, "JSXExpressionContainer") || !node.value.expression) return;
|
|
37212
|
+
collectCallbackRefFieldsFromExpression(node.value.expression, classNode, fieldNames, scopes);
|
|
37213
|
+
});
|
|
37214
|
+
callbackRefFieldNamesByClass.set(classNode, fieldNames);
|
|
37215
|
+
return fieldNames;
|
|
37216
|
+
};
|
|
37217
|
+
const collectLifecycleWrittenFieldNames = (lifecycleFunction) => {
|
|
37218
|
+
const fieldNames = /* @__PURE__ */ new Set();
|
|
37219
|
+
const body = lifecycleFunction.body;
|
|
37220
|
+
if (!body) return fieldNames;
|
|
37221
|
+
walkAst(body, (node) => {
|
|
37222
|
+
if (FUNCTION_NODE_TYPES.has(node.type) && !isImmediatelyInvokedFunction(node)) return false;
|
|
37223
|
+
const target = isNodeOfType(node, "AssignmentExpression") && node.left || isNodeOfType(node, "UpdateExpression") && node.argument || null;
|
|
37224
|
+
if (!target) return;
|
|
37225
|
+
const fieldName = getThisFieldName(target);
|
|
37226
|
+
if (fieldName) fieldNames.add(fieldName);
|
|
37227
|
+
});
|
|
37228
|
+
return fieldNames;
|
|
37229
|
+
};
|
|
36532
37230
|
const getThisStateFieldName = (node) => {
|
|
36533
37231
|
const unwrappedNode = stripParenExpression(node);
|
|
36534
37232
|
if (!isNodeOfType(unwrappedNode, "MemberExpression")) return null;
|
|
@@ -36546,15 +37244,17 @@ const collectLocalInitializers = (lifecycleFunction) => {
|
|
|
36546
37244
|
});
|
|
36547
37245
|
return initializers;
|
|
36548
37246
|
};
|
|
36549
|
-
const derivesFromPostMountValue = (node, localInitializers, visitedNames = /* @__PURE__ */ new Set()) => {
|
|
37247
|
+
const derivesFromPostMountValue = (node, localInitializers, callbackRefFieldNames, visitedNames = /* @__PURE__ */ new Set()) => {
|
|
36550
37248
|
if (readsPostMountValue(node)) return true;
|
|
37249
|
+
const fieldName = getThisFieldName(node);
|
|
37250
|
+
if (fieldName && callbackRefFieldNames.has(fieldName)) return true;
|
|
36551
37251
|
const referencedNames = /* @__PURE__ */ new Set();
|
|
36552
37252
|
collectReferenceIdentifierNames(node, referencedNames);
|
|
36553
37253
|
for (const referencedName of referencedNames) {
|
|
36554
37254
|
if (visitedNames.has(referencedName)) continue;
|
|
36555
37255
|
const initializer = localInitializers.get(referencedName);
|
|
36556
37256
|
if (!initializer) continue;
|
|
36557
|
-
if (derivesFromPostMountValue(initializer, localInitializers, new Set([...visitedNames, referencedName]))) return true;
|
|
37257
|
+
if (derivesFromPostMountValue(initializer, localInitializers, callbackRefFieldNames, new Set([...visitedNames, referencedName]))) return true;
|
|
36558
37258
|
}
|
|
36559
37259
|
return false;
|
|
36560
37260
|
};
|
|
@@ -36568,50 +37268,84 @@ const getSetStateFieldValue = (setStateCall, fieldName) => {
|
|
|
36568
37268
|
}
|
|
36569
37269
|
return null;
|
|
36570
37270
|
};
|
|
36571
|
-
const isConvergentPostMountGuard = (test, setStateCall, localInitializers) => {
|
|
36572
|
-
|
|
36573
|
-
|
|
36574
|
-
if (
|
|
36575
|
-
|
|
36576
|
-
const
|
|
36577
|
-
|
|
36578
|
-
|
|
36579
|
-
|
|
36580
|
-
|
|
36581
|
-
|
|
36582
|
-
|
|
36583
|
-
|
|
36584
|
-
|
|
36585
|
-
|
|
36586
|
-
|
|
36587
|
-
return
|
|
36588
|
-
};
|
|
36589
|
-
const
|
|
36590
|
-
|
|
36591
|
-
|
|
36592
|
-
|
|
36593
|
-
if (qualifies) return false;
|
|
36594
|
-
if (!isNodeOfType(node, "BinaryExpression")) return;
|
|
36595
|
-
if (!EQUALITY_OPERATORS.has(node.operator)) return;
|
|
36596
|
-
if (isStatefulOperand(node.left, paramNames, derivedNames) && isStatefulOperand(node.right, paramNames, derivedNames) && (referencesAnyName(node.left, derivedNames) || referencesAnyName(node.right, derivedNames))) {
|
|
36597
|
-
qualifies = true;
|
|
36598
|
-
return false;
|
|
36599
|
-
}
|
|
36600
|
-
});
|
|
36601
|
-
return qualifies;
|
|
37271
|
+
const isConvergentPostMountGuard = (test, setStateCall, localInitializers, callbackRefFieldNames, isTruthfulBranch) => {
|
|
37272
|
+
const expression = stripParenExpression(test);
|
|
37273
|
+
if (isNodeOfType(expression, "LogicalExpression")) {
|
|
37274
|
+
if (expression.operator !== "&&" && expression.operator !== "||") return false;
|
|
37275
|
+
const leftIsConvergent = isConvergentPostMountGuard(expression.left, setStateCall, localInitializers, callbackRefFieldNames, isTruthfulBranch);
|
|
37276
|
+
const rightIsConvergent = isConvergentPostMountGuard(expression.right, setStateCall, localInitializers, callbackRefFieldNames, isTruthfulBranch);
|
|
37277
|
+
return isTruthfulBranch && expression.operator === "||" || !isTruthfulBranch && expression.operator === "&&" ? leftIsConvergent && rightIsConvergent : leftIsConvergent || rightIsConvergent;
|
|
37278
|
+
}
|
|
37279
|
+
if (!isNodeOfType(expression, "BinaryExpression") || !(isTruthfulBranch ? DIFFERENCE_OPERATORS.has(expression.operator) : EQUALITY_OPERATORS.has(expression.operator) && !DIFFERENCE_OPERATORS.has(expression.operator))) return false;
|
|
37280
|
+
const leftFieldName = getThisStateFieldName(expression.left);
|
|
37281
|
+
const rightFieldName = getThisStateFieldName(expression.right);
|
|
37282
|
+
const fieldName = leftFieldName ?? rightFieldName;
|
|
37283
|
+
const comparedValue = leftFieldName ? expression.right : expression.left;
|
|
37284
|
+
if (!fieldName) return false;
|
|
37285
|
+
const assignedValue = getSetStateFieldValue(setStateCall, fieldName);
|
|
37286
|
+
if (!assignedValue || !areExpressionsStructurallyEqual(comparedValue, assignedValue)) return false;
|
|
37287
|
+
return isUndefinedIdentifier(comparedValue) || derivesFromPostMountValue(comparedValue, localInitializers, callbackRefFieldNames);
|
|
37288
|
+
};
|
|
37289
|
+
const containsPositiveStateFieldTest = (test, fieldName) => {
|
|
37290
|
+
const unwrappedTest = stripParenExpression(test);
|
|
37291
|
+
if (getThisStateFieldName(unwrappedTest) === fieldName) return true;
|
|
37292
|
+
return isNodeOfType(unwrappedTest, "LogicalExpression") && unwrappedTest.operator === "&&" && (containsPositiveStateFieldTest(unwrappedTest.left, fieldName) || containsPositiveStateFieldTest(unwrappedTest.right, fieldName));
|
|
36602
37293
|
};
|
|
36603
|
-
const
|
|
37294
|
+
const isConvergentUndefinedClearGuard = (test, setStateCall) => {
|
|
37295
|
+
if (!isNodeOfType(setStateCall, "CallExpression")) return false;
|
|
37296
|
+
const argument = setStateCall.arguments?.[0];
|
|
37297
|
+
if (!argument || !isNodeOfType(argument, "ObjectExpression")) return false;
|
|
37298
|
+
for (const property of argument.properties ?? []) {
|
|
37299
|
+
if (!isNodeOfType(property, "Property") || property.computed === true || !isUndefinedIdentifier(property.value)) continue;
|
|
37300
|
+
const fieldName = isNodeOfType(property.key, "Identifier") && property.key.name || isNodeOfType(property.key, "Literal") && typeof property.key.value === "string" && property.key.value || null;
|
|
37301
|
+
if (fieldName && containsPositiveStateFieldTest(test, fieldName)) return true;
|
|
37302
|
+
}
|
|
37303
|
+
return false;
|
|
37304
|
+
};
|
|
37305
|
+
const isDiffGuardTest = (test, paramNames, derivedNames, isTruthfulBranch) => {
|
|
37306
|
+
const expression = stripParenExpression(test);
|
|
37307
|
+
if (isNodeOfType(expression, "LogicalExpression")) {
|
|
37308
|
+
if (expression.operator !== "&&" && expression.operator !== "||") return false;
|
|
37309
|
+
const leftIsDiffGuard = isDiffGuardTest(expression.left, paramNames, derivedNames, isTruthfulBranch);
|
|
37310
|
+
const rightIsDiffGuard = isDiffGuardTest(expression.right, paramNames, derivedNames, isTruthfulBranch);
|
|
37311
|
+
return isTruthfulBranch && expression.operator === "||" || !isTruthfulBranch && expression.operator === "&&" ? leftIsDiffGuard && rightIsDiffGuard : leftIsDiffGuard || rightIsDiffGuard;
|
|
37312
|
+
}
|
|
37313
|
+
if (!isNodeOfType(expression, "BinaryExpression") || !(isTruthfulBranch ? DIFFERENCE_OPERATORS.has(expression.operator) : EQUALITY_OPERATORS.has(expression.operator) && !DIFFERENCE_OPERATORS.has(expression.operator))) return false;
|
|
37314
|
+
return isStatefulOperand(expression.left, paramNames, derivedNames) && isStatefulOperand(expression.right, paramNames, derivedNames) && (referencesAnyName(expression.left, paramNames) || referencesAnyName(expression.right, paramNames) || referencesAnyName(expression.left, derivedNames) || referencesAnyName(expression.right, derivedNames));
|
|
37315
|
+
};
|
|
37316
|
+
const isInsideDiffGuard = (setStateCall, scopes) => {
|
|
36604
37317
|
const lifecycleFunction = findEnclosingLifecycleFunction(setStateCall);
|
|
36605
37318
|
if (!lifecycleFunction) return false;
|
|
36606
37319
|
const paramNames = /* @__PURE__ */ new Set();
|
|
36607
|
-
|
|
37320
|
+
const parameters = lifecycleFunction.params ?? [];
|
|
37321
|
+
for (const param of parameters) collectPatternNames(param, paramNames);
|
|
37322
|
+
const previousSourcePaths = /* @__PURE__ */ new Map();
|
|
37323
|
+
const [previousPropsParameter, previousStateParameter] = parameters;
|
|
37324
|
+
collectPreviousSourcePaths(previousPropsParameter, "props", [], previousSourcePaths);
|
|
37325
|
+
collectPreviousSourcePaths(previousStateParameter, "state", [], previousSourcePaths);
|
|
36608
37326
|
const derivedNames = collectDiffSourceLocalNames(lifecycleFunction, paramNames);
|
|
36609
37327
|
const localInitializers = collectLocalInitializers(lifecycleFunction);
|
|
37328
|
+
const lifecycleWrittenFieldNames = collectLifecycleWrittenFieldNames(lifecycleFunction);
|
|
37329
|
+
const callbackRefFieldNames = new Set([...getCallbackRefFieldNames(findEnclosingClass(lifecycleFunction), scopes)].filter((fieldName) => !lifecycleWrittenFieldNames.has(fieldName)));
|
|
36610
37330
|
let child = setStateCall;
|
|
36611
37331
|
let ancestor = setStateCall.parent;
|
|
36612
37332
|
while (ancestor && ancestor !== lifecycleFunction) {
|
|
36613
|
-
|
|
36614
|
-
|
|
37333
|
+
let guardTest = null;
|
|
37334
|
+
let isTruthfulBranch = true;
|
|
37335
|
+
if (isNodeOfType(ancestor, "IfStatement")) {
|
|
37336
|
+
if (child === ancestor.consequent) guardTest = ancestor.test;
|
|
37337
|
+
else if (child === ancestor.alternate) {
|
|
37338
|
+
guardTest = ancestor.test;
|
|
37339
|
+
isTruthfulBranch = false;
|
|
37340
|
+
}
|
|
37341
|
+
} else if (isNodeOfType(ancestor, "ConditionalExpression")) {
|
|
37342
|
+
if (child === ancestor.consequent) guardTest = ancestor.test;
|
|
37343
|
+
else if (child === ancestor.alternate) {
|
|
37344
|
+
guardTest = ancestor.test;
|
|
37345
|
+
isTruthfulBranch = false;
|
|
37346
|
+
}
|
|
37347
|
+
} else if (isNodeOfType(ancestor, "LogicalExpression") && ancestor.operator === "&&" && child === ancestor.right) guardTest = ancestor.left;
|
|
37348
|
+
if (guardTest && (isDiffGuardTest(guardTest, paramNames, derivedNames, isTruthfulBranch) || isTruthfulBranch && isHistoricalToCurrentTransitionGuard(guardTest, previousSourcePaths) || isConvergentPostMountGuard(guardTest, setStateCall, localInitializers, callbackRefFieldNames, isTruthfulBranch) || isTruthfulBranch && isConvergentUndefinedClearGuard(guardTest, setStateCall))) return true;
|
|
36615
37349
|
child = ancestor;
|
|
36616
37350
|
ancestor = ancestor.parent ?? null;
|
|
36617
37351
|
}
|
|
@@ -36633,7 +37367,7 @@ const noDidUpdateSetState = defineRule({
|
|
|
36633
37367
|
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
36634
37368
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
36635
37369
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
36636
|
-
if (isInsideDiffGuard(node)) return;
|
|
37370
|
+
if (isInsideDiffGuard(node, context.scopes)) return;
|
|
36637
37371
|
context.report({
|
|
36638
37372
|
node: node.callee,
|
|
36639
37373
|
message: MESSAGE$27
|
|
@@ -40355,41 +41089,223 @@ const readLogicalConditionResult = (operator, leftResult, rightResult) => {
|
|
|
40355
41089
|
if (leftResult === false && rightResult === false) return false;
|
|
40356
41090
|
return null;
|
|
40357
41091
|
};
|
|
40358
|
-
const readHydrationConditionResult = (expression, context, runtime) => {
|
|
41092
|
+
const readHydrationConditionResult = (expression, context, runtime, state) => {
|
|
40359
41093
|
const unwrappedExpression = stripParenExpression(expression);
|
|
40360
41094
|
const predicateMatch = matchBrowserPredicate(unwrappedExpression, context);
|
|
40361
41095
|
if (predicateMatch) return predicateMatch[`${runtime}Result`];
|
|
40362
41096
|
const staticResult = readInitialStateBoolean(unwrappedExpression, context.scopes);
|
|
40363
41097
|
if (staticResult !== null) return staticResult;
|
|
41098
|
+
const expressionSymbol = isNodeOfType(unwrappedExpression, "Identifier") ? context.scopes.symbolFor(unwrappedExpression) : null;
|
|
41099
|
+
const parameterValue = expressionSymbol ? state.parameterValuesBySymbolId.get(expressionSymbol.id) : null;
|
|
41100
|
+
if (expressionSymbol && parameterValue && !state.visitedSymbolIds.has(expressionSymbol.id)) {
|
|
41101
|
+
state.visitedSymbolIds.add(expressionSymbol.id);
|
|
41102
|
+
const result = readHydrationConditionResult(parameterValue, context, runtime, state);
|
|
41103
|
+
state.visitedSymbolIds.delete(expressionSymbol.id);
|
|
41104
|
+
return result;
|
|
41105
|
+
}
|
|
41106
|
+
if (expressionSymbol && expressionSymbol.kind === "const" && expressionSymbol.initializer && expressionSymbol.references.every((reference) => reference.flag === "read") && !state.visitedSymbolIds.has(expressionSymbol.id)) {
|
|
41107
|
+
state.visitedSymbolIds.add(expressionSymbol.id);
|
|
41108
|
+
const result = readHydrationConditionResult(expressionSymbol.initializer, context, runtime, state);
|
|
41109
|
+
state.visitedSymbolIds.delete(expressionSymbol.id);
|
|
41110
|
+
return result;
|
|
41111
|
+
}
|
|
41112
|
+
if (isNodeOfType(unwrappedExpression, "CallExpression")) {
|
|
41113
|
+
const callArguments = unwrappedExpression.arguments ?? [];
|
|
41114
|
+
if (isReactApiCall(unwrappedExpression, "useMemo", context.scopes, {
|
|
41115
|
+
allowGlobalReactNamespace: true,
|
|
41116
|
+
resolveNamedAliases: true
|
|
41117
|
+
})) {
|
|
41118
|
+
const callbackArgument = callArguments[0];
|
|
41119
|
+
if (!callbackArgument || isNodeOfType(callbackArgument, "SpreadElement")) return null;
|
|
41120
|
+
const callbackFunction = resolveExactLocalFunction(callbackArgument, context.scopes);
|
|
41121
|
+
return isFunctionLike$1(callbackFunction) && callbackFunction.params.length === 0 ? readHydrationFunctionResult(callbackFunction, context, runtime, state) : null;
|
|
41122
|
+
}
|
|
41123
|
+
const callee = stripParenExpression(unwrappedExpression.callee);
|
|
41124
|
+
if (isNodeOfType(callee, "Identifier") && callee.name === "Boolean" && context.scopes.isGlobalReference(callee) && callArguments.length === 1 && !isNodeOfType(callArguments[0], "SpreadElement")) return readHydrationConditionResult(callArguments[0], context, runtime, state);
|
|
41125
|
+
const helperFunction = resolveExactLocalFunction(callee, context.scopes);
|
|
41126
|
+
if (!isFunctionLike$1(helperFunction) || helperFunction.async || isNodeOfType(helperFunction, "FunctionDeclaration") && helperFunction.generator || isNodeOfType(helperFunction, "FunctionExpression") && helperFunction.generator || helperFunction.params.some((parameter) => !isNodeOfType(parameter, "Identifier")) || callArguments.some((argument) => isNodeOfType(argument, "SpreadElement"))) return null;
|
|
41127
|
+
const parameterValuesBySymbolId = new Map(state.parameterValuesBySymbolId);
|
|
41128
|
+
for (let parameterIndex = 0; parameterIndex < helperFunction.params.length; parameterIndex++) {
|
|
41129
|
+
const parameter = helperFunction.params[parameterIndex];
|
|
41130
|
+
const argument = callArguments[parameterIndex];
|
|
41131
|
+
if (!argument || !isNodeOfType(parameter, "Identifier")) continue;
|
|
41132
|
+
const parameterSymbol = context.scopes.symbolFor(parameter);
|
|
41133
|
+
if (parameterSymbol) parameterValuesBySymbolId.set(parameterSymbol.id, argument);
|
|
41134
|
+
}
|
|
41135
|
+
return readHydrationFunctionResult(helperFunction, context, runtime, {
|
|
41136
|
+
...state,
|
|
41137
|
+
parameterValuesBySymbolId
|
|
41138
|
+
});
|
|
41139
|
+
}
|
|
40364
41140
|
if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") {
|
|
40365
|
-
const argumentResult = readHydrationConditionResult(unwrappedExpression.argument, context, runtime);
|
|
41141
|
+
const argumentResult = readHydrationConditionResult(unwrappedExpression.argument, context, runtime, state);
|
|
40366
41142
|
return argumentResult === null ? null : !argumentResult;
|
|
40367
41143
|
}
|
|
40368
41144
|
if (!isNodeOfType(unwrappedExpression, "LogicalExpression") || unwrappedExpression.operator !== "&&" && unwrappedExpression.operator !== "||") return null;
|
|
40369
|
-
return readLogicalConditionResult(unwrappedExpression.operator, readHydrationConditionResult(unwrappedExpression.left, context, runtime), readHydrationConditionResult(unwrappedExpression.right, context, runtime));
|
|
41145
|
+
return readLogicalConditionResult(unwrappedExpression.operator, readHydrationConditionResult(unwrappedExpression.left, context, runtime, state), readHydrationConditionResult(unwrappedExpression.right, context, runtime, state));
|
|
41146
|
+
};
|
|
41147
|
+
const readHydrationStatementResult = (statement, context, runtime, state) => {
|
|
41148
|
+
if (isNodeOfType(statement, "ReturnStatement")) return {
|
|
41149
|
+
didReturn: true,
|
|
41150
|
+
value: statement.argument ? readHydrationConditionResult(statement.argument, context, runtime, state) : null
|
|
41151
|
+
};
|
|
41152
|
+
if (isNodeOfType(statement, "BlockStatement")) {
|
|
41153
|
+
for (const childStatement of statement.body) {
|
|
41154
|
+
const result = readHydrationStatementResult(childStatement, context, runtime, state);
|
|
41155
|
+
if (result.didReturn) return result;
|
|
41156
|
+
if (statementAlwaysExits(childStatement)) break;
|
|
41157
|
+
}
|
|
41158
|
+
return {
|
|
41159
|
+
didReturn: false,
|
|
41160
|
+
value: null
|
|
41161
|
+
};
|
|
41162
|
+
}
|
|
41163
|
+
if (!isNodeOfType(statement, "IfStatement")) return {
|
|
41164
|
+
didReturn: false,
|
|
41165
|
+
value: null
|
|
41166
|
+
};
|
|
41167
|
+
const conditionResult = readHydrationConditionResult(statement.test, context, runtime, state);
|
|
41168
|
+
if (conditionResult !== null) {
|
|
41169
|
+
const selectedBranch = conditionResult ? statement.consequent : statement.alternate;
|
|
41170
|
+
return selectedBranch ? readHydrationStatementResult(selectedBranch, context, runtime, state) : {
|
|
41171
|
+
didReturn: false,
|
|
41172
|
+
value: null
|
|
41173
|
+
};
|
|
41174
|
+
}
|
|
41175
|
+
const consequentResult = readHydrationStatementResult(statement.consequent, context, runtime, state);
|
|
41176
|
+
const alternateResult = statement.alternate ? readHydrationStatementResult(statement.alternate, context, runtime, state) : {
|
|
41177
|
+
didReturn: false,
|
|
41178
|
+
value: null
|
|
41179
|
+
};
|
|
41180
|
+
return consequentResult.didReturn && alternateResult.didReturn && consequentResult.value !== null && consequentResult.value === alternateResult.value ? consequentResult : {
|
|
41181
|
+
didReturn: consequentResult.didReturn || alternateResult.didReturn,
|
|
41182
|
+
value: null
|
|
41183
|
+
};
|
|
40370
41184
|
};
|
|
40371
|
-
const
|
|
41185
|
+
const readHydrationFunctionResult = (functionNode, context, runtime, state) => {
|
|
41186
|
+
if (!isFunctionLike$1(functionNode) || state.visitedFunctionNodes.has(functionNode)) return null;
|
|
41187
|
+
state.visitedFunctionNodes.add(functionNode);
|
|
41188
|
+
const result = isNodeOfType(functionNode.body, "BlockStatement") ? readHydrationStatementResult(functionNode.body, context, runtime, state).value : readHydrationConditionResult(functionNode.body, context, runtime, state);
|
|
41189
|
+
state.visitedFunctionNodes.delete(functionNode);
|
|
41190
|
+
return result;
|
|
41191
|
+
};
|
|
41192
|
+
const doEquivalentExpressionBindingsMatch = (leftExpression, rightExpression, scopes) => {
|
|
41193
|
+
const left = stripParenExpression(leftExpression);
|
|
41194
|
+
const right = stripParenExpression(rightExpression);
|
|
41195
|
+
if (isNodeOfType(left, "Identifier") && isNodeOfType(right, "Identifier")) {
|
|
41196
|
+
const leftSymbol = scopes.symbolFor(left);
|
|
41197
|
+
const rightSymbol = scopes.symbolFor(right);
|
|
41198
|
+
return leftSymbol || rightSymbol ? leftSymbol?.id === rightSymbol?.id : true;
|
|
41199
|
+
}
|
|
41200
|
+
if (isNodeOfType(left, "MemberExpression") && isNodeOfType(right, "MemberExpression")) return doEquivalentExpressionBindingsMatch(left.object, right.object, scopes) && (!left.computed || doEquivalentExpressionBindingsMatch(left.property, right.property, scopes));
|
|
41201
|
+
if (isNodeOfType(left, "CallExpression") && isNodeOfType(right, "CallExpression")) {
|
|
41202
|
+
const rightArguments = right.arguments ?? [];
|
|
41203
|
+
return doEquivalentExpressionBindingsMatch(left.callee, right.callee, scopes) && (left.arguments ?? []).every((argument, index) => {
|
|
41204
|
+
const rightArgument = rightArguments[index];
|
|
41205
|
+
return Boolean(rightArgument && doEquivalentExpressionBindingsMatch(argument, rightArgument, scopes));
|
|
41206
|
+
});
|
|
41207
|
+
}
|
|
41208
|
+
return true;
|
|
41209
|
+
};
|
|
41210
|
+
const areHelperReturnValuesEquivalent = (leftValue, rightValue, context) => {
|
|
41211
|
+
if (areExpressionsStructurallyEqual(leftValue, rightValue)) return doEquivalentExpressionBindingsMatch(leftValue, rightValue, context.scopes);
|
|
41212
|
+
const leftBoolean = readInitialStateBoolean(leftValue, context.scopes);
|
|
41213
|
+
const rightBoolean = readInitialStateBoolean(rightValue, context.scopes);
|
|
41214
|
+
return leftBoolean !== null && rightBoolean !== null && leftBoolean === rightBoolean;
|
|
41215
|
+
};
|
|
41216
|
+
const doHelperReturnValuesDiffer = (leftValues, rightValues, context) => {
|
|
41217
|
+
const everyValueHasEquivalent = (values, candidateValues) => values.every((value) => candidateValues.some((candidateValue) => areHelperReturnValuesEquivalent(value, candidateValue, context)));
|
|
41218
|
+
return !everyValueHasEquivalent(leftValues, rightValues) || !everyValueHasEquivalent(rightValues, leftValues);
|
|
41219
|
+
};
|
|
41220
|
+
const matchHydrationConditionInternal = (expression, context, state) => {
|
|
40372
41221
|
const unwrappedExpression = stripParenExpression(expression);
|
|
40373
41222
|
const predicateMatch = matchBrowserPredicate(unwrappedExpression, context);
|
|
40374
41223
|
if (predicateMatch) return {
|
|
40375
41224
|
predicateMatch,
|
|
40376
41225
|
predicateNode: unwrappedExpression
|
|
40377
41226
|
};
|
|
40378
|
-
if (isNodeOfType(unwrappedExpression, "
|
|
40379
|
-
|
|
40380
|
-
|
|
40381
|
-
|
|
40382
|
-
|
|
40383
|
-
|
|
40384
|
-
|
|
40385
|
-
|
|
41227
|
+
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
41228
|
+
const symbol = context.scopes.symbolFor(unwrappedExpression);
|
|
41229
|
+
const parameterValue = symbol ? state.parameterValuesBySymbolId.get(symbol.id) : null;
|
|
41230
|
+
if (symbol && parameterValue && !state.visitedSymbolIds.has(symbol.id)) {
|
|
41231
|
+
state.visitedSymbolIds.add(symbol.id);
|
|
41232
|
+
const match = matchHydrationConditionInternal(parameterValue, context, state);
|
|
41233
|
+
state.visitedSymbolIds.delete(symbol.id);
|
|
41234
|
+
return match;
|
|
41235
|
+
}
|
|
41236
|
+
if (!symbol || symbol.kind !== "const" || !symbol.initializer || symbol.references.some((reference) => reference.flag !== "read") || state.visitedSymbolIds.has(symbol.id)) return null;
|
|
41237
|
+
state.visitedSymbolIds.add(symbol.id);
|
|
41238
|
+
const match = matchHydrationConditionInternal(symbol.initializer, context, state);
|
|
41239
|
+
state.visitedSymbolIds.delete(symbol.id);
|
|
41240
|
+
return match;
|
|
40386
41241
|
}
|
|
41242
|
+
if (isNodeOfType(unwrappedExpression, "CallExpression")) {
|
|
41243
|
+
const callArguments = unwrappedExpression.arguments ?? [];
|
|
41244
|
+
if (isReactApiCall(unwrappedExpression, "useMemo", context.scopes, {
|
|
41245
|
+
allowGlobalReactNamespace: true,
|
|
41246
|
+
resolveNamedAliases: true
|
|
41247
|
+
})) {
|
|
41248
|
+
const callbackArgument = callArguments[0];
|
|
41249
|
+
if (!callbackArgument || isNodeOfType(callbackArgument, "SpreadElement")) return null;
|
|
41250
|
+
const callbackFunction = resolveExactLocalFunction(callbackArgument, context.scopes);
|
|
41251
|
+
return isFunctionLike$1(callbackFunction) && callbackFunction.params.length === 0 ? matchHydrationFunctionResult(callbackFunction, context, state) : null;
|
|
41252
|
+
}
|
|
41253
|
+
const callee = stripParenExpression(unwrappedExpression.callee);
|
|
41254
|
+
if (isNodeOfType(callee, "Identifier") && callee.name === "Boolean" && context.scopes.isGlobalReference(callee) && callArguments.length === 1 && !isNodeOfType(callArguments[0], "SpreadElement")) return matchHydrationConditionInternal(callArguments[0], context, state);
|
|
41255
|
+
const helperFunction = resolveExactLocalFunction(callee, context.scopes);
|
|
41256
|
+
if (!isFunctionLike$1(helperFunction) || helperFunction.async || isNodeOfType(helperFunction, "FunctionDeclaration") && helperFunction.generator || isNodeOfType(helperFunction, "FunctionExpression") && helperFunction.generator || helperFunction.params.some((parameter) => !isNodeOfType(parameter, "Identifier")) || callArguments.some((argument) => isNodeOfType(argument, "SpreadElement"))) return null;
|
|
41257
|
+
const parameterValuesBySymbolId = new Map(state.parameterValuesBySymbolId);
|
|
41258
|
+
for (let parameterIndex = 0; parameterIndex < helperFunction.params.length; parameterIndex++) {
|
|
41259
|
+
const parameter = helperFunction.params[parameterIndex];
|
|
41260
|
+
const argument = callArguments[parameterIndex];
|
|
41261
|
+
if (!argument || !isNodeOfType(parameter, "Identifier")) continue;
|
|
41262
|
+
const parameterSymbol = context.scopes.symbolFor(parameter);
|
|
41263
|
+
if (parameterSymbol) parameterValuesBySymbolId.set(parameterSymbol.id, argument);
|
|
41264
|
+
}
|
|
41265
|
+
return matchHydrationFunctionResult(helperFunction, context, {
|
|
41266
|
+
...state,
|
|
41267
|
+
parameterValuesBySymbolId
|
|
41268
|
+
});
|
|
41269
|
+
}
|
|
41270
|
+
if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") return matchHydrationConditionInternal(unwrappedExpression.argument, context, state);
|
|
41271
|
+
if (!isNodeOfType(unwrappedExpression, "LogicalExpression") || unwrappedExpression.operator !== "&&" && unwrappedExpression.operator !== "||") return null;
|
|
41272
|
+
const leftMatch = matchHydrationConditionInternal(unwrappedExpression.left, context, state);
|
|
41273
|
+
const rightMatch = matchHydrationConditionInternal(unwrappedExpression.right, context, state);
|
|
40387
41274
|
const nestedMatch = leftMatch ?? rightMatch;
|
|
40388
41275
|
if (!nestedMatch) return null;
|
|
40389
|
-
const
|
|
40390
|
-
|
|
40391
|
-
return nestedMatch;
|
|
41276
|
+
const clientResult = readHydrationConditionResult(unwrappedExpression, context, "client", state);
|
|
41277
|
+
const serverResult = readHydrationConditionResult(unwrappedExpression, context, "server", state);
|
|
41278
|
+
return clientResult !== null && serverResult !== null && clientResult === serverResult ? null : nestedMatch;
|
|
40392
41279
|
};
|
|
41280
|
+
const matchHydrationReturningStatement = (statement, context, state) => {
|
|
41281
|
+
if (isNodeOfType(statement, "ReturnStatement")) return statement.argument ? matchHydrationConditionInternal(statement.argument, context, state) : null;
|
|
41282
|
+
if (isNodeOfType(statement, "IfStatement")) {
|
|
41283
|
+
const conditionMatch = matchHydrationConditionInternal(statement.test, context, state);
|
|
41284
|
+
const consequentValues = getReturnedValues(statement.consequent);
|
|
41285
|
+
const alternateValues = statement.alternate ? getReturnedValues(statement.alternate) : findFollowingReturnedValues(statement);
|
|
41286
|
+
if (conditionMatch && consequentValues.length > 0 && alternateValues.length > 0 && doHelperReturnValuesDiffer(consequentValues, alternateValues, context)) return conditionMatch;
|
|
41287
|
+
return matchHydrationReturningStatement(statement.consequent, context, state) ?? (statement.alternate ? matchHydrationReturningStatement(statement.alternate, context, state) : null);
|
|
41288
|
+
}
|
|
41289
|
+
if (!isNodeOfType(statement, "BlockStatement")) return null;
|
|
41290
|
+
for (const childStatement of statement.body) {
|
|
41291
|
+
const match = matchHydrationReturningStatement(childStatement, context, state);
|
|
41292
|
+
if (match) return match;
|
|
41293
|
+
if (statementAlwaysExits(childStatement)) break;
|
|
41294
|
+
}
|
|
41295
|
+
return null;
|
|
41296
|
+
};
|
|
41297
|
+
const matchHydrationFunctionResult = (functionNode, context, state) => {
|
|
41298
|
+
if (!isFunctionLike$1(functionNode) || state.visitedFunctionNodes.has(functionNode)) return null;
|
|
41299
|
+
state.visitedFunctionNodes.add(functionNode);
|
|
41300
|
+
const match = isNodeOfType(functionNode.body, "BlockStatement") ? matchHydrationReturningStatement(functionNode.body, context, state) : matchHydrationConditionInternal(functionNode.body, context, state);
|
|
41301
|
+
state.visitedFunctionNodes.delete(functionNode);
|
|
41302
|
+
return match;
|
|
41303
|
+
};
|
|
41304
|
+
const matchHydrationCondition = (expression, context) => matchHydrationConditionInternal(expression, context, {
|
|
41305
|
+
parameterValuesBySymbolId: /* @__PURE__ */ new Map(),
|
|
41306
|
+
visitedFunctionNodes: /* @__PURE__ */ new Set(),
|
|
41307
|
+
visitedSymbolIds: /* @__PURE__ */ new Set()
|
|
41308
|
+
});
|
|
40393
41309
|
const areNodeArraysEquivalent = (leftNodes, rightNodes) => leftNodes.length === rightNodes.length && leftNodes.every((leftNode, index) => areRenderedBranchesEquivalent(leftNode, rightNodes[index]));
|
|
40394
41310
|
const areRenderedBranchesEquivalent = (leftNode, rightNode) => {
|
|
40395
41311
|
if (!leftNode || !rightNode) return leftNode === rightNode;
|
|
@@ -40532,17 +41448,17 @@ const noHydrationBranchOnBrowserGlobal = defineRule({
|
|
|
40532
41448
|
const { predicateMatch, predicateNode } = conditionMatch;
|
|
40533
41449
|
if (reportedNodes.has(predicateNode)) return;
|
|
40534
41450
|
if (rightBranch && areRenderedBranchesEquivalent(leftBranch, rightBranch)) return;
|
|
40535
|
-
const componentOrHookNode = findRenderPhaseComponentOrHook(
|
|
41451
|
+
const componentOrHookNode = findRenderPhaseComponentOrHook(conditionNode, context.scopes);
|
|
40536
41452
|
if (!componentOrHookNode) return;
|
|
40537
41453
|
if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
|
|
40538
|
-
if (requiresRenderedContext && !isInRenderedOutput(
|
|
41454
|
+
if (requiresRenderedContext && !isInRenderedOutput(conditionNode, componentOrHookNode, context.scopes)) return;
|
|
40539
41455
|
if (!isRenderedValue(leftBranch) && (!rightBranch || !isRenderedValue(rightBranch))) {
|
|
40540
|
-
const attribute = findEnclosingJsxAttribute(
|
|
41456
|
+
const attribute = findEnclosingJsxAttribute(conditionNode);
|
|
40541
41457
|
if (!attribute || isEventHandlerAttribute(attribute)) return;
|
|
40542
41458
|
}
|
|
40543
|
-
if (fileIsEmailTemplate || isGatedByFalsyInitialState(
|
|
40544
|
-
if (isAfterClientOnlyEarlyReturn(
|
|
40545
|
-
const openingElement = findEnclosingJsxOpeningElement(
|
|
41459
|
+
if (fileIsEmailTemplate || isGatedByFalsyInitialState(conditionNode, context.scopes)) return;
|
|
41460
|
+
if (isAfterClientOnlyEarlyReturn(conditionNode, componentOrHookNode, context.scopes)) return;
|
|
41461
|
+
const openingElement = findEnclosingJsxOpeningElement(conditionNode);
|
|
40546
41462
|
if (hasSuppressHydrationWarningAttribute(openingElement) && !isStructuralRenderedValue(leftBranch) && !isStructuralRenderedValue(rightBranch)) return;
|
|
40547
41463
|
if (branchRootsSuppressSameElement(leftBranch, rightBranch)) return;
|
|
40548
41464
|
if (isGeneratedImageRenderContext(context, openingElement ?? leftBranch)) return;
|
|
@@ -40924,7 +41840,7 @@ const noInitializeState = defineRule({
|
|
|
40924
41840
|
if (!dependencies || !isNodeOfType(dependencies, "ArrayExpression") || (dependencies.elements ?? []).length !== 0) return;
|
|
40925
41841
|
const analysis = getProgramAnalysis(node);
|
|
40926
41842
|
if (!analysis) return;
|
|
40927
|
-
for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
|
|
41843
|
+
for (const fact of collectEffectStateWriteFacts(analysis, context, node, context.filename)) {
|
|
40928
41844
|
if (!fact.isRenderKnownCopy || fact.matchesStateInitializer || fact.resetsSourceState) continue;
|
|
40929
41845
|
const stateName = getStateName(fact.stateDeclarator);
|
|
40930
41846
|
context.report({
|
|
@@ -44331,6 +45247,114 @@ const DATA_SINK_METHOD_NAMES = new Set([
|
|
|
44331
45247
|
"deserialize"
|
|
44332
45248
|
]);
|
|
44333
45249
|
//#endregion
|
|
45250
|
+
//#region src/plugin/utils/get-transparent-react-callback-wrapper-argument.ts
|
|
45251
|
+
const getTransparentReactCallbackWrapperArgument = (initializer, resultSymbol, scopes) => {
|
|
45252
|
+
const callExpression = stripParenExpression(initializer);
|
|
45253
|
+
if (!isNodeOfType(callExpression, "CallExpression")) return null;
|
|
45254
|
+
const callbackArgument = callExpression.arguments[0];
|
|
45255
|
+
if (!callbackArgument) return null;
|
|
45256
|
+
if (resultSymbol && symbolHasReactUseEffectEventOrigin(resultSymbol, scopes)) return callbackArgument;
|
|
45257
|
+
return isReactApiCall(callExpression, "useCallback", scopes, {
|
|
45258
|
+
allowGlobalReactNamespace: true,
|
|
45259
|
+
allowUnboundBareCalls: true
|
|
45260
|
+
}) ? callbackArgument : null;
|
|
45261
|
+
};
|
|
45262
|
+
//#endregion
|
|
45263
|
+
//#region src/plugin/rules/state-and-effects/utils/resolve-parent-callback-provenance.ts
|
|
45264
|
+
const getDeclarationKind$1 = (declarator) => {
|
|
45265
|
+
const declaration = declarator.parent;
|
|
45266
|
+
return declaration && isNodeOfType(declaration, "VariableDeclaration") ? declaration.kind : null;
|
|
45267
|
+
};
|
|
45268
|
+
const hasMutableBindingWrite$2 = (reference) => Boolean(reference.resolved?.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init));
|
|
45269
|
+
const mergeRequiredBranches = (leftNames, rightNames) => {
|
|
45270
|
+
if (!leftNames || !rightNames) return null;
|
|
45271
|
+
return new Set([...leftNames, ...rightNames]);
|
|
45272
|
+
};
|
|
45273
|
+
const getPropReferenceName = (analysis, identifier) => {
|
|
45274
|
+
if (!isNodeOfType(identifier, "Identifier")) return null;
|
|
45275
|
+
const reference = getRef(analysis, identifier);
|
|
45276
|
+
if (!reference || !isProp(analysis, reference) || isWholePropsObjectReference(analysis, reference)) return null;
|
|
45277
|
+
const bindingIdentifier = (reference.resolved?.defs.find((definition) => definition.type === "Parameter"))?.name;
|
|
45278
|
+
return (bindingIdentifier && getDestructuredBindingPropertyName(bindingIdentifier)) ?? identifier.name;
|
|
45279
|
+
};
|
|
45280
|
+
const getSingleConstDeclarator = (reference) => {
|
|
45281
|
+
if (!reference.resolved || hasMutableBindingWrite$2(reference)) return null;
|
|
45282
|
+
const declarators = reference.resolved.defs.map((definition) => definition.node).filter((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
|
|
45283
|
+
if (declarators.length !== 1) return null;
|
|
45284
|
+
const declarator = declarators[0];
|
|
45285
|
+
if (!declarator || getDeclarationKind$1(declarator) !== "const") return null;
|
|
45286
|
+
return declarator;
|
|
45287
|
+
};
|
|
45288
|
+
const resolveParentCallbackPropNames = (analysis, expression, scopes, visitedReferences, allowFunctionForwarder = false) => {
|
|
45289
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
45290
|
+
if (isFunctionLike$1(unwrappedExpression)) {
|
|
45291
|
+
if (!allowFunctionForwarder || Boolean(unwrappedExpression.async)) return null;
|
|
45292
|
+
const callbackNames = /* @__PURE__ */ new Set();
|
|
45293
|
+
walkInsideStatementBlocks(unwrappedExpression.body, (child) => {
|
|
45294
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
45295
|
+
const resolvedNames = resolveParentCallbackPropNames(analysis, child.callee, scopes, new Set(visitedReferences), false);
|
|
45296
|
+
if (!resolvedNames) return;
|
|
45297
|
+
for (const resolvedName of resolvedNames) callbackNames.add(resolvedName);
|
|
45298
|
+
});
|
|
45299
|
+
return callbackNames.size > 0 ? callbackNames : null;
|
|
45300
|
+
}
|
|
45301
|
+
if (isNodeOfType(unwrappedExpression, "ConditionalExpression")) return mergeRequiredBranches(resolveParentCallbackPropNames(analysis, unwrappedExpression.consequent, scopes, new Set(visitedReferences), false), resolveParentCallbackPropNames(analysis, unwrappedExpression.alternate, scopes, new Set(visitedReferences), false));
|
|
45302
|
+
if (isNodeOfType(unwrappedExpression, "LogicalExpression")) return mergeRequiredBranches(resolveParentCallbackPropNames(analysis, unwrappedExpression.left, scopes, new Set(visitedReferences), false), resolveParentCallbackPropNames(analysis, unwrappedExpression.right, scopes, new Set(visitedReferences)));
|
|
45303
|
+
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
45304
|
+
const propName = getPropReferenceName(analysis, unwrappedExpression);
|
|
45305
|
+
if (propName) return new Set([propName]);
|
|
45306
|
+
const reference = getRef(analysis, unwrappedExpression);
|
|
45307
|
+
if (!reference?.resolved || visitedReferences.has(reference.resolved)) return null;
|
|
45308
|
+
const declarator = getSingleConstDeclarator(reference);
|
|
45309
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !declarator.init) return null;
|
|
45310
|
+
visitedReferences.add(reference.resolved);
|
|
45311
|
+
const wrappedArgument = getTransparentReactCallbackWrapperArgument(declarator.init, scopes.symbolFor(unwrappedExpression), scopes);
|
|
45312
|
+
const allowsFunctionForwarder = Boolean(wrappedArgument && !isReactApiCall(declarator.init, "useCallback", scopes, {
|
|
45313
|
+
allowGlobalReactNamespace: true,
|
|
45314
|
+
allowUnboundBareCalls: true
|
|
45315
|
+
}));
|
|
45316
|
+
return resolveParentCallbackPropNames(analysis, wrappedArgument ?? declarator.init, scopes, visitedReferences, allowsFunctionForwarder);
|
|
45317
|
+
}
|
|
45318
|
+
if (!isNodeOfType(unwrappedExpression, "MemberExpression")) return null;
|
|
45319
|
+
const propertyName = getStaticMemberPropertyName(unwrappedExpression);
|
|
45320
|
+
if (!propertyName) return null;
|
|
45321
|
+
const receiver = stripParenExpression(unwrappedExpression.object);
|
|
45322
|
+
if (!isNodeOfType(receiver, "Identifier")) return null;
|
|
45323
|
+
const receiverReference = getRef(analysis, receiver);
|
|
45324
|
+
if (!receiverReference?.resolved || visitedReferences.has(receiverReference.resolved)) return null;
|
|
45325
|
+
if (isWholePropsObjectReference(analysis, receiverReference)) return new Set([propertyName]);
|
|
45326
|
+
const declarator = getSingleConstDeclarator(receiverReference);
|
|
45327
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !declarator.init) return null;
|
|
45328
|
+
visitedReferences.add(receiverReference.resolved);
|
|
45329
|
+
const initializer = stripParenExpression(declarator.init);
|
|
45330
|
+
if (propertyName === "current" && isNodeOfType(initializer, "CallExpression")) {
|
|
45331
|
+
if (!isReactApiCall(initializer, "useRef", scopes, {
|
|
45332
|
+
allowGlobalReactNamespace: true,
|
|
45333
|
+
allowUnboundBareCalls: true
|
|
45334
|
+
})) return null;
|
|
45335
|
+
const callbackArgument = initializer.arguments[0];
|
|
45336
|
+
if (!callbackArgument) return null;
|
|
45337
|
+
let callbackNames = resolveParentCallbackPropNames(analysis, callbackArgument, scopes, new Set(visitedReferences), false);
|
|
45338
|
+
if (!callbackNames) return null;
|
|
45339
|
+
for (const candidateReference of receiverReference.resolved.references) {
|
|
45340
|
+
const candidateIdentifier = candidateReference.identifier;
|
|
45341
|
+
const candidateMember = candidateIdentifier.parent;
|
|
45342
|
+
if (!candidateMember || !isNodeOfType(candidateMember, "MemberExpression") || candidateMember.object !== candidateIdentifier || getStaticMemberPropertyName(candidateMember) !== "current") continue;
|
|
45343
|
+
const assignment = candidateMember.parent;
|
|
45344
|
+
if (!assignment || !isNodeOfType(assignment, "AssignmentExpression") || assignment.left !== candidateMember) continue;
|
|
45345
|
+
if (assignment.operator !== "=") return null;
|
|
45346
|
+
callbackNames = mergeRequiredBranches(callbackNames, resolveParentCallbackPropNames(analysis, assignment.right, scopes, new Set(visitedReferences), false));
|
|
45347
|
+
if (!callbackNames) return null;
|
|
45348
|
+
}
|
|
45349
|
+
return callbackNames;
|
|
45350
|
+
}
|
|
45351
|
+
if (!isNodeOfType(initializer, "ObjectExpression")) return null;
|
|
45352
|
+
const property = initializer.properties.find((candidateProperty) => isNodeOfType(candidateProperty, "Property") && getStaticPropertyKeyName(candidateProperty, { allowComputedString: true }) === propertyName);
|
|
45353
|
+
if (!property || !isNodeOfType(property, "Property")) return null;
|
|
45354
|
+
return resolveParentCallbackPropNames(analysis, property.value, scopes, visitedReferences, false);
|
|
45355
|
+
};
|
|
45356
|
+
const getParentCallbackPropNames = ({ analysis, expression, scopes }) => resolveParentCallbackPropNames(analysis, expression, scopes, /* @__PURE__ */ new Set(), false);
|
|
45357
|
+
//#endregion
|
|
44334
45358
|
//#region src/plugin/rules/state-and-effects/no-pass-data-to-parent.ts
|
|
44335
45359
|
const isUseStateIdentifier = (identifier) => {
|
|
44336
45360
|
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
@@ -44359,14 +45383,18 @@ const FUNCTION_WRAPPER_HOOK_NAMES$1 = new Set([
|
|
|
44359
45383
|
"useStableCallback",
|
|
44360
45384
|
"useCallbackRef"
|
|
44361
45385
|
]);
|
|
44362
|
-
const getWrapperHookWrappedFunction = (initializer) => {
|
|
45386
|
+
const getWrapperHookWrappedFunction = (initializer, resultSymbol, scopes) => {
|
|
44363
45387
|
if (!isNodeOfType(initializer, "CallExpression")) return null;
|
|
45388
|
+
const transparentReactArgument = getTransparentReactCallbackWrapperArgument(initializer, resultSymbol, scopes);
|
|
45389
|
+
if (transparentReactArgument) return transparentReactArgument;
|
|
44364
45390
|
const callee = initializer.callee;
|
|
44365
45391
|
const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") ? callee.property.name : null;
|
|
44366
45392
|
if (!calleeName || !FUNCTION_WRAPPER_HOOK_NAMES$1.has(calleeName)) return null;
|
|
44367
45393
|
const wrapped = initializer.arguments?.[0];
|
|
44368
|
-
if (!wrapped
|
|
44369
|
-
return
|
|
45394
|
+
if (!wrapped) return null;
|
|
45395
|
+
if (calleeName === "useEffectEvent") return null;
|
|
45396
|
+
if (isFunctionLike$1(wrapped)) return wrapped;
|
|
45397
|
+
return null;
|
|
44370
45398
|
};
|
|
44371
45399
|
const HANDLER_NAMED_PROP_PATTERN = /^(on|handle)[A-Z]/;
|
|
44372
45400
|
const wrappedFunctionNotifiesParent = (analysis, wrappedFunction) => getDownstreamRefs(analysis, wrappedFunction).some((innerRef) => {
|
|
@@ -44376,16 +45404,29 @@ const wrappedFunctionNotifiesParent = (analysis, wrappedFunction) => getDownstre
|
|
|
44376
45404
|
const innerParent = innerIdentifier.parent;
|
|
44377
45405
|
return Boolean(innerParent && isNodeOfType(innerParent, "CallExpression") && innerParent.callee === innerIdentifier);
|
|
44378
45406
|
});
|
|
44379
|
-
const isDirectParentCallbackRef = (analysis, ref) => {
|
|
45407
|
+
const isDirectParentCallbackRef = (analysis, ref, scopes) => {
|
|
44380
45408
|
if (isProp(analysis, ref)) return true;
|
|
45409
|
+
if (hasMutableBindingWrite$1(ref)) {
|
|
45410
|
+
if (!(ref.resolved?.references.filter((candidateReference) => candidateReference.isWrite() && !candidateReference.init) ?? []).every((candidateReference) => {
|
|
45411
|
+
const candidateIdentifier = candidateReference.identifier;
|
|
45412
|
+
const assignment = candidateIdentifier.parent;
|
|
45413
|
+
if (!assignment || !isNodeOfType(assignment, "AssignmentExpression") || assignment.operator !== "=" || assignment.left !== candidateIdentifier) return false;
|
|
45414
|
+
const assignedReferences = getDownstreamRefs(analysis, assignment.right);
|
|
45415
|
+
return assignedReferences.length > 0 && assignedReferences.every((assignedReference) => isProp(analysis, assignedReference));
|
|
45416
|
+
})) return false;
|
|
45417
|
+
}
|
|
44381
45418
|
return Boolean(ref.resolved?.defs.some((def) => {
|
|
44382
45419
|
const node = def.node;
|
|
44383
45420
|
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
44384
45421
|
const initializer = unwrapChainExpression(node.init);
|
|
44385
|
-
const wrappedFunction = getWrapperHookWrappedFunction(initializer);
|
|
45422
|
+
const wrappedFunction = getWrapperHookWrappedFunction(initializer, isNodeOfType(node.id, "Identifier") ? scopes.symbolFor(node.id) ?? null : null, scopes);
|
|
44386
45423
|
if (wrappedFunction) {
|
|
44387
45424
|
if (wrappedFunction.async) return false;
|
|
44388
|
-
return wrappedFunctionNotifiesParent(analysis, wrappedFunction);
|
|
45425
|
+
if (isFunctionLike$1(wrappedFunction)) return wrappedFunctionNotifiesParent(analysis, wrappedFunction);
|
|
45426
|
+
const directName = getParentCallbackPropName(analysis, wrappedFunction);
|
|
45427
|
+
const downstreamReferences = getDownstreamRefs(analysis, wrappedFunction);
|
|
45428
|
+
if (directName !== null) return true;
|
|
45429
|
+
return downstreamReferences.some((wrappedReference) => !hasMutableBindingWrite$1(wrappedReference) && getUpstreamRefs(analysis, wrappedReference).some((upstreamReference) => isProp(analysis, upstreamReference)));
|
|
44389
45430
|
}
|
|
44390
45431
|
if (!isNodeOfType(initializer, "Identifier") && !isNodeOfType(initializer, "MemberExpression")) return false;
|
|
44391
45432
|
return getDownstreamRefs(analysis, initializer).some((initializerRef) => getUpstreamRefs(analysis, initializerRef).some((upstreamRef) => isProp(analysis, upstreamRef)));
|
|
@@ -44395,7 +45436,7 @@ const getDeclarationKind = (declarator) => {
|
|
|
44395
45436
|
const declaration = declarator.parent;
|
|
44396
45437
|
return declaration && isNodeOfType(declaration, "VariableDeclaration") ? declaration.kind : null;
|
|
44397
45438
|
};
|
|
44398
|
-
const hasMutableBindingWrite = (reference) => Boolean(reference.resolved?.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init));
|
|
45439
|
+
const hasMutableBindingWrite$1 = (reference) => Boolean(reference.resolved?.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init));
|
|
44399
45440
|
const getParentCallbackPropName = (analysis, expression, visitedVariables = /* @__PURE__ */ new Set()) => {
|
|
44400
45441
|
const unwrappedExpression = stripParenExpression(expression);
|
|
44401
45442
|
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
@@ -44407,7 +45448,7 @@ const getParentCallbackPropName = (analysis, expression, visitedVariables = /* @
|
|
|
44407
45448
|
const bindingIdentifier = callbackVariable.defs.find((definition) => definition.type === "Parameter")?.name;
|
|
44408
45449
|
return (bindingIdentifier && getDestructuredBindingPropertyName(bindingIdentifier)) ?? unwrappedExpression.name;
|
|
44409
45450
|
}
|
|
44410
|
-
if (hasMutableBindingWrite(callbackReference)) return null;
|
|
45451
|
+
if (hasMutableBindingWrite$1(callbackReference)) return null;
|
|
44411
45452
|
const definitions = callbackVariable.defs.map((definition) => definition.node).filter((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
|
|
44412
45453
|
if (definitions.length !== 1) return null;
|
|
44413
45454
|
const declarator = definitions[0];
|
|
@@ -44473,7 +45514,7 @@ const getRefAliasDeclarator = (identifier) => {
|
|
|
44473
45514
|
const getRefBindingProvenance = (analysis, receiver, isReactUseRefCall) => {
|
|
44474
45515
|
if (!isNodeOfType(receiver, "Identifier")) return null;
|
|
44475
45516
|
const receiverReference = getRef(analysis, receiver);
|
|
44476
|
-
if (!receiverReference?.resolved || hasMutableBindingWrite(receiverReference)) return null;
|
|
45517
|
+
if (!receiverReference?.resolved || hasMutableBindingWrite$1(receiverReference)) return null;
|
|
44477
45518
|
const variables = /* @__PURE__ */ new Set();
|
|
44478
45519
|
let currentVariable = receiverReference.resolved;
|
|
44479
45520
|
let refCall = null;
|
|
@@ -44489,7 +45530,7 @@ const getRefBindingProvenance = (analysis, receiver, isReactUseRefCall) => {
|
|
|
44489
45530
|
}
|
|
44490
45531
|
if (getDeclarationKind(declarator) !== "const" || !isNodeOfType(stripParenExpression(declarator.init), "Identifier")) return null;
|
|
44491
45532
|
const upstreamReference = getRef(analysis, stripParenExpression(declarator.init));
|
|
44492
|
-
if (!upstreamReference?.resolved || hasMutableBindingWrite(upstreamReference)) return null;
|
|
45533
|
+
if (!upstreamReference?.resolved || hasMutableBindingWrite$1(upstreamReference)) return null;
|
|
44493
45534
|
currentVariable = upstreamReference.resolved;
|
|
44494
45535
|
}
|
|
44495
45536
|
if (!refCall) return null;
|
|
@@ -44564,7 +45605,7 @@ const isParentPropsContextMerge = (analysis, expression) => {
|
|
|
44564
45605
|
while (isNodeOfType(currentExpression, "Identifier")) {
|
|
44565
45606
|
const currentReference = getRef(analysis, currentExpression);
|
|
44566
45607
|
const currentVariable = currentReference?.resolved;
|
|
44567
|
-
if (!currentReference || !currentVariable || visitedVariables.has(currentVariable) || hasMutableBindingWrite(currentReference)) return false;
|
|
45608
|
+
if (!currentReference || !currentVariable || visitedVariables.has(currentVariable) || hasMutableBindingWrite$1(currentReference)) return false;
|
|
44568
45609
|
visitedVariables.add(currentVariable);
|
|
44569
45610
|
const definitions = currentVariable.defs.filter((definition) => isNodeOfType(definition.node, "VariableDeclarator"));
|
|
44570
45611
|
if (definitions.length !== 1) return false;
|
|
@@ -44578,11 +45619,11 @@ const isParentPropsContextMerge = (analysis, expression) => {
|
|
|
44578
45619
|
const propsExpression = stripParenExpression(propsSpread.argument);
|
|
44579
45620
|
if (!isNodeOfType(propsExpression, "Identifier")) return false;
|
|
44580
45621
|
const propsReference = getRef(analysis, propsExpression);
|
|
44581
|
-
if (!propsReference?.resolved || !isWholePropsObjectReference(analysis, propsReference) || hasMutableBindingWrite(propsReference) || propsReference.resolved.references.some((candidateReference) => candidateReference !== propsReference)) return false;
|
|
45622
|
+
if (!propsReference?.resolved || !isWholePropsObjectReference(analysis, propsReference) || hasMutableBindingWrite$1(propsReference) || propsReference.resolved.references.some((candidateReference) => candidateReference !== propsReference)) return false;
|
|
44582
45623
|
const contextExpression = stripParenExpression(contextSpread.argument);
|
|
44583
45624
|
if (!isNodeOfType(contextExpression, "Identifier")) return false;
|
|
44584
45625
|
const contextReference = getRef(analysis, contextExpression);
|
|
44585
|
-
if (!contextReference?.resolved || hasMutableBindingWrite(contextReference) || contextReference.resolved.references.some((candidateReference) => !candidateReference.init && candidateReference !== contextReference)) return false;
|
|
45626
|
+
if (!contextReference?.resolved || hasMutableBindingWrite$1(contextReference) || contextReference.resolved.references.some((candidateReference) => !candidateReference.init && candidateReference !== contextReference)) return false;
|
|
44586
45627
|
const contextInitializer = contextReference.resolved?.defs.map((definition) => definition.node).find((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
|
|
44587
45628
|
if (!contextInitializer || !isNodeOfType(contextInitializer, "VariableDeclarator") || getDeclarationKind(contextInitializer) !== "const" || !contextInitializer.init || !isNodeOfType(contextInitializer.init, "CallExpression")) return false;
|
|
44588
45629
|
const contextHook = stripParenExpression(contextInitializer.init.callee);
|
|
@@ -44596,7 +45637,7 @@ const getImmutableParentCallbackPropName = (analysis, expression) => {
|
|
|
44596
45637
|
while (isNodeOfType(currentExpression, "Identifier")) {
|
|
44597
45638
|
const currentReference = getRef(analysis, currentExpression);
|
|
44598
45639
|
const currentVariable = currentReference?.resolved;
|
|
44599
|
-
if (!currentReference || !currentVariable || visitedVariables.has(currentVariable) || hasMutableBindingWrite(currentReference)) return null;
|
|
45640
|
+
if (!currentReference || !currentVariable || visitedVariables.has(currentVariable) || hasMutableBindingWrite$1(currentReference)) return null;
|
|
44600
45641
|
visitedVariables.add(currentVariable);
|
|
44601
45642
|
const definition = currentVariable.defs.length === 1 ? currentVariable.defs[0] : null;
|
|
44602
45643
|
const bindingIdentifier = definition?.name;
|
|
@@ -44665,7 +45706,7 @@ const getCommandCallbackPropName = (analysis, expression, isReactUseRefCall) =>
|
|
|
44665
45706
|
while (isNodeOfType(currentExpression, "Identifier")) {
|
|
44666
45707
|
const callbackReference = getRef(analysis, currentExpression);
|
|
44667
45708
|
const callbackVariable = callbackReference?.resolved;
|
|
44668
|
-
if (!callbackReference || !callbackVariable || visitedVariables.has(callbackVariable) || hasMutableBindingWrite(callbackReference)) return null;
|
|
45709
|
+
if (!callbackReference || !callbackVariable || visitedVariables.has(callbackVariable) || hasMutableBindingWrite$1(callbackReference)) return null;
|
|
44669
45710
|
visitedVariables.add(callbackVariable);
|
|
44670
45711
|
const definition = callbackVariable.defs.length === 1 ? callbackVariable.defs[0] : null;
|
|
44671
45712
|
const declarator = definition?.node;
|
|
@@ -44685,10 +45726,11 @@ const getCommandCallbackPropName = (analysis, expression, isReactUseRefCall) =>
|
|
|
44685
45726
|
if (!propertyName || !COMMAND_PROP_NAME_PATTERN.test(propertyName)) return null;
|
|
44686
45727
|
return refCurrentObjectPreservesCallbackProperty(analysis, currentExpression.object, propertyName, isReactUseRefCall) ? propertyName : null;
|
|
44687
45728
|
};
|
|
44688
|
-
const isWrapperHookCallbackRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
45729
|
+
const isWrapperHookCallbackRef = (analysis, ref, scopes) => Boolean(ref.resolved?.defs.some((def) => {
|
|
44689
45730
|
const node = def.node;
|
|
44690
45731
|
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
44691
|
-
|
|
45732
|
+
const resultSymbol = isNodeOfType(node.id, "Identifier") ? scopes.symbolFor(node.id) ?? null : null;
|
|
45733
|
+
return getWrapperHookWrappedFunction(unwrapChainExpression(node.init), resultSymbol, scopes) !== null;
|
|
44692
45734
|
}));
|
|
44693
45735
|
const isHandlerBagArgument = (analysis, argument) => {
|
|
44694
45736
|
if (!isNodeOfType(argument, "ObjectExpression")) return false;
|
|
@@ -44707,14 +45749,25 @@ const isHandlerBagArgument = (analysis, argument) => {
|
|
|
44707
45749
|
};
|
|
44708
45750
|
const getFunctionalUpdaterDataRefs = (analysis, updater) => getDownstreamRefs(analysis, updater).filter((updaterRef) => !updaterRef.resolved?.defs.some((def) => def.type === "Parameter" && def.node === updater));
|
|
44709
45751
|
const HOOK_NAME_PATTERN$1 = /^use[A-Z0-9]/;
|
|
44710
|
-
const EXTERNAL_SUBSCRIPTION_HOOK_NAMES = new Set([
|
|
45752
|
+
const EXTERNAL_SUBSCRIPTION_HOOK_NAMES$1 = new Set([
|
|
44711
45753
|
"useIntersectionObserver",
|
|
44712
45754
|
"useMatchMedia",
|
|
45755
|
+
"useMediaJobProgress",
|
|
44713
45756
|
"useMediaQuery",
|
|
44714
45757
|
"useResizeObserver",
|
|
44715
45758
|
"useVisibility",
|
|
44716
45759
|
"useWindowSize"
|
|
44717
45760
|
]);
|
|
45761
|
+
const isCallbackPropReference = (analysis, ref) => {
|
|
45762
|
+
if (!isProp(analysis, ref)) return false;
|
|
45763
|
+
const identifier = ref.identifier;
|
|
45764
|
+
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
45765
|
+
if (!isWholePropsObjectReference(analysis, ref)) return HANDLER_NAMED_PROP_PATTERN.test(identifier.name);
|
|
45766
|
+
const member = identifier.parent;
|
|
45767
|
+
if (!member || !isNodeOfType(member, "MemberExpression") || member.object !== identifier) return false;
|
|
45768
|
+
const propertyName = getStaticMemberPropertyName(member);
|
|
45769
|
+
return Boolean(propertyName && HANDLER_NAMED_PROP_PATTERN.test(propertyName));
|
|
45770
|
+
};
|
|
44718
45771
|
const isParentWiredHookResultRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
44719
45772
|
const node = def.node;
|
|
44720
45773
|
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
@@ -44722,7 +45775,7 @@ const isParentWiredHookResultRef = (analysis, ref) => Boolean(ref.resolved?.defs
|
|
|
44722
45775
|
if (!isNodeOfType(init, "CallExpression")) return false;
|
|
44723
45776
|
const callee = init.callee;
|
|
44724
45777
|
if (!isNodeOfType(callee, "Identifier") || !HOOK_NAME_PATTERN$1.test(callee.name)) return false;
|
|
44725
|
-
return (init.arguments ?? []).some((hookArgument) => getDownstreamRefs(analysis, hookArgument).some((downstreamRef) =>
|
|
45778
|
+
return (init.arguments ?? []).some((hookArgument) => getDownstreamRefs(analysis, hookArgument).some((downstreamRef) => isCallbackPropReference(analysis, downstreamRef)));
|
|
44726
45779
|
}));
|
|
44727
45780
|
const isParentWiredHookResultArgument = (analysis, argument) => {
|
|
44728
45781
|
if (!isNodeOfType(argument, "Identifier")) return false;
|
|
@@ -44735,19 +45788,19 @@ const isParentWiredHookCalleeRef = (analysis, ref) => {
|
|
|
44735
45788
|
if (!isNodeOfType(identifier, "Identifier") || !HOOK_NAME_PATTERN$1.test(identifier.name)) return false;
|
|
44736
45789
|
const parent = identifier.parent;
|
|
44737
45790
|
if (!parent || !isNodeOfType(parent, "CallExpression") || parent.callee !== identifier) return false;
|
|
44738
|
-
return (parent.arguments ?? []).some((hookArgument) => getDownstreamRefs(analysis, hookArgument).some((downstreamRef) =>
|
|
45791
|
+
return (parent.arguments ?? []).some((hookArgument) => getDownstreamRefs(analysis, hookArgument).some((downstreamRef) => isCallbackPropReference(analysis, downstreamRef)));
|
|
44739
45792
|
};
|
|
44740
45793
|
const isExternalSubscriptionHookRef = (ref) => {
|
|
44741
45794
|
const identifier = ref.identifier;
|
|
44742
45795
|
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
44743
|
-
if (EXTERNAL_SUBSCRIPTION_HOOK_NAMES.has(identifier.name) && isCalleePosition(identifier)) return true;
|
|
45796
|
+
if (EXTERNAL_SUBSCRIPTION_HOOK_NAMES$1.has(identifier.name) && isCalleePosition(identifier)) return true;
|
|
44744
45797
|
return Boolean(ref.resolved?.defs.some((def) => {
|
|
44745
45798
|
const node = def.node;
|
|
44746
45799
|
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
44747
45800
|
const initializer = stripParenExpression(node.init);
|
|
44748
45801
|
if (!isNodeOfType(initializer, "CallExpression")) return false;
|
|
44749
45802
|
const callee = stripParenExpression(initializer.callee);
|
|
44750
|
-
return isNodeOfType(callee, "Identifier") && EXTERNAL_SUBSCRIPTION_HOOK_NAMES.has(callee.name);
|
|
45803
|
+
return isNodeOfType(callee, "Identifier") && EXTERNAL_SUBSCRIPTION_HOOK_NAMES$1.has(callee.name);
|
|
44751
45804
|
}));
|
|
44752
45805
|
};
|
|
44753
45806
|
const isImportBindingRef = (ref) => Boolean(ref.resolved?.defs.some((def) => def.type === "ImportBinding"));
|
|
@@ -44783,16 +45836,22 @@ const noPassDataToParent = defineRule({
|
|
|
44783
45836
|
const callExpr = getCallExpr(ref);
|
|
44784
45837
|
if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) continue;
|
|
44785
45838
|
const callbackRefProvenance = getCallbackRefProvenance(analysis, node, callExpr, isReactUseRefCall, isReactUseEffectCall);
|
|
44786
|
-
if (isRefCall(analysis, ref) && !callbackRefProvenance) continue;
|
|
44787
45839
|
if (!isSynchronous(ref.identifier, effectFn)) continue;
|
|
44788
45840
|
const calleeNode = unwrapChainExpression(callExpr.callee);
|
|
44789
45841
|
const identifier = ref.identifier;
|
|
44790
|
-
|
|
44791
|
-
|
|
45842
|
+
const resolvedCallbackPropNames = isNodeOfType(calleeNode, "MemberExpression") && getStaticMemberPropertyName(calleeNode) === "current" ? null : getParentCallbackPropNames({
|
|
45843
|
+
analysis,
|
|
45844
|
+
expression: calleeNode,
|
|
45845
|
+
scopes: context.scopes
|
|
45846
|
+
});
|
|
45847
|
+
const callbackPropNames = callbackRefProvenance?.callbackPropNames ?? resolvedCallbackPropNames;
|
|
45848
|
+
if (isRefCall(analysis, ref) && !callbackPropNames) continue;
|
|
45849
|
+
if (callbackPropNames) {
|
|
45850
|
+
if ([...callbackPropNames].some((callbackPropName) => COMMAND_PROP_NAME_PATTERN.test(callbackPropName))) continue;
|
|
44792
45851
|
} else if (calleeNode === identifier) {
|
|
44793
45852
|
const callbackPropName = getCommandCallbackPropName(analysis, identifier, isReactUseRefCall);
|
|
44794
45853
|
if (callbackPropName && COMMAND_PROP_NAME_PATTERN.test(callbackPropName)) continue;
|
|
44795
|
-
if (!isDirectParentCallbackRef(analysis, ref)) continue;
|
|
45854
|
+
if (!isDirectParentCallbackRef(analysis, ref, context.scopes)) continue;
|
|
44796
45855
|
if (isNodeOfType(identifier, "Identifier") && COMMAND_PROP_NAME_PATTERN.test(identifier.name)) continue;
|
|
44797
45856
|
} else if (isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === identifier) {
|
|
44798
45857
|
if (!isWholePropsObjectReference(analysis, ref)) continue;
|
|
@@ -44800,10 +45859,10 @@ const noPassDataToParent = defineRule({
|
|
|
44800
45859
|
} else continue;
|
|
44801
45860
|
const methodName = getCallMethodName(calleeNode);
|
|
44802
45861
|
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
44803
|
-
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
|
|
45862
|
+
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead && !callbackPropNames) continue;
|
|
44804
45863
|
if (methodName && COMMAND_PROP_NAME_PATTERN.test(methodName)) continue;
|
|
44805
|
-
if (!
|
|
44806
|
-
const isSetterNamedCallee =
|
|
45864
|
+
if (!callbackPropNames && isNamespacedApiCallee(calleeNode)) continue;
|
|
45865
|
+
const isSetterNamedCallee = callbackPropNames ? [...callbackPropNames].every((callbackPropName) => SETTER_NAMED_PROP_PATTERN.test(callbackPropName)) : Boolean((isNodeOfType(identifier, "Identifier") ? identifier.name : methodName) && SETTER_NAMED_PROP_PATTERN.test((isNodeOfType(identifier, "Identifier") ? identifier.name : methodName) ?? ""));
|
|
44807
45866
|
const isLeafRef = (argRef) => getUpstreamRefs(analysis, argRef).length === 1;
|
|
44808
45867
|
const argsUpstreamRefs = (callExpr.arguments ?? []).flatMap((argument) => {
|
|
44809
45868
|
if (isFunctionLike$1(argument)) {
|
|
@@ -44818,7 +45877,7 @@ const noPassDataToParent = defineRule({
|
|
|
44818
45877
|
}
|
|
44819
45878
|
return getDownstreamRefs(analysis, argument);
|
|
44820
45879
|
}).flatMap((argumentRef) => isExternallyDrivenState(analysis, argumentRef) ? [] : getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
|
|
44821
|
-
if (calleeNode === identifier && isWrapperHookCallbackRef(analysis, ref)) argsUpstreamRefs.push(...getArgsUpstreamRefs(analysis, ref).filter(isLeafRef));
|
|
45880
|
+
if (calleeNode === identifier && isWrapperHookCallbackRef(analysis, ref, context.scopes)) argsUpstreamRefs.push(...getArgsUpstreamRefs(analysis, ref).filter(isLeafRef));
|
|
44822
45881
|
if (!argsUpstreamRefs.some((argRef) => {
|
|
44823
45882
|
if (isUseStateIdentifier(argRef.identifier)) return false;
|
|
44824
45883
|
if (isExternalSubscriptionHookRef(argRef)) return false;
|
|
@@ -44856,9 +45915,47 @@ const isCallResultConsumedAsArgument = (callExpression) => {
|
|
|
44856
45915
|
return false;
|
|
44857
45916
|
};
|
|
44858
45917
|
//#endregion
|
|
45918
|
+
//#region src/plugin/rules/state-and-effects/utils/is-custom-hook-state-result-reference.ts
|
|
45919
|
+
const NON_STATE_CUSTOM_HOOK_NAMES = new Set([
|
|
45920
|
+
"useCallbackRef",
|
|
45921
|
+
"useEffectEvent",
|
|
45922
|
+
"useEvent",
|
|
45923
|
+
"useEventCallback",
|
|
45924
|
+
"useLatest",
|
|
45925
|
+
"useMemoizedFn",
|
|
45926
|
+
"useStableCallback"
|
|
45927
|
+
]);
|
|
45928
|
+
const EXTERNAL_SUBSCRIPTION_HOOK_NAMES = new Set([
|
|
45929
|
+
"useIntersectionObserver",
|
|
45930
|
+
"useMatchMedia",
|
|
45931
|
+
"useMediaJobProgress",
|
|
45932
|
+
"useMediaQuery",
|
|
45933
|
+
"useResizeObserver",
|
|
45934
|
+
"useVisibility",
|
|
45935
|
+
"useWindowSize"
|
|
45936
|
+
]);
|
|
45937
|
+
const getHookCalleeName = (initializer) => {
|
|
45938
|
+
const unwrappedInitializer = stripParenExpression(initializer);
|
|
45939
|
+
if (!isNodeOfType(unwrappedInitializer, "CallExpression")) return null;
|
|
45940
|
+
const callee = stripParenExpression(unwrappedInitializer.callee);
|
|
45941
|
+
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
45942
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
45943
|
+
return null;
|
|
45944
|
+
};
|
|
45945
|
+
const isCustomHookStateResultReference = (analysis, reference) => Boolean(reference.resolved?.defs.some((definition) => {
|
|
45946
|
+
const declarator = definition.node;
|
|
45947
|
+
if (!isNodeOfType(declarator, "VariableDeclarator") || !declarator.init) return false;
|
|
45948
|
+
const calleeName = getHookCalleeName(declarator.init);
|
|
45949
|
+
if (!calleeName || !HOOK_NAME_PATTERN$3.test(calleeName) || BUILTIN_HOOK_NAMES.has(calleeName) || NON_STATE_CUSTOM_HOOK_NAMES.has(calleeName) || EXTERNAL_SUBSCRIPTION_HOOK_NAMES.has(calleeName)) return false;
|
|
45950
|
+
const initializer = stripParenExpression(declarator.init);
|
|
45951
|
+
if (!isNodeOfType(initializer, "CallExpression")) return false;
|
|
45952
|
+
return initializer.arguments.some((argument) => getDownstreamRefs(analysis, argument).some((argumentReference) => isProp(analysis, argumentReference)));
|
|
45953
|
+
}));
|
|
45954
|
+
//#endregion
|
|
44859
45955
|
//#region src/plugin/rules/state-and-effects/no-pass-live-state-to-parent.ts
|
|
44860
45956
|
const SETTER_NAMED_CALLBACK_PATTERN = /^set[A-Z]/;
|
|
44861
45957
|
const DATA_FETCHING_CALLBACK_PATTERN = /^(fetch|refetch|load|query|request)([A-Z_]|$)/;
|
|
45958
|
+
const hasMutableBindingWrite = (reference) => Boolean(reference.resolved?.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init));
|
|
44862
45959
|
const getCallCalleeName = (callExpr) => {
|
|
44863
45960
|
if (!isNodeOfType(callExpr, "CallExpression")) return null;
|
|
44864
45961
|
const callee = callExpr.callee;
|
|
@@ -44903,6 +46000,10 @@ const collectUpstreamStateRefs = (analysis, ref, stateRefs, visited) => {
|
|
|
44903
46000
|
stateRefs.push(ref);
|
|
44904
46001
|
return;
|
|
44905
46002
|
}
|
|
46003
|
+
if (isCustomHookStateResultReference(analysis, ref)) {
|
|
46004
|
+
stateRefs.push(ref);
|
|
46005
|
+
return;
|
|
46006
|
+
}
|
|
44906
46007
|
for (const def of ref.resolved?.defs ?? []) {
|
|
44907
46008
|
if (def.type === "ImportBinding" || def.type === "Parameter") continue;
|
|
44908
46009
|
const defNode = def.node;
|
|
@@ -44932,6 +46033,32 @@ const collectPropCallbackBoundStateRefs = (analysis, ref, isPropCallbackRef) =>
|
|
|
44932
46033
|
}
|
|
44933
46034
|
return stateRefs;
|
|
44934
46035
|
};
|
|
46036
|
+
const collectDirectCallStateRefs = (analysis, callExpression) => {
|
|
46037
|
+
const stateReferences = [];
|
|
46038
|
+
for (const argument of callExpression.arguments) {
|
|
46039
|
+
if (isFunctionLike$1(argument)) continue;
|
|
46040
|
+
for (const argumentReference of getDownstreamRefs(analysis, argument)) {
|
|
46041
|
+
if (resolveToFunction(argumentReference)) continue;
|
|
46042
|
+
collectUpstreamStateRefs(analysis, argumentReference, stateReferences, /* @__PURE__ */ new Set());
|
|
46043
|
+
}
|
|
46044
|
+
}
|
|
46045
|
+
return stateReferences;
|
|
46046
|
+
};
|
|
46047
|
+
const getTransparentWrapperPropReference = (analysis, reference, context) => {
|
|
46048
|
+
for (const definition of reference.resolved?.defs ?? []) {
|
|
46049
|
+
const declarator = definition.node;
|
|
46050
|
+
if (!isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.id, "Identifier") || !declarator.init) continue;
|
|
46051
|
+
const resultSymbol = context.scopes.symbolFor(declarator.id);
|
|
46052
|
+
const callbackArgument = getTransparentReactCallbackWrapperArgument(declarator.init, resultSymbol, context.scopes);
|
|
46053
|
+
if (!callbackArgument) continue;
|
|
46054
|
+
const callbackReferences = getDownstreamRefs(analysis, callbackArgument);
|
|
46055
|
+
const callbackReference = callbackReferences.find((candidateReference) => isPropCallbackInvocationRef(analysis, candidateReference));
|
|
46056
|
+
if (callbackReference) return callbackReference;
|
|
46057
|
+
const propReference = callbackReferences.find((candidateReference) => isProp(analysis, candidateReference) && !candidateReference.resolved?.references.some((candidateUsage) => candidateUsage.isWrite() && !candidateUsage.init));
|
|
46058
|
+
if (propReference) return propReference;
|
|
46059
|
+
}
|
|
46060
|
+
return null;
|
|
46061
|
+
};
|
|
44935
46062
|
const isSetterNamedCallbackReceivingData = (callbackRef) => {
|
|
44936
46063
|
const callExpr = getCallExpr(callbackRef);
|
|
44937
46064
|
if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) return false;
|
|
@@ -44967,6 +46094,16 @@ const resolvesToLocalHookReturnBinding = (ref) => Boolean(ref?.resolved?.defs?.s
|
|
|
44967
46094
|
const calleeName = getInitializerCalleeName(node.init);
|
|
44968
46095
|
return calleeName !== null && isReactHookName(calleeName) && !FUNCTION_WRAPPER_HOOK_NAMES.has(calleeName);
|
|
44969
46096
|
}));
|
|
46097
|
+
const getDirectLocalEffectHelper = (callExpression, effectFunction, context) => {
|
|
46098
|
+
const helperFunction = resolveExactLocalFunction(callExpression.callee, context.scopes);
|
|
46099
|
+
if (!helperFunction) return null;
|
|
46100
|
+
let ancestor = callExpression.parent;
|
|
46101
|
+
while (ancestor && ancestor !== effectFunction) {
|
|
46102
|
+
if (isFunctionLike$1(ancestor)) return null;
|
|
46103
|
+
ancestor = ancestor.parent;
|
|
46104
|
+
}
|
|
46105
|
+
return ancestor === effectFunction ? helperFunction : null;
|
|
46106
|
+
};
|
|
44970
46107
|
const noPassLiveStateToParent = defineRule({
|
|
44971
46108
|
id: "no-pass-live-state-to-parent",
|
|
44972
46109
|
title: "Live state pushed to parent via effect",
|
|
@@ -44981,20 +46118,32 @@ const noPassLiveStateToParent = defineRule({
|
|
|
44981
46118
|
if (!effectFnRefs) return;
|
|
44982
46119
|
const effectFn = getEffectFn(analysis, node);
|
|
44983
46120
|
if (!effectFn) return;
|
|
46121
|
+
const effectFunctionBody = isNodeOfType(effectFn, "ArrowFunctionExpression") || isNodeOfType(effectFn, "FunctionExpression") || isNodeOfType(effectFn, "FunctionDeclaration") ? effectFn.body : null;
|
|
44984
46122
|
for (const ref of effectFnRefs) {
|
|
44985
|
-
const propCallbackRefs = getEventualCallRefsTo(analysis, ref, (innerRef) => isParentNotificationCallbackRef(analysis, innerRef));
|
|
44986
|
-
if (propCallbackRefs.length === 0) continue;
|
|
44987
|
-
if (resolvesToLocalHookReturnBinding(ref)) continue;
|
|
44988
|
-
if (!isSynchronous(ref.identifier, effectFn)) continue;
|
|
44989
46123
|
const callExpr = getCallExpr(ref);
|
|
44990
|
-
if (!callExpr) continue;
|
|
46124
|
+
if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) continue;
|
|
46125
|
+
const directLocalEffectHelper = getDirectLocalEffectHelper(callExpr, effectFn, context);
|
|
46126
|
+
const callGraphReferences = directLocalEffectHelper ? [ref, ...getDownstreamRefs(analysis, directLocalEffectHelper)] : [ref];
|
|
46127
|
+
const resolvedCallbackPropNames = getParentCallbackPropNames({
|
|
46128
|
+
analysis,
|
|
46129
|
+
expression: callExpr.callee,
|
|
46130
|
+
scopes: context.scopes
|
|
46131
|
+
});
|
|
46132
|
+
const callExpressionRoot = findTransparentExpressionRoot(callExpr);
|
|
46133
|
+
const notificationCallbackPropNames = Boolean(resolvedCallbackPropNames && callExpr.arguments.length > 0 && (!isCallResultCapturedToLocal(callExpr) || isNodeOfType(callExpressionRoot.parent, "ReturnStatement") && callExpressionRoot.parent.parent === effectFunctionBody) && [...resolvedCallbackPropNames].every((callbackPropName) => !DATA_FETCHING_CALLBACK_PATTERN.test(callbackPropName))) ? resolvedCallbackPropNames : null;
|
|
46134
|
+
if (!notificationCallbackPropNames && hasMutableBindingWrite(ref)) continue;
|
|
46135
|
+
const propCallbackRefs = callGraphReferences.flatMap((callGraphReference) => getEventualCallRefsTo(analysis, callGraphReference, (innerRef) => isParentNotificationCallbackRef(analysis, innerRef)));
|
|
46136
|
+
const transparentPropReference = propCallbackRefs.length === 0 ? getTransparentWrapperPropReference(analysis, ref, context) : null;
|
|
46137
|
+
if (propCallbackRefs.length === 0 && !transparentPropReference && !notificationCallbackPropNames) continue;
|
|
46138
|
+
if (!notificationCallbackPropNames && resolvesToLocalHookReturnBinding(ref)) continue;
|
|
46139
|
+
if (!isSynchronous(ref.identifier, effectFn) && !directLocalEffectHelper) continue;
|
|
44991
46140
|
if (isCallResultConsumedAsArgument(callExpr)) continue;
|
|
44992
46141
|
const calleeNode = callExpr.callee;
|
|
44993
46142
|
const methodName = calleeNode ? getCallMethodName(calleeNode) : null;
|
|
44994
46143
|
const isPropCallbackNamedLikeStringRead = Boolean(methodName && STRING_READ_METHOD_NAMES.has(methodName) && calleeNode && isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === ref.identifier && isWholePropsObjectReference(analysis, ref));
|
|
44995
|
-
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead) continue;
|
|
44996
|
-
if (calleeNode && isNamespacedApiCallee(calleeNode)) continue;
|
|
44997
|
-
const stateArgRefs = collectPropCallbackBoundStateRefs(analysis,
|
|
46144
|
+
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead && !notificationCallbackPropNames) continue;
|
|
46145
|
+
if (!notificationCallbackPropNames && calleeNode && isNamespacedApiCallee(calleeNode)) continue;
|
|
46146
|
+
const stateArgRefs = transparentPropReference || notificationCallbackPropNames ? collectDirectCallStateRefs(analysis, callExpr) : callGraphReferences.flatMap((callGraphReference) => collectPropCallbackBoundStateRefs(analysis, callGraphReference, (innerRef) => isParentNotificationCallbackRef(analysis, innerRef)));
|
|
44998
46147
|
const handsSetterNamedCallbackData = propCallbackRefs.some(isSetterNamedCallbackReceivingData);
|
|
44999
46148
|
if (stateArgRefs.length === 0 && !handsSetterNamedCallbackData) continue;
|
|
45000
46149
|
context.report({
|
|
@@ -45387,6 +46536,7 @@ const isStateLikeDependency = (analysis, element, isPropName) => {
|
|
|
45387
46536
|
if (!analysis) return true;
|
|
45388
46537
|
const reference = getRef(analysis, element);
|
|
45389
46538
|
if (!reference) return true;
|
|
46539
|
+
if (isCustomHookStateResultReference(analysis, reference)) return true;
|
|
45390
46540
|
const upstreamReferences = getUpstreamRefs(analysis, reference);
|
|
45391
46541
|
if (upstreamReferences.some((upstreamReference) => isState(analysis, upstreamReference))) return true;
|
|
45392
46542
|
return !upstreamReferences.some((upstreamReference) => isProp(analysis, upstreamReference));
|
|
@@ -45403,6 +46553,22 @@ const getRefHeldPropCallbackName = (callExpression, isPropName) => {
|
|
|
45403
46553
|
if (!callbackArgument || !isNodeOfType(callbackArgument, "Identifier")) return null;
|
|
45404
46554
|
return isPropName(callbackArgument.name) ? callbackArgument.name : null;
|
|
45405
46555
|
};
|
|
46556
|
+
const getTransparentWrappedPropCallbackName = (callExpression, context, isPropName) => {
|
|
46557
|
+
const callee = stripParenExpression(callExpression.callee);
|
|
46558
|
+
if (!isNodeOfType(callee, "Identifier")) return null;
|
|
46559
|
+
const binding = findVariableInitializer(callExpression, callee.name);
|
|
46560
|
+
if (!binding?.initializer) return null;
|
|
46561
|
+
const resultSymbol = context.scopes.symbolFor(callee);
|
|
46562
|
+
const callbackArgument = getTransparentReactCallbackWrapperArgument(binding.initializer, resultSymbol, context.scopes);
|
|
46563
|
+
if (!callbackArgument) return null;
|
|
46564
|
+
const callbackSource = stripParenExpression(callbackArgument);
|
|
46565
|
+
if (isNodeOfType(callbackSource, "Identifier")) return isPropName(callbackSource.name, callbackSource) ? callbackSource.name : null;
|
|
46566
|
+
if (!isNodeOfType(callbackSource, "MemberExpression")) return null;
|
|
46567
|
+
const receiver = stripParenExpression(callbackSource.object);
|
|
46568
|
+
const propertyName = getStaticPropertyName(callbackSource);
|
|
46569
|
+
if (!isNodeOfType(receiver, "Identifier") || !propertyName) return null;
|
|
46570
|
+
return isPropName(receiver.name, receiver) ? propertyName : null;
|
|
46571
|
+
};
|
|
45406
46572
|
const noPropCallbackInEffect = defineRule({
|
|
45407
46573
|
id: "no-prop-callback-in-effect",
|
|
45408
46574
|
title: "Parent kept in sync with a callback effect",
|
|
@@ -45436,9 +46602,16 @@ const noPropCallbackInEffect = defineRule({
|
|
|
45436
46602
|
walkInsideStatementBlocks(callback.body, (child) => {
|
|
45437
46603
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
45438
46604
|
const directCallee = stripParenExpression(child.callee);
|
|
45439
|
-
const
|
|
46605
|
+
const resolvedCallbackPropNames = analysis && propStackTracker.getCurrentPropNames().size > 0 ? getParentCallbackPropNames({
|
|
46606
|
+
analysis,
|
|
46607
|
+
expression: directCallee,
|
|
46608
|
+
scopes: context.scopes
|
|
46609
|
+
}) : null;
|
|
46610
|
+
const calleeName = resolvedCallbackPropNames && [...resolvedCallbackPropNames][0] || isNodeOfType(directCallee, "Identifier") && propStackTracker.isPropName(directCallee.name) && directCallee.name || getRefHeldPropCallbackName(child, propStackTracker.isPropName) || getTransparentWrappedPropCallbackName(child, context, propStackTracker.isPropName);
|
|
45440
46611
|
if (!calleeName) return;
|
|
45441
|
-
|
|
46612
|
+
const callExpressionRoot = findTransparentExpressionRoot(child);
|
|
46613
|
+
const isDirectEffectReturn = isNodeOfType(callExpressionRoot.parent, "ReturnStatement") && callExpressionRoot.parent.parent === callback.body;
|
|
46614
|
+
if (!isResultDiscardedCall(child) && !isDirectEffectReturn) return;
|
|
45442
46615
|
if (reportedNodes.has(child)) return;
|
|
45443
46616
|
reportedNodes.add(child);
|
|
45444
46617
|
context.report({
|
|
@@ -46271,6 +47444,69 @@ const noRedundantShouldComponentUpdate = defineRule({
|
|
|
46271
47444
|
}
|
|
46272
47445
|
});
|
|
46273
47446
|
//#endregion
|
|
47447
|
+
//#region src/plugin/rules/correctness/no-ref-callback-cleanup-before-react-19.ts
|
|
47448
|
+
const resolveFunctionExpressions = (rawExpression, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
47449
|
+
const expression = stripParenExpression(rawExpression);
|
|
47450
|
+
if (isFunctionLike$1(expression)) return expression.async || expression.generator ? [] : [expression];
|
|
47451
|
+
if (isNodeOfType(expression, "ConditionalExpression")) {
|
|
47452
|
+
if (isNodeOfType(expression.test, "Literal")) return resolveFunctionExpressions(expression.test.value ? expression.consequent : expression.alternate, scopes, visitedSymbolIds);
|
|
47453
|
+
return [...resolveFunctionExpressions(expression.consequent, scopes, visitedSymbolIds), ...resolveFunctionExpressions(expression.alternate, scopes, visitedSymbolIds)];
|
|
47454
|
+
}
|
|
47455
|
+
if (isNodeOfType(expression, "LogicalExpression")) {
|
|
47456
|
+
if (isNodeOfType(expression.left, "Literal")) {
|
|
47457
|
+
const isLeftTruthy = Boolean(expression.left.value);
|
|
47458
|
+
if (expression.operator === "&&" && !isLeftTruthy) return [];
|
|
47459
|
+
if (expression.operator === "||" && isLeftTruthy) return [];
|
|
47460
|
+
if (expression.operator === "??" && expression.left.value !== null) return [];
|
|
47461
|
+
}
|
|
47462
|
+
if (expression.operator === "&&") return resolveFunctionExpressions(expression.right, scopes, visitedSymbolIds);
|
|
47463
|
+
return [...resolveFunctionExpressions(expression.left, scopes, visitedSymbolIds), ...resolveFunctionExpressions(expression.right, scopes, visitedSymbolIds)];
|
|
47464
|
+
}
|
|
47465
|
+
if (isNodeOfType(expression, "SequenceExpression")) {
|
|
47466
|
+
const finalExpression = expression.expressions.at(-1);
|
|
47467
|
+
return finalExpression ? resolveFunctionExpressions(finalExpression, scopes, visitedSymbolIds) : [];
|
|
47468
|
+
}
|
|
47469
|
+
if (isNodeOfType(expression, "CallExpression")) {
|
|
47470
|
+
if (!isReactApiCall(expression, "useCallback", scopes)) return [];
|
|
47471
|
+
const callback = expression.arguments[0];
|
|
47472
|
+
return callback && !isNodeOfType(callback, "SpreadElement") ? resolveFunctionExpressions(callback, scopes, visitedSymbolIds) : [];
|
|
47473
|
+
}
|
|
47474
|
+
if (!isNodeOfType(expression, "Identifier")) return [];
|
|
47475
|
+
const symbol = scopes.symbolFor(expression);
|
|
47476
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return [];
|
|
47477
|
+
if (symbol.kind === "function" && isNodeOfType(symbol.declarationNode, "FunctionDeclaration") && symbol.references.every((reference) => reference.flag === "read")) return resolveFunctionExpressions(symbol.declarationNode, scopes, new Set([...visitedSymbolIds, symbol.id]));
|
|
47478
|
+
const initializer = getDirectConstInitializer(symbol);
|
|
47479
|
+
if (!initializer) return [];
|
|
47480
|
+
return resolveFunctionExpressions(initializer, scopes, new Set([...visitedSymbolIds, symbol.id]));
|
|
47481
|
+
};
|
|
47482
|
+
const functionReturnsCleanupFunction = (functionExpression, scopes) => {
|
|
47483
|
+
if (!isFunctionLike$1(functionExpression)) return false;
|
|
47484
|
+
if (!isNodeOfType(functionExpression.body, "BlockStatement")) return resolveFunctionExpressions(functionExpression.body, scopes).length > 0;
|
|
47485
|
+
return collectFunctionReturnStatements(functionExpression).some((returnStatement) => Boolean(returnStatement.argument && resolveFunctionExpressions(returnStatement.argument, scopes).length > 0));
|
|
47486
|
+
};
|
|
47487
|
+
const callbackReturnsCleanupFunction = (callback, scopes) => {
|
|
47488
|
+
return resolveFunctionExpressions(callback, scopes).some((functionExpression) => functionReturnsCleanupFunction(functionExpression, scopes));
|
|
47489
|
+
};
|
|
47490
|
+
const noRefCallbackCleanupBeforeReact19 = defineRule({
|
|
47491
|
+
id: "no-ref-callback-cleanup-before-react-19",
|
|
47492
|
+
title: "Ref cleanup requires React 19",
|
|
47493
|
+
requires: ["react:18"],
|
|
47494
|
+
disabledWhen: ["react:19"],
|
|
47495
|
+
severity: "warn",
|
|
47496
|
+
recommendation: "React 18 ignores functions returned from ref callbacks. Handle cleanup when React calls the ref with `null`, or require React 19 before returning a cleanup function.",
|
|
47497
|
+
create: (context) => ({ JSXAttribute(node) {
|
|
47498
|
+
if (getJsxAttributeName(node.name) !== "ref") return;
|
|
47499
|
+
if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
|
|
47500
|
+
const callback = node.value.expression;
|
|
47501
|
+
if (!callback || isNodeOfType(callback, "JSXEmptyExpression")) return;
|
|
47502
|
+
if (!callbackReturnsCleanupFunction(callback, context.scopes)) return;
|
|
47503
|
+
context.report({
|
|
47504
|
+
node,
|
|
47505
|
+
message: "This ref callback returns a cleanup function, but React 18 ignores ref cleanup returns, so the cleanup never runs. Handle detachment when React calls the ref with `null`, or require React 19."
|
|
47506
|
+
});
|
|
47507
|
+
} })
|
|
47508
|
+
});
|
|
47509
|
+
//#endregion
|
|
46274
47510
|
//#region src/plugin/rules/state-and-effects/no-ref-current-in-render.ts
|
|
46275
47511
|
const REPEATED_ANCESTOR_TYPES = new Set([
|
|
46276
47512
|
"DoWhileStatement",
|
|
@@ -46866,7 +48102,7 @@ const doConditionsImplyFormula = (conditions, target) => {
|
|
|
46866
48102
|
}
|
|
46867
48103
|
return facts.didConflict || evaluateBooleanFormula$1(target, facts.assignments) === true;
|
|
46868
48104
|
};
|
|
46869
|
-
const getFunctionBindingSymbol = (functionNode, scopes) => {
|
|
48105
|
+
const getFunctionBindingSymbol$1 = (functionNode, scopes) => {
|
|
46870
48106
|
if (isNodeOfType(functionNode, "FunctionDeclaration") && functionNode.id) return scopes.symbolFor(functionNode.id);
|
|
46871
48107
|
const parent = functionNode.parent;
|
|
46872
48108
|
if ((isNodeOfType(functionNode, "ArrowFunctionExpression") || isNodeOfType(functionNode, "FunctionExpression")) && isNodeOfType(parent, "VariableDeclarator") && parent.init === functionNode && isNodeOfType(parent.id, "Identifier")) return scopes.symbolFor(parent.id);
|
|
@@ -46899,7 +48135,7 @@ const isNodeEvaluatedDuringRender = (node, componentNode, scopes, visitedFunctio
|
|
|
46899
48135
|
const synchronousCallbackCall = getSynchronousCallbackCall(functionNode);
|
|
46900
48136
|
if (synchronousCallbackCall) return isNodeEvaluatedDuringRender(synchronousCallbackCall, componentNode, scopes, visitedFunctionSymbolIds);
|
|
46901
48137
|
if (executesDuringRender(functionNode, scopes)) return isNodeEvaluatedDuringRender(functionNode.parent ?? functionNode, componentNode, scopes, visitedFunctionSymbolIds);
|
|
46902
|
-
const functionSymbol = getFunctionBindingSymbol(functionNode, scopes);
|
|
48138
|
+
const functionSymbol = getFunctionBindingSymbol$1(functionNode, scopes);
|
|
46903
48139
|
if (!functionSymbol || visitedFunctionSymbolIds.has(functionSymbol.id)) return false;
|
|
46904
48140
|
visitedFunctionSymbolIds.add(functionSymbol.id);
|
|
46905
48141
|
let callCount = 0;
|
|
@@ -46948,7 +48184,7 @@ const collectExposureConditions = (analysis, context, node, componentNode, prote
|
|
|
46948
48184
|
parent = synchronousCallbackCall.parent;
|
|
46949
48185
|
continue;
|
|
46950
48186
|
}
|
|
46951
|
-
const functionSymbol = getFunctionBindingSymbol(parent, context.scopes);
|
|
48187
|
+
const functionSymbol = getFunctionBindingSymbol$1(parent, context.scopes);
|
|
46952
48188
|
if (functionSymbol?.references.length === 1) {
|
|
46953
48189
|
const callExpression = isReferenceDirectlyCalled(functionSymbol.references[0].identifier);
|
|
46954
48190
|
if (callExpression) {
|
|
@@ -47158,7 +48394,7 @@ const getSetterExposureConditions = (analysis, context, setterReference, compone
|
|
|
47158
48394
|
const functionNode = findEnclosingFunction$1(setterReference.identifier);
|
|
47159
48395
|
if (!functionNode) return null;
|
|
47160
48396
|
if (isInlineJsxCallback(functionNode)) return [collectExposureConditions(analysis, context, functionNode, componentNode, protectedSymbolIds)];
|
|
47161
|
-
const functionSymbol = getFunctionBindingSymbol(functionNode, context.scopes);
|
|
48397
|
+
const functionSymbol = getFunctionBindingSymbol$1(functionNode, context.scopes);
|
|
47162
48398
|
if (!functionSymbol || functionSymbol.references.length === 0) return null;
|
|
47163
48399
|
const conditionsByReference = [];
|
|
47164
48400
|
for (const reference of functionSymbol.references) {
|
|
@@ -47546,7 +48782,7 @@ const isSelfReferentialSentinelValue = (variableName, literalValue) => literalVa
|
|
|
47546
48782
|
const isIdentifierLikeKeyNameValue = (literalValue) => {
|
|
47547
48783
|
const wordSegments = literalValue.replace(/^[_$\s]+|[_$\s]+$/g, "").split(/[_\-:./$]+/).filter((segment) => segment.length > 0);
|
|
47548
48784
|
if (wordSegments.length < 2) return false;
|
|
47549
|
-
return wordSegments.every((segment) => /^[a-z]
|
|
48785
|
+
return wordSegments.every((segment) => /^[a-z]+(?:[A-Z][a-z]+)*$/.test(segment));
|
|
47550
48786
|
};
|
|
47551
48787
|
const FRAMEWORK_ENV_ADVICE = [
|
|
47552
48788
|
[
|
|
@@ -47620,7 +48856,7 @@ const noSecretsInClientCode = defineRule({
|
|
|
47620
48856
|
const isServerOnlyScope = isInsideServerOnlyScope(node);
|
|
47621
48857
|
const trailingSuffix = getIdentifierTrailingWord(variableName);
|
|
47622
48858
|
const isUiConstant = SECRET_FALSE_POSITIVE_SUFFIXES.has(trailingSuffix);
|
|
47623
|
-
if (shouldUseVariableNameHeuristic && !isServerOnlyScope && SECRET_VARIABLE_PATTERN.test(variableName) && !isUiConstant && !isPublicUrlValue(literalValue) && !isPlaceholderValueForVariableHeuristic && !isSelfReferentialSentinelValue(variableName, literalValue) && !isIdentifierLikeKeyNameValue(literalValue) &&
|
|
48859
|
+
if (shouldUseVariableNameHeuristic && !isServerOnlyScope && SECRET_VARIABLE_PATTERN.test(variableName) && !isUiConstant && !isPublicUrlValue(literalValue) && !isPlaceholderValueForVariableHeuristic && !isSelfReferentialSentinelValue(variableName, literalValue) && !isIdentifierLikeKeyNameValue(literalValue) && literalValue.length > 24) {
|
|
47624
48860
|
context.report({
|
|
47625
48861
|
node,
|
|
47626
48862
|
message: `Hardcoding "${variableName}" in client code is a security vulnerability: the secret ships to the browser where anyone can read it.`
|
|
@@ -53031,12 +54267,6 @@ const isInsideEs6Component$1 = (methodDefinition) => {
|
|
|
53031
54267
|
if (!owningClass) return false;
|
|
53032
54268
|
return isPreactOrReactComponentClass(owningClass);
|
|
53033
54269
|
};
|
|
53034
|
-
const stripThisParameter = (params) => {
|
|
53035
|
-
const first = params[0];
|
|
53036
|
-
if (!first) return params;
|
|
53037
|
-
if (isNodeOfType(first, "Identifier") && first.name === "this") return params.slice(1);
|
|
53038
|
-
return params;
|
|
53039
|
-
};
|
|
53040
54270
|
const preactNoRenderArguments = defineRule({
|
|
53041
54271
|
id: "preact-no-render-arguments",
|
|
53042
54272
|
title: "render() reads props from arguments",
|
|
@@ -56238,8 +57468,39 @@ const isUseStateSetterInScope = (node, setterName) => isHookBindingInScope(node,
|
|
|
56238
57468
|
destructureIndex: 1
|
|
56239
57469
|
});
|
|
56240
57470
|
//#endregion
|
|
57471
|
+
//#region src/plugin/utils/unwrap-return-expression.ts
|
|
57472
|
+
const unwrapReturnExpression = (node) => isNodeOfType(node, "ReturnStatement") && node.argument ? node.argument : node;
|
|
57473
|
+
//#endregion
|
|
56241
57474
|
//#region src/plugin/rules/performance/rendering-hydration-no-flicker.ts
|
|
56242
57475
|
const USE_EFFECT_ONLY = new Set(["useEffect"]);
|
|
57476
|
+
const USE_CALLBACK_ONLY = new Set(["useCallback"]);
|
|
57477
|
+
const USE_STATE_ONLY = new Set(["useState"]);
|
|
57478
|
+
const REACT_API_CALL_OPTIONS = {
|
|
57479
|
+
allowGlobalReactNamespace: true,
|
|
57480
|
+
allowUnboundBareCalls: true,
|
|
57481
|
+
resolveNamedAliases: true
|
|
57482
|
+
};
|
|
57483
|
+
const expressionReadsDerivedSymbol = (context, expression, stateDerivedSymbolIds) => {
|
|
57484
|
+
let readsDerivedSymbol = false;
|
|
57485
|
+
walkAst(expression, (node) => {
|
|
57486
|
+
if (readsDerivedSymbol) return false;
|
|
57487
|
+
if (node !== expression && isFunctionLike$1(node)) return false;
|
|
57488
|
+
if (isNodeOfType(node, "Identifier") && stateDerivedSymbolIds.has(context.scopes.symbolFor(node)?.id ?? -1)) readsDerivedSymbol = true;
|
|
57489
|
+
});
|
|
57490
|
+
return readsDerivedSymbol;
|
|
57491
|
+
};
|
|
57492
|
+
const getStaticObjectPropertyName = (property) => {
|
|
57493
|
+
if (!isNodeOfType(property, "Property") || property.computed || property.method || property.kind !== "init") return null;
|
|
57494
|
+
if (isNodeOfType(property.key, "Identifier")) return property.key.name;
|
|
57495
|
+
if (isNodeOfType(property.key, "Literal") && (typeof property.key.value === "string" || typeof property.key.value === "number")) return String(property.key.value);
|
|
57496
|
+
return null;
|
|
57497
|
+
};
|
|
57498
|
+
const isNonVisibleJsxSpreadProperty = (propertyName) => propertyName === "id" || propertyName.startsWith("aria-") || /^on[A-Z]/.test(propertyName);
|
|
57499
|
+
const isTransparentAssignmentTarget = (identifier) => {
|
|
57500
|
+
const expressionRoot = findTransparentExpressionRoot(identifier);
|
|
57501
|
+
const parent = expressionRoot.parent;
|
|
57502
|
+
return Boolean(isNodeOfType(parent, "AssignmentExpression") && parent.left === expressionRoot || isNodeOfType(parent, "UpdateExpression") && parent.argument === expressionRoot || isNodeOfType(parent, "UnaryExpression") && parent.operator === "delete" && parent.argument === expressionRoot);
|
|
57503
|
+
};
|
|
56243
57504
|
const argumentsReadRefCurrent = (callArguments) => callArguments.some((argument) => {
|
|
56244
57505
|
let readsCurrent = false;
|
|
56245
57506
|
walkAst(argument, (child) => {
|
|
@@ -56291,6 +57552,166 @@ const isStateUsedOnlyInIdOrAriaAttributes = (setterCall, setterName) => {
|
|
|
56291
57552
|
});
|
|
56292
57553
|
return referenceCount > 0 && !nonAriaReferenceFound;
|
|
56293
57554
|
};
|
|
57555
|
+
const isGlobalWindowMember = (context, node, propertyName) => {
|
|
57556
|
+
const member = stripParenExpression(node);
|
|
57557
|
+
if (!isNodeOfType(member, "MemberExpression") || member.computed) return false;
|
|
57558
|
+
const receiver = stripParenExpression(member.object);
|
|
57559
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === "window" && context.scopes.isGlobalReference(receiver) && isNodeOfType(member.property, "Identifier") && member.property.name === propertyName;
|
|
57560
|
+
};
|
|
57561
|
+
const getDirectWindowWidthSetter = (context, statement) => {
|
|
57562
|
+
const call = unwrapDiscardedExpression(statement);
|
|
57563
|
+
if (!isNodeOfType(call, "CallExpression") || call.arguments?.length !== 1) return null;
|
|
57564
|
+
if (!isNodeOfType(call.callee, "Identifier") || !isSetterCall(call)) return null;
|
|
57565
|
+
const argument = call.arguments[0];
|
|
57566
|
+
return isGlobalWindowMember(context, argument, "innerWidth") ? call : null;
|
|
57567
|
+
};
|
|
57568
|
+
const getResizeListenerHandler = (context, statement, methodName) => {
|
|
57569
|
+
const call = unwrapDiscardedExpression(statement);
|
|
57570
|
+
if (!isNodeOfType(call, "CallExpression") || call.arguments?.length !== 2) return null;
|
|
57571
|
+
if (!isGlobalWindowMember(context, call.callee, methodName)) return null;
|
|
57572
|
+
const eventName = call.arguments[0];
|
|
57573
|
+
const handler = call.arguments[1];
|
|
57574
|
+
if (!isNodeOfType(eventName, "Literal") || eventName.value !== "resize") return null;
|
|
57575
|
+
return isNodeOfType(handler, "Identifier") ? handler : null;
|
|
57576
|
+
};
|
|
57577
|
+
const getCleanupResizeHandler = (context, statement) => {
|
|
57578
|
+
if (!isNodeOfType(statement, "ReturnStatement") || !isFunctionLike$1(statement.argument)) return null;
|
|
57579
|
+
const cleanupStatements = getCallbackStatements(statement.argument);
|
|
57580
|
+
if (cleanupStatements.length !== 1) return null;
|
|
57581
|
+
return getResizeListenerHandler(context, unwrapReturnExpression(cleanupStatements[0]), "removeEventListener");
|
|
57582
|
+
};
|
|
57583
|
+
const findExactViewportState = (context, componentFunction, setterCall) => {
|
|
57584
|
+
if (!isFunctionLike$1(componentFunction) || !isNodeOfType(componentFunction.body, "BlockStatement")) return null;
|
|
57585
|
+
const componentBody = componentFunction.body;
|
|
57586
|
+
if (!isNodeOfType(setterCall.callee, "Identifier")) return null;
|
|
57587
|
+
const setterSymbol = context.scopes.symbolFor(setterCall.callee);
|
|
57588
|
+
if (!setterSymbol || setterSymbol.kind !== "const" || !isNodeOfType(setterSymbol.declarationNode, "VariableDeclarator")) return null;
|
|
57589
|
+
const declarator = setterSymbol.declarationNode;
|
|
57590
|
+
if (!isNodeOfType(declarator.id, "ArrayPattern")) return null;
|
|
57591
|
+
const stateIdentifier = declarator.id.elements?.[0];
|
|
57592
|
+
const setterIdentifier = declarator.id.elements?.[1];
|
|
57593
|
+
if (!isNodeOfType(stateIdentifier, "Identifier") || !isNodeOfType(setterIdentifier, "Identifier") || setterIdentifier !== setterSymbol.bindingIdentifier || !isNodeOfType(declarator.init, "CallExpression") || !isReactApiCall(declarator.init, USE_STATE_ONLY, context.scopes, REACT_API_CALL_OPTIONS)) return null;
|
|
57594
|
+
const initializer = declarator.init.arguments?.[0];
|
|
57595
|
+
if (!isNodeOfType(initializer, "Literal") || initializer.value !== 0) return null;
|
|
57596
|
+
const stateSymbol = context.scopes.symbolFor(stateIdentifier);
|
|
57597
|
+
if (!stateSymbol) return null;
|
|
57598
|
+
const stateDerivedSymbolIds = new Set([stateSymbol.id]);
|
|
57599
|
+
let didAddDerivedSymbol = true;
|
|
57600
|
+
while (didAddDerivedSymbol) {
|
|
57601
|
+
didAddDerivedSymbol = false;
|
|
57602
|
+
for (const statement of componentBody.body ?? []) {
|
|
57603
|
+
if (!isNodeOfType(statement, "VariableDeclaration")) continue;
|
|
57604
|
+
for (const candidateDeclarator of statement.declarations ?? []) {
|
|
57605
|
+
if (!isNodeOfType(candidateDeclarator.id, "Identifier") || !candidateDeclarator.init) continue;
|
|
57606
|
+
const candidateInitializer = stripParenExpression(candidateDeclarator.init);
|
|
57607
|
+
if (isFunctionLike$1(candidateInitializer) || isNodeOfType(candidateInitializer, "CallExpression") && isReactApiCall(candidateInitializer, USE_CALLBACK_ONLY, context.scopes, REACT_API_CALL_OPTIONS)) continue;
|
|
57608
|
+
if (!expressionReadsDerivedSymbol(context, candidateInitializer, stateDerivedSymbolIds)) continue;
|
|
57609
|
+
const candidateSymbol = context.scopes.symbolFor(candidateDeclarator.id);
|
|
57610
|
+
if (candidateSymbol?.kind === "const" && candidateSymbol.references.every((reference) => reference.flag === "read" && !isTransparentAssignmentTarget(reference.identifier)) && !stateDerivedSymbolIds.has(candidateSymbol.id)) {
|
|
57611
|
+
stateDerivedSymbolIds.add(candidateSymbol.id);
|
|
57612
|
+
didAddDerivedSymbol = true;
|
|
57613
|
+
}
|
|
57614
|
+
}
|
|
57615
|
+
}
|
|
57616
|
+
}
|
|
57617
|
+
const staticSpreadVisibilityBySymbolId = /* @__PURE__ */ new Map();
|
|
57618
|
+
const hasOnlyStaticObjectReferences = (identifier, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
57619
|
+
const symbol = context.scopes.symbolFor(identifier);
|
|
57620
|
+
if (!symbol) return false;
|
|
57621
|
+
if (visitedSymbolIds.has(symbol.id)) return true;
|
|
57622
|
+
const nextVisitedSymbolIds = new Set(visitedSymbolIds);
|
|
57623
|
+
nextVisitedSymbolIds.add(symbol.id);
|
|
57624
|
+
let hasUnknownReference = false;
|
|
57625
|
+
walkAst(componentBody, (node) => {
|
|
57626
|
+
if (hasUnknownReference || !isNodeOfType(node, "Identifier") || context.scopes.symbolFor(node)?.id !== symbol.id || node === symbol.bindingIdentifier) return;
|
|
57627
|
+
const referenceRoot = findTransparentExpressionRoot(node);
|
|
57628
|
+
const parent = referenceRoot.parent;
|
|
57629
|
+
if (isNodeOfType(parent, "JSXSpreadAttribute") && parent.argument === referenceRoot) return;
|
|
57630
|
+
if (isNodeOfType(parent, "VariableDeclarator") && parent.init === referenceRoot && isNodeOfType(parent.id, "Identifier") && isNodeOfType(parent.parent, "VariableDeclaration") && parent.parent.kind === "const" && hasOnlyStaticObjectReferences(parent.id, nextVisitedSymbolIds)) return;
|
|
57631
|
+
hasUnknownReference = true;
|
|
57632
|
+
return false;
|
|
57633
|
+
});
|
|
57634
|
+
return !hasUnknownReference;
|
|
57635
|
+
};
|
|
57636
|
+
const classifyStaticSpreadObject = (identifier, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
57637
|
+
const symbol = context.scopes.symbolFor(identifier);
|
|
57638
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return "unknown";
|
|
57639
|
+
const cachedVisibility = staticSpreadVisibilityBySymbolId.get(symbol.id);
|
|
57640
|
+
if (cachedVisibility) return cachedVisibility;
|
|
57641
|
+
if (symbol.kind !== "const" || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || !isNodeOfType(symbol.declarationNode.id, "Identifier") || symbol.declarationNode.id !== symbol.bindingIdentifier || !symbol.declarationNode.init) return "unknown";
|
|
57642
|
+
if (!hasOnlyStaticObjectReferences(identifier)) return "unknown";
|
|
57643
|
+
const initializer = stripParenExpression(symbol.declarationNode.init);
|
|
57644
|
+
const nextVisitedSymbolIds = new Set(visitedSymbolIds);
|
|
57645
|
+
nextVisitedSymbolIds.add(symbol.id);
|
|
57646
|
+
if (isNodeOfType(initializer, "Identifier")) {
|
|
57647
|
+
const visibility = classifyStaticSpreadObject(initializer, nextVisitedSymbolIds);
|
|
57648
|
+
staticSpreadVisibilityBySymbolId.set(symbol.id, visibility);
|
|
57649
|
+
return visibility;
|
|
57650
|
+
}
|
|
57651
|
+
if (!isNodeOfType(initializer, "ObjectExpression")) return "unknown";
|
|
57652
|
+
let visibility = "non-visible";
|
|
57653
|
+
for (const property of initializer.properties ?? []) {
|
|
57654
|
+
const propertyName = getStaticObjectPropertyName(property);
|
|
57655
|
+
if (!isNodeOfType(property, "Property") || !propertyName) {
|
|
57656
|
+
visibility = "unknown";
|
|
57657
|
+
break;
|
|
57658
|
+
}
|
|
57659
|
+
if (expressionReadsDerivedSymbol(context, property.value, stateDerivedSymbolIds) && !isNonVisibleJsxSpreadProperty(propertyName)) visibility = "visible";
|
|
57660
|
+
}
|
|
57661
|
+
staticSpreadVisibilityBySymbolId.set(symbol.id, visibility);
|
|
57662
|
+
return visibility;
|
|
57663
|
+
};
|
|
57664
|
+
let hasNonAriaReference = false;
|
|
57665
|
+
walkAst(componentBody, (node) => {
|
|
57666
|
+
if (hasNonAriaReference) return false;
|
|
57667
|
+
if (!isNodeOfType(node, "Identifier") || !stateDerivedSymbolIds.has(context.scopes.symbolFor(node)?.id ?? -1)) return;
|
|
57668
|
+
if (findEnclosingFunction$1(node) !== componentFunction) return;
|
|
57669
|
+
const parent = node.parent;
|
|
57670
|
+
if (parent && (isNodeOfType(parent, "MemberExpression") && parent.property === node && !parent.computed || isNodeOfType(parent, "Property") && parent.key === node && !parent.computed)) return;
|
|
57671
|
+
let cursor = parent;
|
|
57672
|
+
while (cursor && cursor !== componentBody) {
|
|
57673
|
+
if (isFunctionLike$1(cursor)) return;
|
|
57674
|
+
if (isNodeOfType(cursor, "JSXSpreadAttribute")) {
|
|
57675
|
+
if (isNodeOfType(node, "Identifier") && classifyStaticSpreadObject(node) === "visible") hasNonAriaReference = true;
|
|
57676
|
+
return;
|
|
57677
|
+
}
|
|
57678
|
+
if (isNodeOfType(cursor, "JSXAttribute")) {
|
|
57679
|
+
if (isEventHandlerAttribute(cursor)) return;
|
|
57680
|
+
if (!isInsideIdOrAriaAttribute(node)) hasNonAriaReference = true;
|
|
57681
|
+
return;
|
|
57682
|
+
}
|
|
57683
|
+
if (isNodeOfType(cursor, "ReturnStatement")) {
|
|
57684
|
+
hasNonAriaReference = true;
|
|
57685
|
+
return;
|
|
57686
|
+
}
|
|
57687
|
+
cursor = cursor.parent;
|
|
57688
|
+
}
|
|
57689
|
+
});
|
|
57690
|
+
return hasNonAriaReference ? stateIdentifier.name : null;
|
|
57691
|
+
};
|
|
57692
|
+
const isExactViewportSubscriptionEffect = (context, effectCall, callback) => {
|
|
57693
|
+
if (!isReactApiCall(effectCall, USE_EFFECT_ONLY, context.scopes, REACT_API_CALL_OPTIONS)) return false;
|
|
57694
|
+
if (!isFunctionLike$1(callback) || callback.async || !isNodeOfType(callback.body, "BlockStatement")) return false;
|
|
57695
|
+
const statements = getCallbackStatements(callback);
|
|
57696
|
+
if (statements.length !== 4) return false;
|
|
57697
|
+
const handlerDeclaration = statements[0];
|
|
57698
|
+
if (!isNodeOfType(handlerDeclaration, "VariableDeclaration") || handlerDeclaration.kind !== "const" || handlerDeclaration.declarations?.length !== 1) return false;
|
|
57699
|
+
const handlerDeclarator = handlerDeclaration.declarations[0];
|
|
57700
|
+
if (!isNodeOfType(handlerDeclarator.id, "Identifier") || !isFunctionLike$1(handlerDeclarator.init)) return false;
|
|
57701
|
+
const handlerStatements = getCallbackStatements(handlerDeclarator.init);
|
|
57702
|
+
if (handlerStatements.length !== 1) return false;
|
|
57703
|
+
const handlerSetter = getDirectWindowWidthSetter(context, unwrapReturnExpression(handlerStatements[0]));
|
|
57704
|
+
const subscribedHandler = getResizeListenerHandler(context, statements[1], "addEventListener");
|
|
57705
|
+
const immediateSetter = getDirectWindowWidthSetter(context, statements[2]);
|
|
57706
|
+
const cleanupHandler = getCleanupResizeHandler(context, statements[3]);
|
|
57707
|
+
if (!handlerSetter || !subscribedHandler || !immediateSetter || !cleanupHandler) return false;
|
|
57708
|
+
const handlerSymbol = context.scopes.symbolFor(handlerDeclarator.id);
|
|
57709
|
+
if (!handlerSymbol || context.scopes.symbolFor(subscribedHandler) !== handlerSymbol || context.scopes.symbolFor(cleanupHandler) !== handlerSymbol) return false;
|
|
57710
|
+
if (!isNodeOfType(handlerSetter.callee, "Identifier") || !isNodeOfType(immediateSetter.callee, "Identifier") || context.scopes.symbolFor(handlerSetter.callee) !== context.scopes.symbolFor(immediateSetter.callee)) return false;
|
|
57711
|
+
const componentFunction = findEnclosingFunction$1(effectCall);
|
|
57712
|
+
if (!isFunctionLike$1(componentFunction) || !isNodeOfType(componentFunction.body, "BlockStatement")) return false;
|
|
57713
|
+
return findExactViewportState(context, componentFunction, immediateSetter) !== null;
|
|
57714
|
+
};
|
|
56294
57715
|
const renderingHydrationNoFlicker = defineRule({
|
|
56295
57716
|
id: "rendering-hydration-no-flicker",
|
|
56296
57717
|
title: "useEffect setState flashes on mount",
|
|
@@ -56303,7 +57724,14 @@ const renderingHydrationNoFlicker = defineRule({
|
|
|
56303
57724
|
if (!isNodeOfType(depsNode, "ArrayExpression") || depsNode.elements?.length !== 0) return;
|
|
56304
57725
|
const callback = getEffectCallback(node);
|
|
56305
57726
|
if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
|
|
56306
|
-
|
|
57727
|
+
if (isExactViewportSubscriptionEffect(context, node, callback)) {
|
|
57728
|
+
context.report({
|
|
57729
|
+
node,
|
|
57730
|
+
message: "This flashes for your users because useEffect(setState, []) runs after the first paint, so use useSyncExternalStore, or add suppressHydrationWarning"
|
|
57731
|
+
});
|
|
57732
|
+
return;
|
|
57733
|
+
}
|
|
57734
|
+
const bodyStatements = getCallbackStatements(callback);
|
|
56307
57735
|
if (bodyStatements.length !== 1) return;
|
|
56308
57736
|
const soleStatement = bodyStatements[0];
|
|
56309
57737
|
if (!isNodeOfType(soleStatement, "ExpressionStatement")) return;
|
|
@@ -64945,6 +66373,109 @@ const isDeferrableSideEffectCall = (objectName, methodName) => {
|
|
|
64945
66373
|
if (ANALYTICS_DEFERRABLE_OBJECTS.has(objectName)) return ANALYTICS_DEFERRABLE_METHODS.has(methodName);
|
|
64946
66374
|
return false;
|
|
64947
66375
|
};
|
|
66376
|
+
const NEXT_SERVER_SOURCE = "next/server";
|
|
66377
|
+
const NEXT_AFTER_EXPORT_NAMES = new Set(["after", "unstable_after"]);
|
|
66378
|
+
const isNextAfterImportSymbol = (symbol, contextNode) => {
|
|
66379
|
+
if (symbol.kind !== "import") return false;
|
|
66380
|
+
const importBinding = getImportBindingForName(contextNode, symbol.name);
|
|
66381
|
+
return Boolean(importBinding && importBinding.source === NEXT_SERVER_SOURCE && !importBinding.isNamespace && importBinding.exportedName && NEXT_AFTER_EXPORT_NAMES.has(importBinding.exportedName));
|
|
66382
|
+
};
|
|
66383
|
+
const isDirectObjectPatternBinding = (symbol) => {
|
|
66384
|
+
if (!isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return false;
|
|
66385
|
+
if (!isNodeOfType(symbol.declarationNode.id, "ObjectPattern")) return false;
|
|
66386
|
+
let bindingNode = symbol.bindingIdentifier;
|
|
66387
|
+
if (isNodeOfType(bindingNode.parent, "AssignmentPattern") && bindingNode.parent.left === bindingNode) bindingNode = bindingNode.parent;
|
|
66388
|
+
const property = bindingNode.parent;
|
|
66389
|
+
return Boolean(isNodeOfType(property, "Property") && property.value === bindingNode && property.parent === symbol.declarationNode.id);
|
|
66390
|
+
};
|
|
66391
|
+
const isNextServerNamespace = (expression, contextNode, scopes) => {
|
|
66392
|
+
let candidate = stripParenExpression(expression);
|
|
66393
|
+
const visitedSymbolIds = /* @__PURE__ */ new Set();
|
|
66394
|
+
while (isNodeOfType(candidate, "Identifier")) {
|
|
66395
|
+
const symbol = scopes.symbolFor(candidate);
|
|
66396
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
|
|
66397
|
+
if (symbol.kind === "import") {
|
|
66398
|
+
const importBinding = getImportBindingForName(contextNode, symbol.name);
|
|
66399
|
+
return Boolean(importBinding?.source === NEXT_SERVER_SOURCE && importBinding.isNamespace);
|
|
66400
|
+
}
|
|
66401
|
+
if (symbol.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return false;
|
|
66402
|
+
visitedSymbolIds.add(symbol.id);
|
|
66403
|
+
candidate = stripParenExpression(symbol.initializer);
|
|
66404
|
+
}
|
|
66405
|
+
return false;
|
|
66406
|
+
};
|
|
66407
|
+
const isNextAfterCallee = (callee, contextNode, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
66408
|
+
const candidate = stripParenExpression(callee);
|
|
66409
|
+
if (isNodeOfType(candidate, "MemberExpression")) {
|
|
66410
|
+
const propertyName = getStaticPropertyKeyName(candidate, { allowComputedString: true });
|
|
66411
|
+
return Boolean(propertyName && NEXT_AFTER_EXPORT_NAMES.has(propertyName) && isNextServerNamespace(candidate.object, contextNode, scopes));
|
|
66412
|
+
}
|
|
66413
|
+
if (!isNodeOfType(candidate, "Identifier")) return false;
|
|
66414
|
+
const symbol = scopes.symbolFor(candidate);
|
|
66415
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
|
|
66416
|
+
if (isNextAfterImportSymbol(symbol, contextNode)) return true;
|
|
66417
|
+
const destructuredPropertyName = getDestructuredBindingPropertyName(symbol.bindingIdentifier);
|
|
66418
|
+
if (symbol.kind === "const" && symbol.initializer && isDirectObjectPatternBinding(symbol) && destructuredPropertyName && NEXT_AFTER_EXPORT_NAMES.has(destructuredPropertyName)) return isNextServerNamespace(symbol.initializer, contextNode, scopes);
|
|
66419
|
+
if (symbol.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return false;
|
|
66420
|
+
visitedSymbolIds.add(symbol.id);
|
|
66421
|
+
return isNextAfterCallee(symbol.initializer, contextNode, scopes, visitedSymbolIds);
|
|
66422
|
+
};
|
|
66423
|
+
const getDirectArgumentCall = (expression) => {
|
|
66424
|
+
const expressionRoot = findTransparentExpressionRoot(expression);
|
|
66425
|
+
const parent = expressionRoot.parent;
|
|
66426
|
+
if (!isNodeOfType(parent, "CallExpression")) return null;
|
|
66427
|
+
return parent.arguments[0] === expressionRoot ? parent : null;
|
|
66428
|
+
};
|
|
66429
|
+
const isScheduledByNextAfter = (expression, scopes) => {
|
|
66430
|
+
const callExpression = getDirectArgumentCall(expression);
|
|
66431
|
+
return Boolean(callExpression && isNextAfterCallee(callExpression.callee, callExpression, scopes));
|
|
66432
|
+
};
|
|
66433
|
+
const getFunctionBindingSymbol = (functionNode, scopes) => {
|
|
66434
|
+
if (isNodeOfType(functionNode, "FunctionDeclaration") && functionNode.id) return scopes.scopeFor(functionNode).symbols.find((symbol) => symbol.declarationNode === functionNode) ?? null;
|
|
66435
|
+
const functionRoot = findTransparentExpressionRoot(functionNode);
|
|
66436
|
+
const parent = functionRoot.parent;
|
|
66437
|
+
if (!isNodeOfType(parent, "VariableDeclarator") || parent.init !== functionRoot || !isNodeOfType(parent.id, "Identifier")) return null;
|
|
66438
|
+
return scopes.symbolFor(parent.id);
|
|
66439
|
+
};
|
|
66440
|
+
const isDirectlyExported = (symbol) => {
|
|
66441
|
+
let declaration = symbol.declarationNode;
|
|
66442
|
+
if (isNodeOfType(declaration, "VariableDeclarator")) declaration = declaration.parent;
|
|
66443
|
+
return Boolean(declaration?.parent && (isNodeOfType(declaration.parent, "ExportNamedDeclaration") || isNodeOfType(declaration.parent, "ExportDefaultDeclaration")));
|
|
66444
|
+
};
|
|
66445
|
+
const isLexicallyInsideFunction = (node, functionNode) => {
|
|
66446
|
+
let enclosingFunction = findEnclosingFunction$1(node);
|
|
66447
|
+
while (enclosingFunction) {
|
|
66448
|
+
if (enclosingFunction === functionNode) return true;
|
|
66449
|
+
enclosingFunction = findEnclosingFunction$1(enclosingFunction);
|
|
66450
|
+
}
|
|
66451
|
+
return false;
|
|
66452
|
+
};
|
|
66453
|
+
const isExclusivelyScheduledByNextAfter = (functionNode, scopes, visitedFunctionSymbolIds) => {
|
|
66454
|
+
if (isScheduledByNextAfter(functionNode, scopes)) return true;
|
|
66455
|
+
const functionSymbol = getFunctionBindingSymbol(functionNode, scopes);
|
|
66456
|
+
if (!functionSymbol || isDirectlyExported(functionSymbol) || visitedFunctionSymbolIds.has(functionSymbol.id)) return false;
|
|
66457
|
+
const nextVisitedFunctionSymbolIds = new Set(visitedFunctionSymbolIds).add(functionSymbol.id);
|
|
66458
|
+
let hasAfterUse = false;
|
|
66459
|
+
for (const reference of functionSymbol.references) {
|
|
66460
|
+
if (reference.flag !== "read") return false;
|
|
66461
|
+
if (isLexicallyInsideFunction(reference.identifier, functionNode)) continue;
|
|
66462
|
+
if (isScheduledByNextAfter(reference.identifier, scopes)) {
|
|
66463
|
+
hasAfterUse = true;
|
|
66464
|
+
continue;
|
|
66465
|
+
}
|
|
66466
|
+
if (!isInsideNextAfterCallback(reference.identifier, scopes, nextVisitedFunctionSymbolIds)) return false;
|
|
66467
|
+
hasAfterUse = true;
|
|
66468
|
+
}
|
|
66469
|
+
return hasAfterUse;
|
|
66470
|
+
};
|
|
66471
|
+
const isInsideNextAfterCallback = (node, scopes, visitedFunctionSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
66472
|
+
let enclosingFunction = findEnclosingFunction$1(node);
|
|
66473
|
+
while (enclosingFunction) {
|
|
66474
|
+
if (isExclusivelyScheduledByNextAfter(enclosingFunction, scopes, visitedFunctionSymbolIds)) return true;
|
|
66475
|
+
enclosingFunction = findEnclosingFunction$1(enclosingFunction);
|
|
66476
|
+
}
|
|
66477
|
+
return false;
|
|
66478
|
+
};
|
|
64948
66479
|
const serverAfterNonblocking = defineRule({
|
|
64949
66480
|
id: "server-after-nonblocking",
|
|
64950
66481
|
title: "Blocking side effect before response",
|
|
@@ -64979,6 +66510,7 @@ const serverAfterNonblocking = defineRule({
|
|
|
64979
66510
|
if (!objectName) return;
|
|
64980
66511
|
const methodName = node.callee.property.name;
|
|
64981
66512
|
if (!isDeferrableSideEffectCall(objectName, methodName)) return;
|
|
66513
|
+
if (isInsideNextAfterCallback(node, context.scopes)) return;
|
|
64982
66514
|
context.report({
|
|
64983
66515
|
node,
|
|
64984
66516
|
message: `${objectName}.${methodName}() runs before the response, so your users wait longer for it.`
|
|
@@ -66031,14 +67563,6 @@ const isStateKey = (key) => {
|
|
|
66031
67563
|
if (isNodeOfType(key, "Literal") && typeof key.value === "string") return key.value === "state";
|
|
66032
67564
|
return false;
|
|
66033
67565
|
};
|
|
66034
|
-
const findEnclosingClass = (node) => {
|
|
66035
|
-
let ancestor = node.parent;
|
|
66036
|
-
while (ancestor) {
|
|
66037
|
-
if (isNodeOfType(ancestor, "ClassDeclaration") || isNodeOfType(ancestor, "ClassExpression")) return ancestor;
|
|
66038
|
-
ancestor = ancestor.parent ?? null;
|
|
66039
|
-
}
|
|
66040
|
-
return null;
|
|
66041
|
-
};
|
|
66042
67566
|
const isInConstructor = (node) => {
|
|
66043
67567
|
let ancestor = node.parent;
|
|
66044
67568
|
while (ancestor) {
|
|
@@ -66253,17 +67777,34 @@ const stylePropObject = defineRule({
|
|
|
66253
67777
|
};
|
|
66254
67778
|
}
|
|
66255
67779
|
});
|
|
67780
|
+
//#endregion
|
|
67781
|
+
//#region src/plugin/rules/security-scan/utils/has-use-server-directive-in-content.ts
|
|
67782
|
+
const hasUseServerDirectiveInContent = (content, relativePath = "source.tsx") => {
|
|
67783
|
+
const programNode = parseSourceText({
|
|
67784
|
+
filename: relativePath,
|
|
67785
|
+
sourceText: content,
|
|
67786
|
+
shouldAttachParentReferences: false
|
|
67787
|
+
});
|
|
67788
|
+
return programNode === null ? false : hasDirective(programNode, "use server");
|
|
67789
|
+
};
|
|
67790
|
+
//#endregion
|
|
67791
|
+
//#region src/plugin/rules/security-scan/supabase-client-owned-authz-field.ts
|
|
67792
|
+
const scanSupabaseClientOwnedAuthzField = scanByPattern({
|
|
67793
|
+
shouldScan: (file) => isClientSourcePath(file.relativePath),
|
|
67794
|
+
pattern: /\b(?:ownerId|ownerID|creatorId|creatorID|userId|userID|uid|providerId|providerID|orgId|orgID|tenantId|tenantID|teamId|teamID|workspaceId|workspaceID|ghostOrg|role|roles|isAdmin|admin)\b/,
|
|
67795
|
+
requireAll: [/\b(?:supabase\b|\.from\s*\(\s*["'][^"']+["']\s*\))[\s\S]{0,700}\b(?:insert|upsert|update)\s*\(\s*(?:\{|\[?\s*\{)[\s\S]{0,700}\b(?:ownerId|creatorId|userId|orgId|tenantId|role|isAdmin)\b/i],
|
|
67796
|
+
message: "Client Supabase code appears to write user, tenant, owner, or role fields that should be enforced by RLS."
|
|
67797
|
+
});
|
|
66256
67798
|
const supabaseClientOwnedAuthzField = defineRule({
|
|
66257
67799
|
id: "supabase-client-owned-authz-field",
|
|
66258
67800
|
title: "Client writes Supabase authorization field",
|
|
66259
67801
|
severity: "error",
|
|
66260
67802
|
recommendation: "Use RLS policies based on `auth.uid()` and server-owned membership rows; do not trust client-provided owner, org, or role columns.",
|
|
66261
|
-
scan:
|
|
66262
|
-
|
|
66263
|
-
|
|
66264
|
-
|
|
66265
|
-
|
|
66266
|
-
})
|
|
67803
|
+
scan: (file) => {
|
|
67804
|
+
const findings = scanSupabaseClientOwnedAuthzField(file);
|
|
67805
|
+
if (findings.length === 0) return findings;
|
|
67806
|
+
return hasUseServerDirectiveInContent(file.content, file.relativePath) ? [] : findings;
|
|
67807
|
+
}
|
|
66267
67808
|
});
|
|
66268
67809
|
//#endregion
|
|
66269
67810
|
//#region src/plugin/rules/security-scan/utils/is-supabase-migration-path.ts
|
|
@@ -70733,6 +72274,17 @@ const reactDoctorRules = [
|
|
|
70733
72274
|
requires: [...new Set(["react", ...noRedundantShouldComponentUpdate.requires ?? []])]
|
|
70734
72275
|
}
|
|
70735
72276
|
},
|
|
72277
|
+
{
|
|
72278
|
+
key: "react-doctor/no-ref-callback-cleanup-before-react-19",
|
|
72279
|
+
id: "no-ref-callback-cleanup-before-react-19",
|
|
72280
|
+
source: "react-doctor",
|
|
72281
|
+
originallyExternal: false,
|
|
72282
|
+
rule: {
|
|
72283
|
+
...noRefCallbackCleanupBeforeReact19,
|
|
72284
|
+
framework: "global",
|
|
72285
|
+
category: "Bugs"
|
|
72286
|
+
}
|
|
72287
|
+
},
|
|
70736
72288
|
{
|
|
70737
72289
|
key: "react-doctor/no-ref-current-in-render",
|
|
70738
72290
|
id: "no-ref-current-in-render",
|