oxlint-plugin-react-doctor 0.7.9-dev.db5fe10 → 0.7.9-dev.e5a5f73
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 +2013 -308
- 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,13 +14973,188 @@ 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 isJsxRefAttribute = (node) => isNodeOfType(node, "JSXAttribute") && isNodeOfType(node.name, "JSXIdentifier") && node.name.name === "ref";
|
|
14977
|
+
const isFunctionForwardedToReactRef = (functionNode, context) => {
|
|
14978
|
+
const bindingIdentifier = getFunctionBindingIdentifier$1(functionNode);
|
|
14979
|
+
if (!bindingIdentifier) return false;
|
|
14980
|
+
const symbol = context.scopes.symbolFor(bindingIdentifier);
|
|
14981
|
+
if (!symbol) return false;
|
|
14982
|
+
return symbol.references.some((reference) => {
|
|
14983
|
+
const referenceRoot = findTransparentExpressionRoot(reference.identifier);
|
|
14984
|
+
const expressionContainer = referenceRoot.parent;
|
|
14985
|
+
return Boolean(isNodeOfType(expressionContainer, "JSXExpressionContainer") && expressionContainer.expression === referenceRoot && isJsxRefAttribute(expressionContainer.parent));
|
|
14986
|
+
});
|
|
14987
|
+
};
|
|
14988
|
+
const findRetainedDisposerStorages = (disposerFunction, usage, context) => {
|
|
14989
|
+
if (!isFunctionLike$1(disposerFunction) || disposerFunction.async || disposerFunction.generator) return [];
|
|
14990
|
+
const usageFunction = findEnclosingFunction$1(usage.node);
|
|
14991
|
+
if (!usageFunction || !isFunctionLike$1(usageFunction)) return [];
|
|
14992
|
+
const assignments = /* @__PURE__ */ new Map();
|
|
14993
|
+
const collectAssignment = (expression) => {
|
|
14994
|
+
const expressionRoot = findTransparentExpressionRoot(expression);
|
|
14995
|
+
const assignment = expressionRoot.parent;
|
|
14996
|
+
if (!isNodeOfType(assignment, "AssignmentExpression") || assignment.operator !== "=" || assignment.right !== expressionRoot) return;
|
|
14997
|
+
const refSymbol = resolveReactRefSymbol(stripParenExpression(assignment.left), context.scopes);
|
|
14998
|
+
const refCurrentKey = resolveExpressionKey(assignment.left, context);
|
|
14999
|
+
const retainedFunction = findEnclosingFunction$1(assignment);
|
|
15000
|
+
const assignmentStart = getRangeStart(assignment);
|
|
15001
|
+
if (!refSymbol || !refCurrentKey || !retainedFunction || retainedFunction !== usageFunction || assignmentStart === null) return;
|
|
15002
|
+
assignments.set(assignmentStart, {
|
|
15003
|
+
assignmentNode: assignment,
|
|
15004
|
+
refCurrentKey,
|
|
15005
|
+
retainedFunction
|
|
15006
|
+
});
|
|
15007
|
+
};
|
|
15008
|
+
collectAssignment(disposerFunction);
|
|
15009
|
+
const bindingIdentifier = getFunctionBindingIdentifier$1(disposerFunction);
|
|
15010
|
+
const symbol = bindingIdentifier ? context.scopes.symbolFor(bindingIdentifier) : null;
|
|
15011
|
+
for (const reference of symbol?.references ?? []) collectAssignment(reference.identifier);
|
|
15012
|
+
walkAst(usageFunction.body, (child) => {
|
|
15013
|
+
if (child !== usageFunction.body && isFunctionLike$1(child)) return false;
|
|
15014
|
+
if (isNodeOfType(child, "AssignmentExpression") && resolveStableValue(child.right, context) === disposerFunction) collectAssignment(child.right);
|
|
15015
|
+
});
|
|
15016
|
+
return [...assignments.values()];
|
|
15017
|
+
};
|
|
15018
|
+
const isRetainedDisposerStorageEstablished = (storage, usage, context) => doMatchingNodesCoverEveryPathBeforeUsage(usage.node, [storage.assignmentNode], storage.retainedFunction, context) || doMatchingNodesCoverEveryPathAfterUsage(usage.node, [storage.assignmentNode], context);
|
|
15019
|
+
const hasUnsafeRetainedDisposerOverwrite = (storage, usage, context) => {
|
|
15020
|
+
let hasUnsafeOverwrite = false;
|
|
15021
|
+
walkAst(storage.retainedFunction.body, (child) => {
|
|
15022
|
+
if (hasUnsafeOverwrite) return false;
|
|
15023
|
+
if (child !== storage.retainedFunction.body && isFunctionLike$1(child)) return false;
|
|
15024
|
+
if (!isNodeOfType(child, "AssignmentExpression") || child === storage.assignmentNode || resolveExpressionKey(child.left, context) !== storage.refCurrentKey || !canNodeReachLaterNodeWithinFunction(usage.node, child, storage.retainedFunction, context)) return;
|
|
15025
|
+
const storedValue = resolveStableValue(child.right, context);
|
|
15026
|
+
if (!storedValue || !isFunctionLike$1(storedValue) || !doesCleanupFunctionReleaseUsage(storedValue, usage, context)) {
|
|
15027
|
+
hasUnsafeOverwrite = true;
|
|
15028
|
+
return false;
|
|
15029
|
+
}
|
|
15030
|
+
});
|
|
15031
|
+
return hasUnsafeOverwrite;
|
|
15032
|
+
};
|
|
15033
|
+
const hasEffectCleanupInvocation = (storage, usage, context) => {
|
|
15034
|
+
const componentFunction = findEnclosingFunction$1(storage.retainedFunction);
|
|
15035
|
+
if (!componentFunction || !isFunctionLike$1(componentFunction)) return false;
|
|
15036
|
+
const cleanupFunctionInvokesRef = (cleanupFunction) => {
|
|
15037
|
+
if (!isFunctionLike$1(cleanupFunction)) return false;
|
|
15038
|
+
let didFindCleanupCall = false;
|
|
15039
|
+
walkAst(cleanupFunction.body, (child) => {
|
|
15040
|
+
if (didFindCleanupCall) return false;
|
|
15041
|
+
if (child !== cleanupFunction.body && isFunctionLike$1(child)) return false;
|
|
15042
|
+
if (isNodeOfType(child, "CallExpression") && resolveExpressionKey(child.callee, context) === storage.refCurrentKey) {
|
|
15043
|
+
const callRoot = findTransparentExpressionRoot(child);
|
|
15044
|
+
const callStatement = callRoot.parent;
|
|
15045
|
+
const isDirectBlockStatement = isNodeOfType(cleanupFunction.body, "BlockStatement") && isNodeOfType(callStatement, "ExpressionStatement") && callStatement.parent === cleanupFunction.body;
|
|
15046
|
+
const isConciseBody = cleanupFunction.body === callRoot;
|
|
15047
|
+
if ((isDirectBlockStatement || isConciseBody) && !hasUnprovenReturnBeforeRefOwnedRelease(cleanupFunction, child, storage.refCurrentKey, context)) {
|
|
15048
|
+
didFindCleanupCall = true;
|
|
15049
|
+
return false;
|
|
15050
|
+
}
|
|
15051
|
+
}
|
|
15052
|
+
});
|
|
15053
|
+
return didFindCleanupCall;
|
|
15054
|
+
};
|
|
15055
|
+
const effectReturnsCleanup = (effectCallback) => {
|
|
15056
|
+
if (!isFunctionLike$1(effectCallback)) return false;
|
|
15057
|
+
if (!isNodeOfType(effectCallback.body, "BlockStatement")) {
|
|
15058
|
+
const cleanupFunction = resolveRefOwnedCleanupFunction(effectCallback.body, context);
|
|
15059
|
+
return Boolean(cleanupFunction && cleanupFunctionInvokesRef(cleanupFunction));
|
|
15060
|
+
}
|
|
15061
|
+
const matchingReturns = [];
|
|
15062
|
+
walkInsideStatementBlocks(effectCallback.body, (child) => {
|
|
15063
|
+
if (!isNodeOfType(child, "ReturnStatement") || !child.argument) return;
|
|
15064
|
+
const cleanupFunction = resolveRefOwnedCleanupFunction(child.argument, context);
|
|
15065
|
+
if (!cleanupFunction || !cleanupFunctionInvokesRef(cleanupFunction)) return;
|
|
15066
|
+
matchingReturns.push(child);
|
|
15067
|
+
});
|
|
15068
|
+
return doMatchingNodesCoverEveryPathFromFunctionEntry(effectCallback, matchingReturns, context);
|
|
15069
|
+
};
|
|
15070
|
+
let didFindInvocation = false;
|
|
15071
|
+
walkAst(componentFunction.body, (child) => {
|
|
15072
|
+
if (didFindInvocation) return false;
|
|
15073
|
+
if (!isNodeOfType(child, "CallExpression") || findEnclosingFunction$1(child) !== componentFunction || !isReactApiCall(child, "useEffect", context.scopes)) return;
|
|
15074
|
+
const effectCallback = getEffectCallback(child);
|
|
15075
|
+
if (effectCallback && effectReturnsCleanup(effectCallback)) {
|
|
15076
|
+
didFindInvocation = true;
|
|
15077
|
+
return false;
|
|
15078
|
+
}
|
|
15079
|
+
});
|
|
15080
|
+
return didFindInvocation;
|
|
15081
|
+
};
|
|
15082
|
+
const hasCallbackRefReplacementInvocation = (storage, usage, context) => {
|
|
15083
|
+
const isReturnedCallbackRefShape = () => {
|
|
15084
|
+
if (!isFunctionLike$1(storage.retainedFunction)) return false;
|
|
15085
|
+
const callbackCall = findTransparentExpressionRoot(storage.retainedFunction).parent;
|
|
15086
|
+
if (!isNodeOfType(callbackCall, "CallExpression") || !isReactApiCall(callbackCall, "useCallback", context.scopes)) return false;
|
|
15087
|
+
const nodeParameter = storage.retainedFunction.params?.[0];
|
|
15088
|
+
const nodeParameterKey = resolveExpressionKey(nodeParameter, context);
|
|
15089
|
+
if (!nodeParameterKey || usage.receiverKey !== nodeParameterKey) return false;
|
|
15090
|
+
const bindingIdentifier = getFunctionBindingIdentifier$1(storage.retainedFunction);
|
|
15091
|
+
const symbol = bindingIdentifier ? context.scopes.symbolFor(bindingIdentifier) : null;
|
|
15092
|
+
if (!Boolean(symbol?.references.some((reference) => {
|
|
15093
|
+
const referenceRoot = findTransparentExpressionRoot(reference.identifier);
|
|
15094
|
+
const property = referenceRoot.parent;
|
|
15095
|
+
if (!isNodeOfType(property, "Property") || property.value !== referenceRoot || !isNodeOfType(property.parent, "ObjectExpression")) return false;
|
|
15096
|
+
const returnedObject = findTransparentExpressionRoot(property.parent);
|
|
15097
|
+
const returnStatement = returnedObject.parent;
|
|
15098
|
+
if (!isNodeOfType(returnStatement, "ReturnStatement") || returnStatement.argument !== returnedObject) return false;
|
|
15099
|
+
const hookFunction = findEnclosingFunction$1(returnStatement);
|
|
15100
|
+
return Boolean(hookFunction && getFunctionBindingIdentifier$1(hookFunction)?.name.startsWith("use"));
|
|
15101
|
+
}))) return false;
|
|
15102
|
+
const usageStart = getRangeStart(usage.node);
|
|
15103
|
+
if (usageStart === null) return false;
|
|
15104
|
+
let hasNullExit = false;
|
|
15105
|
+
walkAst(storage.retainedFunction.body, (child) => {
|
|
15106
|
+
if (hasNullExit) return false;
|
|
15107
|
+
if (child !== storage.retainedFunction.body && isFunctionLike$1(child)) return false;
|
|
15108
|
+
if (!isNodeOfType(child, "IfStatement") || (getRangeStart(child) ?? usageStart) >= usageStart) return;
|
|
15109
|
+
const test = stripParenExpression(child.test);
|
|
15110
|
+
if (!isNodeOfType(test, "UnaryExpression") || test.operator !== "!" || resolveExpressionKey(test.argument, context) !== nodeParameterKey) return;
|
|
15111
|
+
const consequent = child.consequent;
|
|
15112
|
+
hasNullExit = isNodeOfType(consequent, "ReturnStatement") || isNodeOfType(consequent, "BlockStatement") && consequent.body.some((statement) => isNodeOfType(statement, "ReturnStatement"));
|
|
15113
|
+
if (hasNullExit) return false;
|
|
15114
|
+
});
|
|
15115
|
+
return hasNullExit;
|
|
15116
|
+
};
|
|
15117
|
+
if (!isFunctionForwardedToReactRef(storage.retainedFunction, context) && !isReturnedCallbackRefShape()) return false;
|
|
15118
|
+
const cleanupCalls = [];
|
|
15119
|
+
walkAst(storage.retainedFunction.body, (child) => {
|
|
15120
|
+
if (child !== storage.retainedFunction.body && isFunctionLike$1(child)) return false;
|
|
15121
|
+
if (isNodeOfType(child, "CallExpression") && resolveExpressionKey(child.callee, context) === storage.refCurrentKey) cleanupCalls.push(child);
|
|
15122
|
+
});
|
|
15123
|
+
return doMatchingNodesCoverEveryPathBeforeUsage(usage.node, cleanupCalls, storage.retainedFunction, context);
|
|
15124
|
+
};
|
|
15125
|
+
const isRetainedDisposerRefRelease = (releaseNode, usage, context) => {
|
|
15126
|
+
const disposerFunction = findEnclosingFunction$1(releaseNode);
|
|
15127
|
+
if (!disposerFunction) return false;
|
|
15128
|
+
return findRetainedDisposerStorages(disposerFunction, usage, context).some((storage) => isRetainedDisposerStorageEstablished(storage, usage, context) && !hasUnsafeRetainedDisposerOverwrite(storage, usage, context) && (hasEffectCleanupInvocation(storage, usage, context) || hasCallbackRefReplacementInvocation(storage, usage, context)));
|
|
15129
|
+
};
|
|
15130
|
+
const isSelfReleasingListenerRelease = (releaseNode, releaseFunction, usage, context) => {
|
|
15131
|
+
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;
|
|
15132
|
+
const registrationCapture = resolveEventListenerCapture(usage.node.arguments?.[2], { allowIndeterminateEntries: true });
|
|
15133
|
+
const releaseCall = isNodeOfType(releaseNode, "ChainExpression") ? releaseNode.expression : releaseNode;
|
|
15134
|
+
if (!isNodeOfType(releaseCall, "CallExpression")) return false;
|
|
15135
|
+
const releaseCapture = resolveEventListenerCapture(releaseCall.arguments?.[2], { allowIndeterminateEntries: true });
|
|
15136
|
+
if (registrationCapture === null || releaseCapture === null || registrationCapture !== releaseCapture) return false;
|
|
15137
|
+
const ownerFunction = findEnclosingFunction$1(releaseFunction);
|
|
15138
|
+
if (!ownerFunction || !isFunctionLike$1(ownerFunction)) return false;
|
|
15139
|
+
const triggerRegistrations = [];
|
|
15140
|
+
walkAst(ownerFunction.body, (child) => {
|
|
15141
|
+
if (child !== ownerFunction.body && isFunctionLike$1(child)) return false;
|
|
15142
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
15143
|
+
const registrationDetails = getCallRegistrationDetails(child, context);
|
|
15144
|
+
if (registrationDetails.registrationVerbName === "addEventListener" && registrationDetails.receiverKey === usage.receiverKey && resolveStableValue(child.arguments?.[1], context) === releaseFunction) triggerRegistrations.push(child);
|
|
15145
|
+
});
|
|
15146
|
+
if (triggerRegistrations.some((triggerRegistration) => triggerRegistration === usage.node)) return true;
|
|
15147
|
+
return doMatchingNodesCoverEveryPathAfterUsage(usage.node, triggerRegistrations, context) || doMatchingNodesCoverEveryPathBeforeUsage(usage.node, triggerRegistrations, ownerFunction, context);
|
|
15148
|
+
};
|
|
14745
15149
|
const isReleaseReachableForUsage = (releaseNode, usage, context) => {
|
|
14746
15150
|
if (!isNodeReachableWithinFunction(releaseNode, context)) return false;
|
|
14747
15151
|
const releaseFunction = findEnclosingFunction$1(releaseNode);
|
|
14748
15152
|
if (!releaseFunction) return true;
|
|
14749
15153
|
if (releaseFunction === findEnclosingFunction$1(usage.node)) return true;
|
|
15154
|
+
if (isRetainedDisposerRefRelease(releaseNode, usage, context)) return true;
|
|
14750
15155
|
const usageFunction = findEnclosingFunction$1(usage.node);
|
|
14751
15156
|
if (usageFunction && isFunctionLike$1(usageFunction) && getAssignedReactRefSymbol(usageFunction, context) && isCleanupFunctionReferencedByReturn(usageFunction, releaseFunction, context)) return isReactRefCallbackCleanupOwnedByEffect(usageFunction, releaseFunction, usage, context);
|
|
15157
|
+
if (isSelfReleasingListenerRelease(releaseNode, releaseFunction, usage, context)) return true;
|
|
14752
15158
|
return isPotentiallyReachableFunction(releaseFunction, context);
|
|
14753
15159
|
};
|
|
14754
15160
|
const fileContainsReleaseForUsage = (usage, context) => {
|
|
@@ -15016,6 +15422,11 @@ const doesResourceResultEscape = (resourceNode, allowReturnedResourceEscape, all
|
|
|
15016
15422
|
parentNode = currentNode.parent;
|
|
15017
15423
|
continue;
|
|
15018
15424
|
}
|
|
15425
|
+
if (isNodeOfType(parentNode, "ConditionalExpression") && (parentNode.consequent === currentNode || parentNode.alternate === currentNode) || isNodeOfType(parentNode, "LogicalExpression") && (parentNode.right === currentNode || parentNode.left === currentNode && parentNode.operator !== "&&")) {
|
|
15426
|
+
currentNode = parentNode;
|
|
15427
|
+
parentNode = currentNode.parent;
|
|
15428
|
+
continue;
|
|
15429
|
+
}
|
|
15019
15430
|
if (isNodeOfType(parentNode, "VariableDeclarator") && parentNode.init === currentNode && isNodeOfType(parentNode.id, "Identifier") && isNodeOfType(parentNode.parent, "VariableDeclaration") && parentNode.parent.kind === "const") {
|
|
15020
15431
|
const ownerFunction = findEnclosingFunction$1(resourceNode);
|
|
15021
15432
|
const resourceSymbol = context.scopes.symbolFor(parentNode.id);
|
|
@@ -16877,7 +17288,7 @@ const collectCaptureDepKeys = (callback, scopes, declaredExactBindingKeys, allow
|
|
|
16877
17288
|
keys.add(depKey);
|
|
16878
17289
|
continue;
|
|
16879
17290
|
}
|
|
16880
|
-
const identitySourceKeys = resolveReactiveIdentitySourceKeys(symbol, scopes);
|
|
17291
|
+
const identitySourceKeys = resolvePureCalledFunctionSourceKeys(reference, symbol, scopes) ?? resolveRenderDerivedMutableSourceKeys(reference, symbol, scopes) ?? resolveReactiveIdentitySourceKeys(symbol, scopes);
|
|
16881
17292
|
if (identitySourceKeys) {
|
|
16882
17293
|
if (identitySourceKeys.size === 0) stableCapturedNames.add(depKey);
|
|
16883
17294
|
for (const identitySourceKey of identitySourceKeys) keys.add(identitySourceKey);
|
|
@@ -16960,6 +17371,161 @@ const resolveReactiveIdentitySourceKeys = (symbol, scopes) => {
|
|
|
16960
17371
|
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
17372
|
return resolveIdentitySourceKeysFromExpression(symbol.initializer, scopes, new Set([symbol.id]));
|
|
16962
17373
|
};
|
|
17374
|
+
const isPureDerivedExpression = (expression) => {
|
|
17375
|
+
const candidate = unwrapExpression$3(expression);
|
|
17376
|
+
if (isNodeOfType(candidate, "Literal") || isNodeOfType(candidate, "Identifier")) return true;
|
|
17377
|
+
if (isNodeOfType(candidate, "MemberExpression")) return isPureDerivedExpression(candidate.object) && (!candidate.computed || isPureDerivedExpression(candidate.property));
|
|
17378
|
+
if (isNodeOfType(candidate, "BinaryExpression") || isNodeOfType(candidate, "LogicalExpression")) return isPureDerivedExpression(candidate.left) && isPureDerivedExpression(candidate.right);
|
|
17379
|
+
if (isNodeOfType(candidate, "UnaryExpression")) return candidate.operator !== "delete" && isPureDerivedExpression(candidate.argument);
|
|
17380
|
+
if (isNodeOfType(candidate, "ConditionalExpression")) return isPureDerivedExpression(candidate.test) && isPureDerivedExpression(candidate.consequent) && isPureDerivedExpression(candidate.alternate);
|
|
17381
|
+
if (isNodeOfType(candidate, "TemplateLiteral")) return candidate.expressions.every((nestedExpression) => isPureDerivedExpression(nestedExpression));
|
|
17382
|
+
return false;
|
|
17383
|
+
};
|
|
17384
|
+
const isPureDerivedStatement = (statement) => {
|
|
17385
|
+
if (isNodeOfType(statement, "BlockStatement")) return statement.body.every((nestedStatement) => isPureDerivedStatement(nestedStatement));
|
|
17386
|
+
if (isNodeOfType(statement, "ReturnStatement")) return !statement.argument || isPureDerivedExpression(statement.argument);
|
|
17387
|
+
if (isNodeOfType(statement, "IfStatement")) return isPureDerivedExpression(statement.test) && isPureDerivedStatement(statement.consequent) && (!statement.alternate || isPureDerivedStatement(statement.alternate));
|
|
17388
|
+
return false;
|
|
17389
|
+
};
|
|
17390
|
+
const isPureDerivedFunction = (functionNode) => {
|
|
17391
|
+
if (!isNodeOfType(functionNode, "FunctionDeclaration") && !isNodeOfType(functionNode, "FunctionExpression") && !isNodeOfType(functionNode, "ArrowFunctionExpression")) return false;
|
|
17392
|
+
if (functionNode.async || functionNode.generator) return false;
|
|
17393
|
+
return isNodeOfType(functionNode.body, "BlockStatement") ? isPureDerivedStatement(functionNode.body) : isPureDerivedExpression(functionNode.body);
|
|
17394
|
+
};
|
|
17395
|
+
const resolvePureCalledFunctionSourceKeys = (reference, symbol, scopes) => {
|
|
17396
|
+
if (symbol.references.some((symbolReference) => symbolReference.flag !== "read")) return null;
|
|
17397
|
+
const referenceRoot = findTransparentExpressionRoot(reference.identifier);
|
|
17398
|
+
const callExpression = referenceRoot.parent;
|
|
17399
|
+
if (!isNodeOfType(callExpression, "CallExpression") || callExpression.callee !== referenceRoot) return null;
|
|
17400
|
+
const functionNode = getFunctionValueNode(symbol);
|
|
17401
|
+
if (!functionNode || !isPureDerivedFunction(functionNode)) return null;
|
|
17402
|
+
const sourceKeys = /* @__PURE__ */ new Set();
|
|
17403
|
+
for (const capturedReference of closureCaptures(functionNode, scopes)) {
|
|
17404
|
+
const capturedSymbol = capturedReference.resolvedSymbol;
|
|
17405
|
+
if (!capturedSymbol || capturedSymbol.id === symbol.id) continue;
|
|
17406
|
+
if (isOutsideAllFunctions(capturedSymbol) || symbolHasStableValue(capturedSymbol, scopes)) continue;
|
|
17407
|
+
const capturedKey = computeDepKey(capturedReference);
|
|
17408
|
+
if (!capturedKey) return null;
|
|
17409
|
+
if (capturedKey === capturedSymbol.name) {
|
|
17410
|
+
const nestedSourceKeys = resolveReactiveIdentitySourceKeys(capturedSymbol, scopes);
|
|
17411
|
+
if (nestedSourceKeys) {
|
|
17412
|
+
for (const nestedSourceKey of nestedSourceKeys) sourceKeys.add(nestedSourceKey);
|
|
17413
|
+
continue;
|
|
17414
|
+
}
|
|
17415
|
+
}
|
|
17416
|
+
sourceKeys.add(capturedKey);
|
|
17417
|
+
}
|
|
17418
|
+
return sourceKeys.size > 0 ? sourceKeys : null;
|
|
17419
|
+
};
|
|
17420
|
+
const mergeDerivedExpressionSourceKeys = (expressions, scopes, visitedSymbolIds) => {
|
|
17421
|
+
const sourceKeys = /* @__PURE__ */ new Set();
|
|
17422
|
+
for (const expression of expressions) {
|
|
17423
|
+
const expressionSourceKeys = resolveDerivedExpressionSourceKeys(expression, scopes, visitedSymbolIds);
|
|
17424
|
+
if (!expressionSourceKeys) return null;
|
|
17425
|
+
for (const expressionSourceKey of expressionSourceKeys) sourceKeys.add(expressionSourceKey);
|
|
17426
|
+
}
|
|
17427
|
+
return sourceKeys;
|
|
17428
|
+
};
|
|
17429
|
+
const resolveDerivedExpressionSourceKeys = (expression, scopes, visitedSymbolIds) => {
|
|
17430
|
+
const candidate = unwrapExpression$3(expression);
|
|
17431
|
+
if (isNodeOfType(candidate, "Literal")) return /* @__PURE__ */ new Set();
|
|
17432
|
+
if (isNodeOfType(candidate, "Identifier")) {
|
|
17433
|
+
if (scopes.isGlobalReference(candidate)) return /* @__PURE__ */ new Set();
|
|
17434
|
+
const sourceSymbol = scopes.symbolFor(candidate);
|
|
17435
|
+
if (!sourceSymbol) return null;
|
|
17436
|
+
if (isOutsideAllFunctions(sourceSymbol) || symbolHasStableValue(sourceSymbol, scopes)) return /* @__PURE__ */ new Set();
|
|
17437
|
+
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)) {
|
|
17438
|
+
visitedSymbolIds.add(sourceSymbol.id);
|
|
17439
|
+
const sourceKeys = resolveDerivedExpressionSourceKeys(sourceSymbol.initializer, scopes, visitedSymbolIds);
|
|
17440
|
+
visitedSymbolIds.delete(sourceSymbol.id);
|
|
17441
|
+
if (sourceKeys) return sourceKeys;
|
|
17442
|
+
}
|
|
17443
|
+
return new Set([sourceSymbol.name]);
|
|
17444
|
+
}
|
|
17445
|
+
if (isNodeOfType(candidate, "MemberExpression")) {
|
|
17446
|
+
if (hasComputedMemberExpression(candidate)) return null;
|
|
17447
|
+
const sourceKey = stringifyMemberChain(candidate);
|
|
17448
|
+
const rootIdentifier = getMemberRootIdentifier(candidate);
|
|
17449
|
+
const rootSymbol = rootIdentifier ? scopes.symbolFor(rootIdentifier) : null;
|
|
17450
|
+
if (!sourceKey || !rootSymbol) return null;
|
|
17451
|
+
if (isOutsideAllFunctions(rootSymbol) || symbolHasStableValue(rootSymbol, scopes)) return /* @__PURE__ */ new Set();
|
|
17452
|
+
return new Set([sourceKey]);
|
|
17453
|
+
}
|
|
17454
|
+
if (isNodeOfType(candidate, "BinaryExpression") || isNodeOfType(candidate, "LogicalExpression")) return mergeDerivedExpressionSourceKeys([candidate.left, candidate.right], scopes, visitedSymbolIds);
|
|
17455
|
+
if (isNodeOfType(candidate, "UnaryExpression") && candidate.operator !== "delete") return resolveDerivedExpressionSourceKeys(candidate.argument, scopes, visitedSymbolIds);
|
|
17456
|
+
if (isNodeOfType(candidate, "ConditionalExpression")) return mergeDerivedExpressionSourceKeys([
|
|
17457
|
+
candidate.test,
|
|
17458
|
+
candidate.consequent,
|
|
17459
|
+
candidate.alternate
|
|
17460
|
+
], scopes, visitedSymbolIds);
|
|
17461
|
+
if (isNodeOfType(candidate, "TemplateLiteral")) return mergeDerivedExpressionSourceKeys(candidate.expressions, scopes, visitedSymbolIds);
|
|
17462
|
+
if (isNodeOfType(candidate, "NewExpression")) {
|
|
17463
|
+
const callee = unwrapExpression$3(candidate.callee);
|
|
17464
|
+
if (!isNodeOfType(callee, "Identifier") || callee.name !== "Error" || !scopes.isGlobalReference(callee)) return null;
|
|
17465
|
+
const argumentsToAnalyze = [];
|
|
17466
|
+
for (const argument of candidate.arguments) {
|
|
17467
|
+
if (!isAstNode(argument) || isNodeOfType(argument, "SpreadElement")) return null;
|
|
17468
|
+
argumentsToAnalyze.push(argument);
|
|
17469
|
+
}
|
|
17470
|
+
return mergeDerivedExpressionSourceKeys(argumentsToAnalyze, scopes, visitedSymbolIds);
|
|
17471
|
+
}
|
|
17472
|
+
return null;
|
|
17473
|
+
};
|
|
17474
|
+
const resolveWriteControlSourceKeys = (assignment, boundaryFunction, scopes) => {
|
|
17475
|
+
const sourceKeys = /* @__PURE__ */ new Set();
|
|
17476
|
+
let currentNode = assignment;
|
|
17477
|
+
while (currentNode.parent && currentNode.parent !== boundaryFunction) {
|
|
17478
|
+
const parentNode = currentNode.parent;
|
|
17479
|
+
if (isNodeOfType(parentNode, "IfStatement")) {
|
|
17480
|
+
if (parentNode.test === currentNode) return null;
|
|
17481
|
+
const testSourceKeys = resolveDerivedExpressionSourceKeys(parentNode.test, scopes, /* @__PURE__ */ new Set());
|
|
17482
|
+
if (!testSourceKeys) return null;
|
|
17483
|
+
for (const testSourceKey of testSourceKeys) sourceKeys.add(testSourceKey);
|
|
17484
|
+
} else if (!isNodeOfType(parentNode, "ExpressionStatement") && !isNodeOfType(parentNode, "BlockStatement")) return null;
|
|
17485
|
+
currentNode = parentNode;
|
|
17486
|
+
}
|
|
17487
|
+
return currentNode.parent === boundaryFunction ? sourceKeys : null;
|
|
17488
|
+
};
|
|
17489
|
+
const isReadOnlyInitialStateUse = (referenceNode, scopes) => {
|
|
17490
|
+
const referenceRoot = findTransparentExpressionRoot(referenceNode);
|
|
17491
|
+
const callExpression = referenceRoot.parent;
|
|
17492
|
+
return isNodeOfType(callExpression, "CallExpression") && callExpression.arguments.some((argument) => argument === referenceRoot) && isReactApiCall(callExpression, "useState", scopes, {
|
|
17493
|
+
allowGlobalReactNamespace: true,
|
|
17494
|
+
allowUnboundBareCalls: true,
|
|
17495
|
+
resolveNamedAliases: true
|
|
17496
|
+
});
|
|
17497
|
+
};
|
|
17498
|
+
const resolveRenderDerivedMutableSourceKeys = (capturedReference, symbol, scopes) => {
|
|
17499
|
+
if (symbol.kind !== "let" || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return null;
|
|
17500
|
+
const boundaryFunction = findEnclosingFunction$1(symbol.bindingIdentifier);
|
|
17501
|
+
if (!boundaryFunction) return null;
|
|
17502
|
+
const capturingFunction = findEnclosingFunction$1(capturedReference.identifier);
|
|
17503
|
+
if (!capturingFunction || capturingFunction === boundaryFunction) return null;
|
|
17504
|
+
const sourceKeys = /* @__PURE__ */ new Set();
|
|
17505
|
+
if (symbol.initializer) {
|
|
17506
|
+
const initializerSourceKeys = resolveDerivedExpressionSourceKeys(symbol.initializer, scopes, new Set([symbol.id]));
|
|
17507
|
+
if (!initializerSourceKeys) return null;
|
|
17508
|
+
for (const initializerSourceKey of initializerSourceKeys) sourceKeys.add(initializerSourceKey);
|
|
17509
|
+
}
|
|
17510
|
+
let writeCount = 0;
|
|
17511
|
+
for (const symbolReference of symbol.references) {
|
|
17512
|
+
if (symbolReference.flag === "read") {
|
|
17513
|
+
if (findEnclosingFunction$1(symbolReference.identifier) !== capturingFunction && !isReadOnlyInitialStateUse(symbolReference.identifier, scopes)) return null;
|
|
17514
|
+
continue;
|
|
17515
|
+
}
|
|
17516
|
+
if (symbolReference.flag !== "write") return null;
|
|
17517
|
+
const referenceRoot = findTransparentExpressionRoot(symbolReference.identifier);
|
|
17518
|
+
const assignment = referenceRoot.parent;
|
|
17519
|
+
if (!isNodeOfType(assignment, "AssignmentExpression") || assignment.operator !== "=" || assignment.left !== referenceRoot || findEnclosingFunction$1(referenceRoot) !== boundaryFunction) return null;
|
|
17520
|
+
const assignmentSourceKeys = resolveDerivedExpressionSourceKeys(assignment.right, scopes, new Set([symbol.id]));
|
|
17521
|
+
const controlSourceKeys = resolveWriteControlSourceKeys(assignment, boundaryFunction, scopes);
|
|
17522
|
+
if (!assignmentSourceKeys || !controlSourceKeys) return null;
|
|
17523
|
+
for (const assignmentSourceKey of assignmentSourceKeys) sourceKeys.add(assignmentSourceKey);
|
|
17524
|
+
for (const controlSourceKey of controlSourceKeys) sourceKeys.add(controlSourceKey);
|
|
17525
|
+
writeCount += 1;
|
|
17526
|
+
}
|
|
17527
|
+
return writeCount > 0 && sourceKeys.size > 0 ? sourceKeys : null;
|
|
17528
|
+
};
|
|
16963
17529
|
const isUseCallbackResultDep = (node, scopes) => {
|
|
16964
17530
|
const rootSymbol = getRootSymbol(node, scopes);
|
|
16965
17531
|
const initializer = rootSymbol?.initializer ? unwrapExpression$3(rootSymbol.initializer) : null;
|
|
@@ -27251,7 +27817,11 @@ const mouseEventsHaveKeyEvents = defineRule({
|
|
|
27251
27817
|
//#region src/plugin/utils/has-directive.ts
|
|
27252
27818
|
const hasDirective = (programNode, directive) => {
|
|
27253
27819
|
if (!isNodeOfType(programNode, "Program")) return false;
|
|
27254
|
-
|
|
27820
|
+
for (const statement of programNode.body) {
|
|
27821
|
+
if (!isNodeOfType(statement, "ExpressionStatement") || statement.directive === void 0) return false;
|
|
27822
|
+
if (statement.directive === directive) return true;
|
|
27823
|
+
}
|
|
27824
|
+
return false;
|
|
27255
27825
|
};
|
|
27256
27826
|
//#endregion
|
|
27257
27827
|
//#region src/plugin/rules/nextjs/nextjs-async-client-component.ts
|
|
@@ -29183,7 +29753,7 @@ const nextjsNoVercelOgImport = defineRule({
|
|
|
29183
29753
|
//#endregion
|
|
29184
29754
|
//#region src/plugin/rules/a11y/no-access-key.ts
|
|
29185
29755
|
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";
|
|
29756
|
+
const isUndefinedIdentifier$1 = (expression) => isNodeOfType(expression, "Identifier") && expression.name === "undefined";
|
|
29187
29757
|
const noAccessKey = defineRule({
|
|
29188
29758
|
id: "no-access-key",
|
|
29189
29759
|
title: "accessKey attribute used",
|
|
@@ -29208,7 +29778,7 @@ const noAccessKey = defineRule({
|
|
|
29208
29778
|
if (isNodeOfType(attributeValue, "JSXExpressionContainer")) {
|
|
29209
29779
|
const expression = attributeValue.expression;
|
|
29210
29780
|
if (!expression || expression.type === "JSXEmptyExpression") return;
|
|
29211
|
-
if (isUndefinedIdentifier(expression)) return;
|
|
29781
|
+
if (isUndefinedIdentifier$1(expression)) return;
|
|
29212
29782
|
context.report({
|
|
29213
29783
|
node: accessKey,
|
|
29214
29784
|
message: MESSAGE$39
|
|
@@ -29973,6 +30543,12 @@ const isReactNamespaceImportReference = (ref) => Boolean(ref?.resolved?.defs.som
|
|
|
29973
30543
|
const importDeclaration = declarationNode.parent;
|
|
29974
30544
|
return Boolean(importDeclaration && isNodeOfType(importDeclaration, "ImportDeclaration") && isNodeOfType(importDeclaration.source, "Literal") && importDeclaration.source.value === "react");
|
|
29975
30545
|
}));
|
|
30546
|
+
const isReactNamespaceReceiver = (analysis, node) => {
|
|
30547
|
+
const receiver = stripParenExpression(node);
|
|
30548
|
+
if (!isNodeOfType(receiver, "Identifier")) return false;
|
|
30549
|
+
const namespaceReference = getRef(analysis, receiver);
|
|
30550
|
+
return namespaceReference?.resolved ? isReactNamespaceImportReference(namespaceReference) : receiver.name === "React";
|
|
30551
|
+
};
|
|
29976
30552
|
const isGenuineReactHookDeclarator = (analysis, declarator, hookName) => {
|
|
29977
30553
|
if (!isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.init, "CallExpression")) return false;
|
|
29978
30554
|
const callee = stripParenExpression(declarator.init.callee);
|
|
@@ -29981,24 +30557,20 @@ const isGenuineReactHookDeclarator = (analysis, declarator, hookName) => {
|
|
|
29981
30557
|
if (!reference?.resolved) return callee.name === hookName;
|
|
29982
30558
|
return isReactNamedImportReference(reference, hookName);
|
|
29983
30559
|
}
|
|
29984
|
-
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.
|
|
29985
|
-
|
|
29986
|
-
if (!namespaceReference?.resolved) return callee.object.name === "React";
|
|
29987
|
-
return isReactNamespaceImportReference(namespaceReference);
|
|
30560
|
+
if (!isNodeOfType(callee, "MemberExpression") || callee.computed || !isNodeOfType(callee.property, "Identifier") || callee.property.name !== hookName) return false;
|
|
30561
|
+
return isReactNamespaceReceiver(analysis, callee.object);
|
|
29988
30562
|
};
|
|
29989
30563
|
const isHookCallee$1 = (analysis, node, hookName) => {
|
|
29990
30564
|
if (!node) return false;
|
|
29991
30565
|
if (isNodeOfType(node, "Identifier")) {
|
|
29992
30566
|
if (node.name === hookName) return true;
|
|
29993
30567
|
if (isReactNamedImportReference(getRef(analysis, node), hookName)) return true;
|
|
29994
|
-
const
|
|
29995
|
-
|
|
30568
|
+
const receiverRoot = findTransparentExpressionRoot(node);
|
|
30569
|
+
const parent = receiverRoot.parent;
|
|
30570
|
+
if (parent && isNodeOfType(parent, "MemberExpression") && parent.object === receiverRoot && isReactNamespaceReceiver(analysis, node) && isNodeOfType(parent.property, "Identifier") && parent.property.name === hookName) return true;
|
|
29996
30571
|
return false;
|
|
29997
30572
|
}
|
|
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
|
-
}
|
|
30573
|
+
if (isNodeOfType(node, "MemberExpression")) return isReactNamespaceReceiver(analysis, node.object) && isNodeOfType(node.property, "Identifier") && node.property.name === hookName;
|
|
30002
30574
|
return false;
|
|
30003
30575
|
};
|
|
30004
30576
|
const isUseEffect = (node) => {
|
|
@@ -30422,7 +30994,88 @@ const isIndependentWriterIdentifier = (componentFunction, identifier, includeDef
|
|
|
30422
30994
|
if (HANDLER_BINDING_NAME_PATTERN.test(bindingName)) return true;
|
|
30423
30995
|
return isSetterWiredToJsxHandler(componentFunction, bindingName);
|
|
30424
30996
|
};
|
|
30425
|
-
const
|
|
30997
|
+
const isSynchronousFunction = (functionNode) => {
|
|
30998
|
+
const functionMetadata = functionNode;
|
|
30999
|
+
return functionMetadata.async !== true && functionMetadata.generator !== true;
|
|
31000
|
+
};
|
|
31001
|
+
const findBindingVariable = (analysis, bindingIdentifier) => {
|
|
31002
|
+
for (const scope of analysis.scopeManager.scopes) for (const variable of scope.variables) if (variable.identifiers.includes(bindingIdentifier)) return variable;
|
|
31003
|
+
return null;
|
|
31004
|
+
};
|
|
31005
|
+
const getImmutableFunctionVariable = (analysis, componentFunction, functionNode) => {
|
|
31006
|
+
if (!isSynchronousFunction(functionNode) || !isAstDescendant(functionNode, componentFunction)) return null;
|
|
31007
|
+
const bindingIdentifier = getFunctionBindingIdentifier$1(functionNode);
|
|
31008
|
+
if (!bindingIdentifier) return null;
|
|
31009
|
+
const variable = findBindingVariable(analysis, bindingIdentifier);
|
|
31010
|
+
if (!variable || variable.defs.length !== 1 || variable.references.some((reference) => reference.isWrite() && !reference.init)) return null;
|
|
31011
|
+
const definition = variable.defs[0];
|
|
31012
|
+
if (definition.type === "FunctionName") return definition.node === functionNode ? variable : null;
|
|
31013
|
+
if (definition.type !== "Variable") return null;
|
|
31014
|
+
const declarator = definition.node;
|
|
31015
|
+
if (!isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.parent, "VariableDeclaration") || declarator.parent.kind !== "const") return null;
|
|
31016
|
+
if (declarator.init === functionNode) return variable;
|
|
31017
|
+
if (isNodeOfType(declarator.init, "CallExpression") && declarator.init.arguments?.[0] === functionNode && isGenuineReactHookDeclarator(analysis, declarator, "useCallback")) return variable;
|
|
31018
|
+
return null;
|
|
31019
|
+
};
|
|
31020
|
+
const getJsxEventValueAttribute = (identifier) => {
|
|
31021
|
+
const expression = findTransparentExpressionRoot(identifier);
|
|
31022
|
+
const expressionContainer = expression.parent;
|
|
31023
|
+
if (!isNodeOfType(expressionContainer, "JSXExpressionContainer") || expressionContainer.expression !== expression) return null;
|
|
31024
|
+
const attribute = expressionContainer.parent;
|
|
31025
|
+
if (!isNodeOfType(attribute, "JSXAttribute")) return null;
|
|
31026
|
+
const attributeName = getJsxAttributeName(attribute.name);
|
|
31027
|
+
return attributeName && isEventHandlerName(attributeName) ? attribute : null;
|
|
31028
|
+
};
|
|
31029
|
+
const getInlineJsxEventCallbackAttribute = (callExpression) => {
|
|
31030
|
+
const callbackFunction = findEnclosingFunction$1(callExpression);
|
|
31031
|
+
if (!callbackFunction || !isSynchronousFunction(callbackFunction)) return null;
|
|
31032
|
+
return getJsxEventValueAttribute(callbackFunction);
|
|
31033
|
+
};
|
|
31034
|
+
const isReactHookDependencyReference = (identifier) => {
|
|
31035
|
+
const expression = findTransparentExpressionRoot(identifier);
|
|
31036
|
+
const dependencyArray = expression.parent;
|
|
31037
|
+
if (!isNodeOfType(dependencyArray, "ArrayExpression") || !(dependencyArray.elements ?? []).includes(expression)) return false;
|
|
31038
|
+
const hookCall = dependencyArray.parent;
|
|
31039
|
+
if (!isNodeOfType(hookCall, "CallExpression") || hookCall.arguments?.[1] !== dependencyArray) return false;
|
|
31040
|
+
const callee = hookCall.callee;
|
|
31041
|
+
if (isNodeOfType(callee, "Identifier")) return /^use[A-Z0-9]/.test(callee.name);
|
|
31042
|
+
return Boolean(isNodeOfType(callee, "MemberExpression") && !callee.computed && isNodeOfType(callee.property, "Identifier") && /^use[A-Z0-9]/.test(callee.property.name));
|
|
31043
|
+
};
|
|
31044
|
+
const hasReachableJsxEventCallPath = (analysis, context, componentFunction, functionVariable, visitedVariables) => {
|
|
31045
|
+
if (visitedVariables.has(functionVariable)) return false;
|
|
31046
|
+
const nextVisitedVariables = new Set(visitedVariables).add(functionVariable);
|
|
31047
|
+
const callExpressions = [];
|
|
31048
|
+
let hasDirectJsxEventReference = false;
|
|
31049
|
+
for (const reference of functionVariable.references) {
|
|
31050
|
+
if (reference.init) continue;
|
|
31051
|
+
const identifier = reference.identifier;
|
|
31052
|
+
if (reference.isWrite()) return false;
|
|
31053
|
+
const jsxEventValueAttribute = getJsxEventValueAttribute(identifier);
|
|
31054
|
+
if (jsxEventValueAttribute) {
|
|
31055
|
+
if (isNodeReachableWithinFunction(jsxEventValueAttribute, context)) hasDirectJsxEventReference = true;
|
|
31056
|
+
continue;
|
|
31057
|
+
}
|
|
31058
|
+
if (isReactHookDependencyReference(identifier)) continue;
|
|
31059
|
+
const callExpression = getCallExpr(reference);
|
|
31060
|
+
if (!callExpression) return false;
|
|
31061
|
+
const jsxEventCallbackAttribute = getInlineJsxEventCallbackAttribute(callExpression);
|
|
31062
|
+
if (jsxEventCallbackAttribute) {
|
|
31063
|
+
if (isNodeReachableWithinFunction(callExpression, context) && isNodeReachableWithinFunction(jsxEventCallbackAttribute, context)) hasDirectJsxEventReference = true;
|
|
31064
|
+
continue;
|
|
31065
|
+
}
|
|
31066
|
+
callExpressions.push(callExpression);
|
|
31067
|
+
}
|
|
31068
|
+
if (hasDirectJsxEventReference) return true;
|
|
31069
|
+
for (const callExpression of callExpressions) {
|
|
31070
|
+
if (!isNodeReachableWithinFunction(callExpression, context)) continue;
|
|
31071
|
+
const callerFunction = findEnclosingFunction$1(callExpression);
|
|
31072
|
+
if (!callerFunction || callerFunction === componentFunction) continue;
|
|
31073
|
+
const callerVariable = getImmutableFunctionVariable(analysis, componentFunction, callerFunction);
|
|
31074
|
+
if (callerVariable && hasReachableJsxEventCallPath(analysis, context, componentFunction, callerVariable, nextVisitedVariables)) return true;
|
|
31075
|
+
}
|
|
31076
|
+
return false;
|
|
31077
|
+
};
|
|
31078
|
+
const hasUserInputSetterWriter = (analysis, context, setterRef, effectNode, includeDeferredWriters = false) => {
|
|
30426
31079
|
if (!setterRef.resolved) return false;
|
|
30427
31080
|
const componentFunction = findEnclosingFunction$1(effectNode);
|
|
30428
31081
|
if (!componentFunction) return false;
|
|
@@ -30431,6 +31084,11 @@ const hasUserInputSetterWriter = (setterRef, effectNode, includeDeferredWriters
|
|
|
30431
31084
|
const identifier = reference.identifier;
|
|
30432
31085
|
if (isAstDescendant(identifier, effectNode)) continue;
|
|
30433
31086
|
if (isIndependentWriterIdentifier(componentFunction, identifier, includeDeferredWriters)) return true;
|
|
31087
|
+
if (!isNodeReachableWithinFunction(identifier, context)) continue;
|
|
31088
|
+
const writerFunction = findEnclosingFunction$1(identifier);
|
|
31089
|
+
if (!writerFunction || writerFunction === componentFunction) continue;
|
|
31090
|
+
const writerVariable = getImmutableFunctionVariable(analysis, componentFunction, writerFunction);
|
|
31091
|
+
if (writerVariable && hasReachableJsxEventCallPath(analysis, context, componentFunction, writerVariable, /* @__PURE__ */ new Set())) return true;
|
|
30434
31092
|
}
|
|
30435
31093
|
return false;
|
|
30436
31094
|
};
|
|
@@ -31320,7 +31978,7 @@ const areInMutuallyExclusiveBranches = (leftNode, rightNode) => {
|
|
|
31320
31978
|
}
|
|
31321
31979
|
return false;
|
|
31322
31980
|
};
|
|
31323
|
-
const collectEffectStateWriteFacts = (analysis, effectNode, currentFilename) => {
|
|
31981
|
+
const collectEffectStateWriteFacts = (analysis, context, effectNode, currentFilename) => {
|
|
31324
31982
|
const frames = collectBoundedEffectExecutionFrames(analysis, effectNode, currentFilename);
|
|
31325
31983
|
if (frames.length === 0) return [];
|
|
31326
31984
|
const effectHasCleanup = hasCleanup(analysis, effectNode);
|
|
@@ -31350,7 +32008,7 @@ const collectEffectStateWriteFacts = (analysis, effectNode, currentFilename) =>
|
|
|
31350
32008
|
for (const returnedExpression of returnedExpressions) mergeEvidence(valueEvidence, collectValueEvidence(analysis, returnedExpression, updaterFrame, remainingValueCallFrames));
|
|
31351
32009
|
} else valueEvidence = collectValueEvidence(analysis, writtenValue, frame, remainingValueCallFrames);
|
|
31352
32010
|
const sourceReferences = [...valueEvidence.sourceReferences].filter((sourceReference) => getUseStateDecl(analysis, sourceReference) !== stateDeclarator);
|
|
31353
|
-
const hasIndependentWriter = hasUserInputSetterWriter(setterReference, effectNode, true);
|
|
32011
|
+
const hasIndependentWriter = hasUserInputSetterWriter(analysis, context, setterReference, effectNode, true);
|
|
31354
32012
|
const doesMatchStateInitializer = matchesStateInitializer(analysis, callExpression, stateDeclarator);
|
|
31355
32013
|
if (effectHasCleanup && (frame.isDeferred || valueEvidence.hasUnknownSource || valueEvidence.hasDeferredIntroducedValue || valueEvidence.readsExternalValue)) cleanupManagedStateDeclarators.add(stateDeclarator);
|
|
31356
32014
|
const isRenderKnownCopy = sourceReferences.length > 0 && !frame.isDeferred && !valueEvidence.hasUnknownSource && !valueEvidence.hasDeferredIntroducedValue && !valueEvidence.readsExternalValue && !hasIndependentWriter;
|
|
@@ -31391,7 +32049,7 @@ const noAdjustStateOnPropChange = defineRule({
|
|
|
31391
32049
|
const dependencyReferences = getEffectDepsRefs(analysis, node);
|
|
31392
32050
|
if (!dependencyReferences) return;
|
|
31393
32051
|
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)) {
|
|
32052
|
+
for (const fact of collectEffectStateWriteFacts(analysis, context, node, context.filename)) {
|
|
31395
32053
|
if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
|
|
31396
32054
|
context.report({
|
|
31397
32055
|
node: fact.callExpression,
|
|
@@ -34078,6 +34736,7 @@ const noChainStateUpdates = defineRule({
|
|
|
34078
34736
|
id: "no-chain-state-updates",
|
|
34079
34737
|
title: "State updates chained through effects",
|
|
34080
34738
|
severity: "warn",
|
|
34739
|
+
disabledWhen: ["react:18"],
|
|
34081
34740
|
tags: ["test-noise"],
|
|
34082
34741
|
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
34742
|
create: (context) => ({ CallExpression(node) {
|
|
@@ -35964,7 +36623,7 @@ const noDerivedState = defineRule({
|
|
|
35964
36623
|
if (!isUseEffect(node)) return;
|
|
35965
36624
|
const analysis = getProgramAnalysis(node);
|
|
35966
36625
|
if (!analysis) return;
|
|
35967
|
-
for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
|
|
36626
|
+
for (const fact of collectEffectStateWriteFacts(analysis, context, node, context.filename)) {
|
|
35968
36627
|
if (!fact.isRenderKnownCopy || fact.resetsSourceState) continue;
|
|
35969
36628
|
reportStateWrite(fact.callExpression, fact.stateDeclarator);
|
|
35970
36629
|
}
|
|
@@ -35984,7 +36643,7 @@ const noDerivedStateEffect = defineRule({
|
|
|
35984
36643
|
if (!isHookCall$2(node, EFFECT_HOOK_NAMES$1)) return;
|
|
35985
36644
|
const analysis = getProgramAnalysis(node);
|
|
35986
36645
|
if (!analysis) return;
|
|
35987
|
-
if (!collectEffectStateWriteFacts(analysis, node, context.filename).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
|
|
36646
|
+
if (!collectEffectStateWriteFacts(analysis, context, node, context.filename).find((fact) => fact.isRenderKnownCopy && !fact.resetsSourceState)) return;
|
|
35988
36647
|
context.report({
|
|
35989
36648
|
node,
|
|
35990
36649
|
message: "You pay an extra render for state you can derive from other values."
|
|
@@ -36460,9 +37119,20 @@ const noDidMountSetState = defineRule({
|
|
|
36460
37119
|
}
|
|
36461
37120
|
});
|
|
36462
37121
|
//#endregion
|
|
37122
|
+
//#region src/plugin/utils/find-enclosing-class.ts
|
|
37123
|
+
const findEnclosingClass = (node) => {
|
|
37124
|
+
let ancestor = node.parent;
|
|
37125
|
+
while (ancestor) {
|
|
37126
|
+
if (isNodeOfType(ancestor, "ClassDeclaration") || isNodeOfType(ancestor, "ClassExpression")) return ancestor;
|
|
37127
|
+
ancestor = ancestor.parent ?? null;
|
|
37128
|
+
}
|
|
37129
|
+
return null;
|
|
37130
|
+
};
|
|
37131
|
+
//#endregion
|
|
36463
37132
|
//#region src/plugin/rules/react-builtins/no-did-update-set-state.ts
|
|
36464
37133
|
const LIFECYCLE_NAMES$1 = new Set(["componentDidUpdate"]);
|
|
36465
37134
|
const MESSAGE$27 = "Calling setState in componentDidUpdate can trigger another update immediately, loop forever, and freeze the component.";
|
|
37135
|
+
const DIFFERENCE_OPERATORS = new Set(["!=", "!=="]);
|
|
36466
37136
|
const EQUALITY_OPERATORS = new Set([
|
|
36467
37137
|
"==",
|
|
36468
37138
|
"===",
|
|
@@ -36474,6 +37144,8 @@ const FUNCTION_NODE_TYPES = new Set([
|
|
|
36474
37144
|
"FunctionExpression",
|
|
36475
37145
|
"ArrowFunctionExpression"
|
|
36476
37146
|
]);
|
|
37147
|
+
const CLASS_NODE_TYPES = new Set(["ClassDeclaration", "ClassExpression"]);
|
|
37148
|
+
const callbackRefFieldNamesByClass = /* @__PURE__ */ new WeakMap();
|
|
36477
37149
|
const isLifecycleMethodFunction = (node) => {
|
|
36478
37150
|
if (!FUNCTION_NODE_TYPES.has(node.type)) return false;
|
|
36479
37151
|
const parent = node.parent;
|
|
@@ -36529,6 +37201,187 @@ const getStaticMemberName = (node) => {
|
|
|
36529
37201
|
if (!isNodeOfType(node, "MemberExpression") || node.computed === true) return null;
|
|
36530
37202
|
return isNodeOfType(node.property, "Identifier") ? node.property.name : null;
|
|
36531
37203
|
};
|
|
37204
|
+
const getMemberIdentity = (property) => {
|
|
37205
|
+
const propertyName = getPropertyKeyName$2(property);
|
|
37206
|
+
if (propertyName !== void 0) return isNodeOfType(property, "PrivateIdentifier") ? `#${propertyName}` : propertyName;
|
|
37207
|
+
return isNodeOfType(property, "Literal") && typeof property.value === "string" ? property.value : null;
|
|
37208
|
+
};
|
|
37209
|
+
const collectPreviousSourcePaths = (pattern, domain, members, previousSourcePaths) => {
|
|
37210
|
+
if (!pattern) return;
|
|
37211
|
+
const unwrappedPattern = stripParenExpression(pattern);
|
|
37212
|
+
if (isNodeOfType(unwrappedPattern, "Identifier")) {
|
|
37213
|
+
previousSourcePaths.set(unwrappedPattern.name, {
|
|
37214
|
+
domain,
|
|
37215
|
+
members: [...members],
|
|
37216
|
+
source: "previous"
|
|
37217
|
+
});
|
|
37218
|
+
return;
|
|
37219
|
+
}
|
|
37220
|
+
if (isNodeOfType(unwrappedPattern, "AssignmentPattern")) {
|
|
37221
|
+
collectPreviousSourcePaths(unwrappedPattern.left, domain, members, previousSourcePaths);
|
|
37222
|
+
return;
|
|
37223
|
+
}
|
|
37224
|
+
if (!isNodeOfType(unwrappedPattern, "ObjectPattern")) return;
|
|
37225
|
+
for (const property of unwrappedPattern.properties) {
|
|
37226
|
+
if (!isNodeOfType(property, "Property")) continue;
|
|
37227
|
+
const propertyName = getStaticPropertyKeyName(property, { allowComputedString: true });
|
|
37228
|
+
if (!propertyName) continue;
|
|
37229
|
+
collectPreviousSourcePaths(property.value, domain, [...members, propertyName], previousSourcePaths);
|
|
37230
|
+
}
|
|
37231
|
+
};
|
|
37232
|
+
const getStateSourcePath = (node, previousSourcePaths) => {
|
|
37233
|
+
let currentNode = stripParenExpression(node);
|
|
37234
|
+
const members = [];
|
|
37235
|
+
while (isNodeOfType(currentNode, "MemberExpression")) {
|
|
37236
|
+
const memberName = getStaticMemberName(currentNode);
|
|
37237
|
+
if (!memberName) return null;
|
|
37238
|
+
members.unshift(memberName);
|
|
37239
|
+
currentNode = stripParenExpression(currentNode.object);
|
|
37240
|
+
}
|
|
37241
|
+
if (isNodeOfType(currentNode, "ThisExpression")) {
|
|
37242
|
+
const [domain, ...pathMembers] = members;
|
|
37243
|
+
if (domain !== "props" && domain !== "state") return null;
|
|
37244
|
+
return {
|
|
37245
|
+
domain,
|
|
37246
|
+
members: pathMembers,
|
|
37247
|
+
source: "current"
|
|
37248
|
+
};
|
|
37249
|
+
}
|
|
37250
|
+
if (!isNodeOfType(currentNode, "Identifier")) return null;
|
|
37251
|
+
const previousSourcePath = previousSourcePaths.get(currentNode.name);
|
|
37252
|
+
return previousSourcePath ? {
|
|
37253
|
+
...previousSourcePath,
|
|
37254
|
+
members: [...previousSourcePath.members, ...members]
|
|
37255
|
+
} : null;
|
|
37256
|
+
};
|
|
37257
|
+
const haveMatchingStateSourcePaths = (left, right) => left.domain === right.domain && left.members.length === right.members.length && left.members.every((member, index) => member === right.members[index]);
|
|
37258
|
+
const collectConjunctiveStateSourceComparisons = (test, previousSourcePaths, comparisons) => {
|
|
37259
|
+
const expression = stripParenExpression(test);
|
|
37260
|
+
if (isNodeOfType(expression, "LogicalExpression") && expression.operator === "&&") {
|
|
37261
|
+
collectConjunctiveStateSourceComparisons(expression.left, previousSourcePaths, comparisons);
|
|
37262
|
+
collectConjunctiveStateSourceComparisons(expression.right, previousSourcePaths, comparisons);
|
|
37263
|
+
return;
|
|
37264
|
+
}
|
|
37265
|
+
if (!isNodeOfType(expression, "BinaryExpression") || !EQUALITY_OPERATORS.has(expression.operator)) return;
|
|
37266
|
+
const leftPath = getStateSourcePath(expression.left, previousSourcePaths);
|
|
37267
|
+
const rightPath = getStateSourcePath(expression.right, previousSourcePaths);
|
|
37268
|
+
if (Boolean(leftPath) === Boolean(rightPath)) return;
|
|
37269
|
+
const path = leftPath ?? rightPath;
|
|
37270
|
+
if (!path) return;
|
|
37271
|
+
comparisons.push({
|
|
37272
|
+
comparedValue: leftPath ? expression.right : expression.left,
|
|
37273
|
+
isDifference: DIFFERENCE_OPERATORS.has(expression.operator),
|
|
37274
|
+
path
|
|
37275
|
+
});
|
|
37276
|
+
};
|
|
37277
|
+
const isHistoricalToCurrentTransitionGuard = (test, previousSourcePaths) => {
|
|
37278
|
+
const expression = stripParenExpression(test);
|
|
37279
|
+
if (isNodeOfType(expression, "LogicalExpression") && expression.operator === "||") return isHistoricalToCurrentTransitionGuard(expression.left, previousSourcePaths) && isHistoricalToCurrentTransitionGuard(expression.right, previousSourcePaths);
|
|
37280
|
+
const comparisons = [];
|
|
37281
|
+
collectConjunctiveStateSourceComparisons(expression, previousSourcePaths, comparisons);
|
|
37282
|
+
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)));
|
|
37283
|
+
};
|
|
37284
|
+
const getThisFieldName = (node) => {
|
|
37285
|
+
const unwrappedNode = stripParenExpression(node);
|
|
37286
|
+
if (!isNodeOfType(unwrappedNode, "MemberExpression") || unwrappedNode.computed === true || !isNodeOfType(stripParenExpression(unwrappedNode.object), "ThisExpression")) return null;
|
|
37287
|
+
return getMemberIdentity(unwrappedNode.property);
|
|
37288
|
+
};
|
|
37289
|
+
const isUndefinedIdentifier = (node) => {
|
|
37290
|
+
const unwrappedNode = stripParenExpression(node);
|
|
37291
|
+
return isNodeOfType(unwrappedNode, "Identifier") && unwrappedNode.name === "undefined";
|
|
37292
|
+
};
|
|
37293
|
+
const isDirectRefParameterValue = (node, parameterSymbolId, scopes) => {
|
|
37294
|
+
const unwrappedNode = stripParenExpression(node);
|
|
37295
|
+
if (isNodeOfType(unwrappedNode, "Identifier")) return scopes.symbolFor(unwrappedNode)?.id === parameterSymbolId;
|
|
37296
|
+
if (!isNodeOfType(unwrappedNode, "LogicalExpression") || unwrappedNode.operator !== "??") return false;
|
|
37297
|
+
const left = stripParenExpression(unwrappedNode.left);
|
|
37298
|
+
return isNodeOfType(left, "Identifier") && scopes.symbolFor(left)?.id === parameterSymbolId && isUndefinedIdentifier(unwrappedNode.right);
|
|
37299
|
+
};
|
|
37300
|
+
const getCallbackRefAssignedFields = (callback, scopes) => {
|
|
37301
|
+
const firstParameter = (callback.params ?? [])[0];
|
|
37302
|
+
if (!firstParameter) return /* @__PURE__ */ new Set();
|
|
37303
|
+
const parameterIdentifier = isNodeOfType(firstParameter, "AssignmentPattern") ? firstParameter.left : firstParameter;
|
|
37304
|
+
if (!isNodeOfType(parameterIdentifier, "Identifier")) return /* @__PURE__ */ new Set();
|
|
37305
|
+
const parameterSymbolId = scopes.symbolFor(parameterIdentifier)?.id;
|
|
37306
|
+
if (parameterSymbolId === void 0) return /* @__PURE__ */ new Set();
|
|
37307
|
+
const body = callback.body;
|
|
37308
|
+
if (!body) return /* @__PURE__ */ new Set();
|
|
37309
|
+
const assignedFieldNames = /* @__PURE__ */ new Set();
|
|
37310
|
+
walkAst(body, (node) => {
|
|
37311
|
+
if (node !== body && (FUNCTION_NODE_TYPES.has(node.type) && !isImmediatelyInvokedFunction(node) || CLASS_NODE_TYPES.has(node.type))) return false;
|
|
37312
|
+
const assignmentTarget = isNodeOfType(node, "AssignmentExpression") && node.left || isNodeOfType(node, "UpdateExpression") && node.argument || isNodeOfType(node, "UnaryExpression") && node.operator === "delete" && node.argument || null;
|
|
37313
|
+
if (!assignmentTarget) return;
|
|
37314
|
+
const fieldName = getThisFieldName(assignmentTarget);
|
|
37315
|
+
if (!fieldName) return;
|
|
37316
|
+
if (isNodeOfType(node, "AssignmentExpression") && node.operator === "=" && isDirectRefParameterValue(node.right, parameterSymbolId, scopes)) {
|
|
37317
|
+
assignedFieldNames.add(fieldName);
|
|
37318
|
+
return;
|
|
37319
|
+
}
|
|
37320
|
+
assignedFieldNames.delete(fieldName);
|
|
37321
|
+
});
|
|
37322
|
+
return assignedFieldNames;
|
|
37323
|
+
};
|
|
37324
|
+
const getClassMemberCallback = (classNode, memberName) => {
|
|
37325
|
+
const classBody = classNode.body?.body ?? [];
|
|
37326
|
+
for (const member of classBody) {
|
|
37327
|
+
if (!isNodeOfType(member, "MethodDefinition") && !isNodeOfType(member, "PropertyDefinition")) continue;
|
|
37328
|
+
if (member.static === true) continue;
|
|
37329
|
+
const key = member.key;
|
|
37330
|
+
if (getMemberIdentity(key) !== memberName) continue;
|
|
37331
|
+
const value = member.value;
|
|
37332
|
+
return value && FUNCTION_NODE_TYPES.has(value.type) ? value : null;
|
|
37333
|
+
}
|
|
37334
|
+
return null;
|
|
37335
|
+
};
|
|
37336
|
+
const collectCallbackRefFieldsFromExpression = (expression, classNode, fieldNames, scopes) => {
|
|
37337
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
37338
|
+
if (FUNCTION_NODE_TYPES.has(unwrappedExpression.type)) {
|
|
37339
|
+
for (const fieldName of getCallbackRefAssignedFields(unwrappedExpression, scopes)) fieldNames.add(fieldName);
|
|
37340
|
+
return;
|
|
37341
|
+
}
|
|
37342
|
+
const handlerName = getThisFieldName(unwrappedExpression);
|
|
37343
|
+
if (handlerName) {
|
|
37344
|
+
const callback = getClassMemberCallback(classNode, handlerName);
|
|
37345
|
+
if (callback) for (const fieldName of getCallbackRefAssignedFields(callback, scopes)) fieldNames.add(fieldName);
|
|
37346
|
+
return;
|
|
37347
|
+
}
|
|
37348
|
+
if (isNodeOfType(unwrappedExpression, "ConditionalExpression")) {
|
|
37349
|
+
collectCallbackRefFieldsFromExpression(unwrappedExpression.consequent, classNode, fieldNames, scopes);
|
|
37350
|
+
collectCallbackRefFieldsFromExpression(unwrappedExpression.alternate, classNode, fieldNames, scopes);
|
|
37351
|
+
return;
|
|
37352
|
+
}
|
|
37353
|
+
if (isNodeOfType(unwrappedExpression, "LogicalExpression")) {
|
|
37354
|
+
if (unwrappedExpression.operator !== "&&") collectCallbackRefFieldsFromExpression(unwrappedExpression.left, classNode, fieldNames, scopes);
|
|
37355
|
+
collectCallbackRefFieldsFromExpression(unwrappedExpression.right, classNode, fieldNames, scopes);
|
|
37356
|
+
}
|
|
37357
|
+
};
|
|
37358
|
+
const getCallbackRefFieldNames = (classNode, scopes) => {
|
|
37359
|
+
if (!classNode) return /* @__PURE__ */ new Set();
|
|
37360
|
+
const cachedFieldNames = callbackRefFieldNamesByClass.get(classNode);
|
|
37361
|
+
if (cachedFieldNames) return cachedFieldNames;
|
|
37362
|
+
const fieldNames = /* @__PURE__ */ new Set();
|
|
37363
|
+
const classBody = classNode.body;
|
|
37364
|
+
if (classBody) walkAst(classBody, (node) => {
|
|
37365
|
+
if (node !== classBody && CLASS_NODE_TYPES.has(node.type)) return false;
|
|
37366
|
+
if (!isNodeOfType(node, "JSXAttribute") || !isNodeOfType(node.name, "JSXIdentifier") || node.name.name !== "ref" || !node.value || !isNodeOfType(node.value, "JSXExpressionContainer") || !node.value.expression) return;
|
|
37367
|
+
collectCallbackRefFieldsFromExpression(node.value.expression, classNode, fieldNames, scopes);
|
|
37368
|
+
});
|
|
37369
|
+
callbackRefFieldNamesByClass.set(classNode, fieldNames);
|
|
37370
|
+
return fieldNames;
|
|
37371
|
+
};
|
|
37372
|
+
const collectLifecycleWrittenFieldNames = (lifecycleFunction) => {
|
|
37373
|
+
const fieldNames = /* @__PURE__ */ new Set();
|
|
37374
|
+
const body = lifecycleFunction.body;
|
|
37375
|
+
if (!body) return fieldNames;
|
|
37376
|
+
walkAst(body, (node) => {
|
|
37377
|
+
if (FUNCTION_NODE_TYPES.has(node.type) && !isImmediatelyInvokedFunction(node)) return false;
|
|
37378
|
+
const target = isNodeOfType(node, "AssignmentExpression") && node.left || isNodeOfType(node, "UpdateExpression") && node.argument || null;
|
|
37379
|
+
if (!target) return;
|
|
37380
|
+
const fieldName = getThisFieldName(target);
|
|
37381
|
+
if (fieldName) fieldNames.add(fieldName);
|
|
37382
|
+
});
|
|
37383
|
+
return fieldNames;
|
|
37384
|
+
};
|
|
36532
37385
|
const getThisStateFieldName = (node) => {
|
|
36533
37386
|
const unwrappedNode = stripParenExpression(node);
|
|
36534
37387
|
if (!isNodeOfType(unwrappedNode, "MemberExpression")) return null;
|
|
@@ -36546,15 +37399,17 @@ const collectLocalInitializers = (lifecycleFunction) => {
|
|
|
36546
37399
|
});
|
|
36547
37400
|
return initializers;
|
|
36548
37401
|
};
|
|
36549
|
-
const derivesFromPostMountValue = (node, localInitializers, visitedNames = /* @__PURE__ */ new Set()) => {
|
|
37402
|
+
const derivesFromPostMountValue = (node, localInitializers, callbackRefFieldNames, visitedNames = /* @__PURE__ */ new Set()) => {
|
|
36550
37403
|
if (readsPostMountValue(node)) return true;
|
|
37404
|
+
const fieldName = getThisFieldName(node);
|
|
37405
|
+
if (fieldName && callbackRefFieldNames.has(fieldName)) return true;
|
|
36551
37406
|
const referencedNames = /* @__PURE__ */ new Set();
|
|
36552
37407
|
collectReferenceIdentifierNames(node, referencedNames);
|
|
36553
37408
|
for (const referencedName of referencedNames) {
|
|
36554
37409
|
if (visitedNames.has(referencedName)) continue;
|
|
36555
37410
|
const initializer = localInitializers.get(referencedName);
|
|
36556
37411
|
if (!initializer) continue;
|
|
36557
|
-
if (derivesFromPostMountValue(initializer, localInitializers, new Set([...visitedNames, referencedName]))) return true;
|
|
37412
|
+
if (derivesFromPostMountValue(initializer, localInitializers, callbackRefFieldNames, new Set([...visitedNames, referencedName]))) return true;
|
|
36558
37413
|
}
|
|
36559
37414
|
return false;
|
|
36560
37415
|
};
|
|
@@ -36568,50 +37423,84 @@ const getSetStateFieldValue = (setStateCall, fieldName) => {
|
|
|
36568
37423
|
}
|
|
36569
37424
|
return null;
|
|
36570
37425
|
};
|
|
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;
|
|
37426
|
+
const isConvergentPostMountGuard = (test, setStateCall, localInitializers, callbackRefFieldNames, isTruthfulBranch) => {
|
|
37427
|
+
const expression = stripParenExpression(test);
|
|
37428
|
+
if (isNodeOfType(expression, "LogicalExpression")) {
|
|
37429
|
+
if (expression.operator !== "&&" && expression.operator !== "||") return false;
|
|
37430
|
+
const leftIsConvergent = isConvergentPostMountGuard(expression.left, setStateCall, localInitializers, callbackRefFieldNames, isTruthfulBranch);
|
|
37431
|
+
const rightIsConvergent = isConvergentPostMountGuard(expression.right, setStateCall, localInitializers, callbackRefFieldNames, isTruthfulBranch);
|
|
37432
|
+
return isTruthfulBranch && expression.operator === "||" || !isTruthfulBranch && expression.operator === "&&" ? leftIsConvergent && rightIsConvergent : leftIsConvergent || rightIsConvergent;
|
|
37433
|
+
}
|
|
37434
|
+
if (!isNodeOfType(expression, "BinaryExpression") || !(isTruthfulBranch ? DIFFERENCE_OPERATORS.has(expression.operator) : EQUALITY_OPERATORS.has(expression.operator) && !DIFFERENCE_OPERATORS.has(expression.operator))) return false;
|
|
37435
|
+
const leftFieldName = getThisStateFieldName(expression.left);
|
|
37436
|
+
const rightFieldName = getThisStateFieldName(expression.right);
|
|
37437
|
+
const fieldName = leftFieldName ?? rightFieldName;
|
|
37438
|
+
const comparedValue = leftFieldName ? expression.right : expression.left;
|
|
37439
|
+
if (!fieldName) return false;
|
|
37440
|
+
const assignedValue = getSetStateFieldValue(setStateCall, fieldName);
|
|
37441
|
+
if (!assignedValue || !areExpressionsStructurallyEqual(comparedValue, assignedValue)) return false;
|
|
37442
|
+
return isUndefinedIdentifier(comparedValue) || derivesFromPostMountValue(comparedValue, localInitializers, callbackRefFieldNames);
|
|
37443
|
+
};
|
|
37444
|
+
const containsPositiveStateFieldTest = (test, fieldName) => {
|
|
37445
|
+
const unwrappedTest = stripParenExpression(test);
|
|
37446
|
+
if (getThisStateFieldName(unwrappedTest) === fieldName) return true;
|
|
37447
|
+
return isNodeOfType(unwrappedTest, "LogicalExpression") && unwrappedTest.operator === "&&" && (containsPositiveStateFieldTest(unwrappedTest.left, fieldName) || containsPositiveStateFieldTest(unwrappedTest.right, fieldName));
|
|
36602
37448
|
};
|
|
36603
|
-
const
|
|
37449
|
+
const isConvergentUndefinedClearGuard = (test, setStateCall) => {
|
|
37450
|
+
if (!isNodeOfType(setStateCall, "CallExpression")) return false;
|
|
37451
|
+
const argument = setStateCall.arguments?.[0];
|
|
37452
|
+
if (!argument || !isNodeOfType(argument, "ObjectExpression")) return false;
|
|
37453
|
+
for (const property of argument.properties ?? []) {
|
|
37454
|
+
if (!isNodeOfType(property, "Property") || property.computed === true || !isUndefinedIdentifier(property.value)) continue;
|
|
37455
|
+
const fieldName = isNodeOfType(property.key, "Identifier") && property.key.name || isNodeOfType(property.key, "Literal") && typeof property.key.value === "string" && property.key.value || null;
|
|
37456
|
+
if (fieldName && containsPositiveStateFieldTest(test, fieldName)) return true;
|
|
37457
|
+
}
|
|
37458
|
+
return false;
|
|
37459
|
+
};
|
|
37460
|
+
const isDiffGuardTest = (test, paramNames, derivedNames, isTruthfulBranch) => {
|
|
37461
|
+
const expression = stripParenExpression(test);
|
|
37462
|
+
if (isNodeOfType(expression, "LogicalExpression")) {
|
|
37463
|
+
if (expression.operator !== "&&" && expression.operator !== "||") return false;
|
|
37464
|
+
const leftIsDiffGuard = isDiffGuardTest(expression.left, paramNames, derivedNames, isTruthfulBranch);
|
|
37465
|
+
const rightIsDiffGuard = isDiffGuardTest(expression.right, paramNames, derivedNames, isTruthfulBranch);
|
|
37466
|
+
return isTruthfulBranch && expression.operator === "||" || !isTruthfulBranch && expression.operator === "&&" ? leftIsDiffGuard && rightIsDiffGuard : leftIsDiffGuard || rightIsDiffGuard;
|
|
37467
|
+
}
|
|
37468
|
+
if (!isNodeOfType(expression, "BinaryExpression") || !(isTruthfulBranch ? DIFFERENCE_OPERATORS.has(expression.operator) : EQUALITY_OPERATORS.has(expression.operator) && !DIFFERENCE_OPERATORS.has(expression.operator))) return false;
|
|
37469
|
+
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));
|
|
37470
|
+
};
|
|
37471
|
+
const isInsideDiffGuard = (setStateCall, scopes) => {
|
|
36604
37472
|
const lifecycleFunction = findEnclosingLifecycleFunction(setStateCall);
|
|
36605
37473
|
if (!lifecycleFunction) return false;
|
|
36606
37474
|
const paramNames = /* @__PURE__ */ new Set();
|
|
36607
|
-
|
|
37475
|
+
const parameters = lifecycleFunction.params ?? [];
|
|
37476
|
+
for (const param of parameters) collectPatternNames(param, paramNames);
|
|
37477
|
+
const previousSourcePaths = /* @__PURE__ */ new Map();
|
|
37478
|
+
const [previousPropsParameter, previousStateParameter] = parameters;
|
|
37479
|
+
collectPreviousSourcePaths(previousPropsParameter, "props", [], previousSourcePaths);
|
|
37480
|
+
collectPreviousSourcePaths(previousStateParameter, "state", [], previousSourcePaths);
|
|
36608
37481
|
const derivedNames = collectDiffSourceLocalNames(lifecycleFunction, paramNames);
|
|
36609
37482
|
const localInitializers = collectLocalInitializers(lifecycleFunction);
|
|
37483
|
+
const lifecycleWrittenFieldNames = collectLifecycleWrittenFieldNames(lifecycleFunction);
|
|
37484
|
+
const callbackRefFieldNames = new Set([...getCallbackRefFieldNames(findEnclosingClass(lifecycleFunction), scopes)].filter((fieldName) => !lifecycleWrittenFieldNames.has(fieldName)));
|
|
36610
37485
|
let child = setStateCall;
|
|
36611
37486
|
let ancestor = setStateCall.parent;
|
|
36612
37487
|
while (ancestor && ancestor !== lifecycleFunction) {
|
|
36613
|
-
|
|
36614
|
-
|
|
37488
|
+
let guardTest = null;
|
|
37489
|
+
let isTruthfulBranch = true;
|
|
37490
|
+
if (isNodeOfType(ancestor, "IfStatement")) {
|
|
37491
|
+
if (child === ancestor.consequent) guardTest = ancestor.test;
|
|
37492
|
+
else if (child === ancestor.alternate) {
|
|
37493
|
+
guardTest = ancestor.test;
|
|
37494
|
+
isTruthfulBranch = false;
|
|
37495
|
+
}
|
|
37496
|
+
} else if (isNodeOfType(ancestor, "ConditionalExpression")) {
|
|
37497
|
+
if (child === ancestor.consequent) guardTest = ancestor.test;
|
|
37498
|
+
else if (child === ancestor.alternate) {
|
|
37499
|
+
guardTest = ancestor.test;
|
|
37500
|
+
isTruthfulBranch = false;
|
|
37501
|
+
}
|
|
37502
|
+
} else if (isNodeOfType(ancestor, "LogicalExpression") && ancestor.operator === "&&" && child === ancestor.right) guardTest = ancestor.left;
|
|
37503
|
+
if (guardTest && (isDiffGuardTest(guardTest, paramNames, derivedNames, isTruthfulBranch) || isTruthfulBranch && isHistoricalToCurrentTransitionGuard(guardTest, previousSourcePaths) || isConvergentPostMountGuard(guardTest, setStateCall, localInitializers, callbackRefFieldNames, isTruthfulBranch) || isTruthfulBranch && isConvergentUndefinedClearGuard(guardTest, setStateCall))) return true;
|
|
36615
37504
|
child = ancestor;
|
|
36616
37505
|
ancestor = ancestor.parent ?? null;
|
|
36617
37506
|
}
|
|
@@ -36633,7 +37522,7 @@ const noDidUpdateSetState = defineRule({
|
|
|
36633
37522
|
if (!isNodeOfType(stripParenExpression(node.callee.object), "ThisExpression")) return;
|
|
36634
37523
|
if (!isNodeOfType(node.callee.property, "Identifier") || node.callee.property.name !== "setState") return;
|
|
36635
37524
|
if (!isSetStateCallInLifecycle(node, LIFECYCLE_NAMES$1, { disallowInNestedFunctions: mode === "disallow-in-func" })) return;
|
|
36636
|
-
if (isInsideDiffGuard(node)) return;
|
|
37525
|
+
if (isInsideDiffGuard(node, context.scopes)) return;
|
|
36637
37526
|
context.report({
|
|
36638
37527
|
node: node.callee,
|
|
36639
37528
|
message: MESSAGE$27
|
|
@@ -40355,41 +41244,223 @@ const readLogicalConditionResult = (operator, leftResult, rightResult) => {
|
|
|
40355
41244
|
if (leftResult === false && rightResult === false) return false;
|
|
40356
41245
|
return null;
|
|
40357
41246
|
};
|
|
40358
|
-
const readHydrationConditionResult = (expression, context, runtime) => {
|
|
41247
|
+
const readHydrationConditionResult = (expression, context, runtime, state) => {
|
|
40359
41248
|
const unwrappedExpression = stripParenExpression(expression);
|
|
40360
41249
|
const predicateMatch = matchBrowserPredicate(unwrappedExpression, context);
|
|
40361
41250
|
if (predicateMatch) return predicateMatch[`${runtime}Result`];
|
|
40362
41251
|
const staticResult = readInitialStateBoolean(unwrappedExpression, context.scopes);
|
|
40363
41252
|
if (staticResult !== null) return staticResult;
|
|
41253
|
+
const expressionSymbol = isNodeOfType(unwrappedExpression, "Identifier") ? context.scopes.symbolFor(unwrappedExpression) : null;
|
|
41254
|
+
const parameterValue = expressionSymbol ? state.parameterValuesBySymbolId.get(expressionSymbol.id) : null;
|
|
41255
|
+
if (expressionSymbol && parameterValue && !state.visitedSymbolIds.has(expressionSymbol.id)) {
|
|
41256
|
+
state.visitedSymbolIds.add(expressionSymbol.id);
|
|
41257
|
+
const result = readHydrationConditionResult(parameterValue, context, runtime, state);
|
|
41258
|
+
state.visitedSymbolIds.delete(expressionSymbol.id);
|
|
41259
|
+
return result;
|
|
41260
|
+
}
|
|
41261
|
+
if (expressionSymbol && expressionSymbol.kind === "const" && expressionSymbol.initializer && expressionSymbol.references.every((reference) => reference.flag === "read") && !state.visitedSymbolIds.has(expressionSymbol.id)) {
|
|
41262
|
+
state.visitedSymbolIds.add(expressionSymbol.id);
|
|
41263
|
+
const result = readHydrationConditionResult(expressionSymbol.initializer, context, runtime, state);
|
|
41264
|
+
state.visitedSymbolIds.delete(expressionSymbol.id);
|
|
41265
|
+
return result;
|
|
41266
|
+
}
|
|
41267
|
+
if (isNodeOfType(unwrappedExpression, "CallExpression")) {
|
|
41268
|
+
const callArguments = unwrappedExpression.arguments ?? [];
|
|
41269
|
+
if (isReactApiCall(unwrappedExpression, "useMemo", context.scopes, {
|
|
41270
|
+
allowGlobalReactNamespace: true,
|
|
41271
|
+
resolveNamedAliases: true
|
|
41272
|
+
})) {
|
|
41273
|
+
const callbackArgument = callArguments[0];
|
|
41274
|
+
if (!callbackArgument || isNodeOfType(callbackArgument, "SpreadElement")) return null;
|
|
41275
|
+
const callbackFunction = resolveExactLocalFunction(callbackArgument, context.scopes);
|
|
41276
|
+
return isFunctionLike$1(callbackFunction) && callbackFunction.params.length === 0 ? readHydrationFunctionResult(callbackFunction, context, runtime, state) : null;
|
|
41277
|
+
}
|
|
41278
|
+
const callee = stripParenExpression(unwrappedExpression.callee);
|
|
41279
|
+
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);
|
|
41280
|
+
const helperFunction = resolveExactLocalFunction(callee, context.scopes);
|
|
41281
|
+
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;
|
|
41282
|
+
const parameterValuesBySymbolId = new Map(state.parameterValuesBySymbolId);
|
|
41283
|
+
for (let parameterIndex = 0; parameterIndex < helperFunction.params.length; parameterIndex++) {
|
|
41284
|
+
const parameter = helperFunction.params[parameterIndex];
|
|
41285
|
+
const argument = callArguments[parameterIndex];
|
|
41286
|
+
if (!argument || !isNodeOfType(parameter, "Identifier")) continue;
|
|
41287
|
+
const parameterSymbol = context.scopes.symbolFor(parameter);
|
|
41288
|
+
if (parameterSymbol) parameterValuesBySymbolId.set(parameterSymbol.id, argument);
|
|
41289
|
+
}
|
|
41290
|
+
return readHydrationFunctionResult(helperFunction, context, runtime, {
|
|
41291
|
+
...state,
|
|
41292
|
+
parameterValuesBySymbolId
|
|
41293
|
+
});
|
|
41294
|
+
}
|
|
40364
41295
|
if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") {
|
|
40365
|
-
const argumentResult = readHydrationConditionResult(unwrappedExpression.argument, context, runtime);
|
|
41296
|
+
const argumentResult = readHydrationConditionResult(unwrappedExpression.argument, context, runtime, state);
|
|
40366
41297
|
return argumentResult === null ? null : !argumentResult;
|
|
40367
41298
|
}
|
|
40368
41299
|
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));
|
|
41300
|
+
return readLogicalConditionResult(unwrappedExpression.operator, readHydrationConditionResult(unwrappedExpression.left, context, runtime, state), readHydrationConditionResult(unwrappedExpression.right, context, runtime, state));
|
|
41301
|
+
};
|
|
41302
|
+
const readHydrationStatementResult = (statement, context, runtime, state) => {
|
|
41303
|
+
if (isNodeOfType(statement, "ReturnStatement")) return {
|
|
41304
|
+
didReturn: true,
|
|
41305
|
+
value: statement.argument ? readHydrationConditionResult(statement.argument, context, runtime, state) : null
|
|
41306
|
+
};
|
|
41307
|
+
if (isNodeOfType(statement, "BlockStatement")) {
|
|
41308
|
+
for (const childStatement of statement.body) {
|
|
41309
|
+
const result = readHydrationStatementResult(childStatement, context, runtime, state);
|
|
41310
|
+
if (result.didReturn) return result;
|
|
41311
|
+
if (statementAlwaysExits(childStatement)) break;
|
|
41312
|
+
}
|
|
41313
|
+
return {
|
|
41314
|
+
didReturn: false,
|
|
41315
|
+
value: null
|
|
41316
|
+
};
|
|
41317
|
+
}
|
|
41318
|
+
if (!isNodeOfType(statement, "IfStatement")) return {
|
|
41319
|
+
didReturn: false,
|
|
41320
|
+
value: null
|
|
41321
|
+
};
|
|
41322
|
+
const conditionResult = readHydrationConditionResult(statement.test, context, runtime, state);
|
|
41323
|
+
if (conditionResult !== null) {
|
|
41324
|
+
const selectedBranch = conditionResult ? statement.consequent : statement.alternate;
|
|
41325
|
+
return selectedBranch ? readHydrationStatementResult(selectedBranch, context, runtime, state) : {
|
|
41326
|
+
didReturn: false,
|
|
41327
|
+
value: null
|
|
41328
|
+
};
|
|
41329
|
+
}
|
|
41330
|
+
const consequentResult = readHydrationStatementResult(statement.consequent, context, runtime, state);
|
|
41331
|
+
const alternateResult = statement.alternate ? readHydrationStatementResult(statement.alternate, context, runtime, state) : {
|
|
41332
|
+
didReturn: false,
|
|
41333
|
+
value: null
|
|
41334
|
+
};
|
|
41335
|
+
return consequentResult.didReturn && alternateResult.didReturn && consequentResult.value !== null && consequentResult.value === alternateResult.value ? consequentResult : {
|
|
41336
|
+
didReturn: consequentResult.didReturn || alternateResult.didReturn,
|
|
41337
|
+
value: null
|
|
41338
|
+
};
|
|
40370
41339
|
};
|
|
40371
|
-
const
|
|
41340
|
+
const readHydrationFunctionResult = (functionNode, context, runtime, state) => {
|
|
41341
|
+
if (!isFunctionLike$1(functionNode) || state.visitedFunctionNodes.has(functionNode)) return null;
|
|
41342
|
+
state.visitedFunctionNodes.add(functionNode);
|
|
41343
|
+
const result = isNodeOfType(functionNode.body, "BlockStatement") ? readHydrationStatementResult(functionNode.body, context, runtime, state).value : readHydrationConditionResult(functionNode.body, context, runtime, state);
|
|
41344
|
+
state.visitedFunctionNodes.delete(functionNode);
|
|
41345
|
+
return result;
|
|
41346
|
+
};
|
|
41347
|
+
const doEquivalentExpressionBindingsMatch = (leftExpression, rightExpression, scopes) => {
|
|
41348
|
+
const left = stripParenExpression(leftExpression);
|
|
41349
|
+
const right = stripParenExpression(rightExpression);
|
|
41350
|
+
if (isNodeOfType(left, "Identifier") && isNodeOfType(right, "Identifier")) {
|
|
41351
|
+
const leftSymbol = scopes.symbolFor(left);
|
|
41352
|
+
const rightSymbol = scopes.symbolFor(right);
|
|
41353
|
+
return leftSymbol || rightSymbol ? leftSymbol?.id === rightSymbol?.id : true;
|
|
41354
|
+
}
|
|
41355
|
+
if (isNodeOfType(left, "MemberExpression") && isNodeOfType(right, "MemberExpression")) return doEquivalentExpressionBindingsMatch(left.object, right.object, scopes) && (!left.computed || doEquivalentExpressionBindingsMatch(left.property, right.property, scopes));
|
|
41356
|
+
if (isNodeOfType(left, "CallExpression") && isNodeOfType(right, "CallExpression")) {
|
|
41357
|
+
const rightArguments = right.arguments ?? [];
|
|
41358
|
+
return doEquivalentExpressionBindingsMatch(left.callee, right.callee, scopes) && (left.arguments ?? []).every((argument, index) => {
|
|
41359
|
+
const rightArgument = rightArguments[index];
|
|
41360
|
+
return Boolean(rightArgument && doEquivalentExpressionBindingsMatch(argument, rightArgument, scopes));
|
|
41361
|
+
});
|
|
41362
|
+
}
|
|
41363
|
+
return true;
|
|
41364
|
+
};
|
|
41365
|
+
const areHelperReturnValuesEquivalent = (leftValue, rightValue, context) => {
|
|
41366
|
+
if (areExpressionsStructurallyEqual(leftValue, rightValue)) return doEquivalentExpressionBindingsMatch(leftValue, rightValue, context.scopes);
|
|
41367
|
+
const leftBoolean = readInitialStateBoolean(leftValue, context.scopes);
|
|
41368
|
+
const rightBoolean = readInitialStateBoolean(rightValue, context.scopes);
|
|
41369
|
+
return leftBoolean !== null && rightBoolean !== null && leftBoolean === rightBoolean;
|
|
41370
|
+
};
|
|
41371
|
+
const doHelperReturnValuesDiffer = (leftValues, rightValues, context) => {
|
|
41372
|
+
const everyValueHasEquivalent = (values, candidateValues) => values.every((value) => candidateValues.some((candidateValue) => areHelperReturnValuesEquivalent(value, candidateValue, context)));
|
|
41373
|
+
return !everyValueHasEquivalent(leftValues, rightValues) || !everyValueHasEquivalent(rightValues, leftValues);
|
|
41374
|
+
};
|
|
41375
|
+
const matchHydrationConditionInternal = (expression, context, state) => {
|
|
40372
41376
|
const unwrappedExpression = stripParenExpression(expression);
|
|
40373
41377
|
const predicateMatch = matchBrowserPredicate(unwrappedExpression, context);
|
|
40374
41378
|
if (predicateMatch) return {
|
|
40375
41379
|
predicateMatch,
|
|
40376
41380
|
predicateNode: unwrappedExpression
|
|
40377
41381
|
};
|
|
40378
|
-
if (isNodeOfType(unwrappedExpression, "
|
|
40379
|
-
|
|
40380
|
-
|
|
40381
|
-
|
|
40382
|
-
|
|
40383
|
-
|
|
40384
|
-
|
|
40385
|
-
|
|
41382
|
+
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
41383
|
+
const symbol = context.scopes.symbolFor(unwrappedExpression);
|
|
41384
|
+
const parameterValue = symbol ? state.parameterValuesBySymbolId.get(symbol.id) : null;
|
|
41385
|
+
if (symbol && parameterValue && !state.visitedSymbolIds.has(symbol.id)) {
|
|
41386
|
+
state.visitedSymbolIds.add(symbol.id);
|
|
41387
|
+
const match = matchHydrationConditionInternal(parameterValue, context, state);
|
|
41388
|
+
state.visitedSymbolIds.delete(symbol.id);
|
|
41389
|
+
return match;
|
|
41390
|
+
}
|
|
41391
|
+
if (!symbol || symbol.kind !== "const" || !symbol.initializer || symbol.references.some((reference) => reference.flag !== "read") || state.visitedSymbolIds.has(symbol.id)) return null;
|
|
41392
|
+
state.visitedSymbolIds.add(symbol.id);
|
|
41393
|
+
const match = matchHydrationConditionInternal(symbol.initializer, context, state);
|
|
41394
|
+
state.visitedSymbolIds.delete(symbol.id);
|
|
41395
|
+
return match;
|
|
41396
|
+
}
|
|
41397
|
+
if (isNodeOfType(unwrappedExpression, "CallExpression")) {
|
|
41398
|
+
const callArguments = unwrappedExpression.arguments ?? [];
|
|
41399
|
+
if (isReactApiCall(unwrappedExpression, "useMemo", context.scopes, {
|
|
41400
|
+
allowGlobalReactNamespace: true,
|
|
41401
|
+
resolveNamedAliases: true
|
|
41402
|
+
})) {
|
|
41403
|
+
const callbackArgument = callArguments[0];
|
|
41404
|
+
if (!callbackArgument || isNodeOfType(callbackArgument, "SpreadElement")) return null;
|
|
41405
|
+
const callbackFunction = resolveExactLocalFunction(callbackArgument, context.scopes);
|
|
41406
|
+
return isFunctionLike$1(callbackFunction) && callbackFunction.params.length === 0 ? matchHydrationFunctionResult(callbackFunction, context, state) : null;
|
|
41407
|
+
}
|
|
41408
|
+
const callee = stripParenExpression(unwrappedExpression.callee);
|
|
41409
|
+
if (isNodeOfType(callee, "Identifier") && callee.name === "Boolean" && context.scopes.isGlobalReference(callee) && callArguments.length === 1 && !isNodeOfType(callArguments[0], "SpreadElement")) return matchHydrationConditionInternal(callArguments[0], context, state);
|
|
41410
|
+
const helperFunction = resolveExactLocalFunction(callee, context.scopes);
|
|
41411
|
+
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;
|
|
41412
|
+
const parameterValuesBySymbolId = new Map(state.parameterValuesBySymbolId);
|
|
41413
|
+
for (let parameterIndex = 0; parameterIndex < helperFunction.params.length; parameterIndex++) {
|
|
41414
|
+
const parameter = helperFunction.params[parameterIndex];
|
|
41415
|
+
const argument = callArguments[parameterIndex];
|
|
41416
|
+
if (!argument || !isNodeOfType(parameter, "Identifier")) continue;
|
|
41417
|
+
const parameterSymbol = context.scopes.symbolFor(parameter);
|
|
41418
|
+
if (parameterSymbol) parameterValuesBySymbolId.set(parameterSymbol.id, argument);
|
|
41419
|
+
}
|
|
41420
|
+
return matchHydrationFunctionResult(helperFunction, context, {
|
|
41421
|
+
...state,
|
|
41422
|
+
parameterValuesBySymbolId
|
|
41423
|
+
});
|
|
40386
41424
|
}
|
|
41425
|
+
if (isNodeOfType(unwrappedExpression, "UnaryExpression") && unwrappedExpression.operator === "!") return matchHydrationConditionInternal(unwrappedExpression.argument, context, state);
|
|
41426
|
+
if (!isNodeOfType(unwrappedExpression, "LogicalExpression") || unwrappedExpression.operator !== "&&" && unwrappedExpression.operator !== "||") return null;
|
|
41427
|
+
const leftMatch = matchHydrationConditionInternal(unwrappedExpression.left, context, state);
|
|
41428
|
+
const rightMatch = matchHydrationConditionInternal(unwrappedExpression.right, context, state);
|
|
40387
41429
|
const nestedMatch = leftMatch ?? rightMatch;
|
|
40388
41430
|
if (!nestedMatch) return null;
|
|
40389
|
-
const
|
|
40390
|
-
|
|
40391
|
-
return nestedMatch;
|
|
41431
|
+
const clientResult = readHydrationConditionResult(unwrappedExpression, context, "client", state);
|
|
41432
|
+
const serverResult = readHydrationConditionResult(unwrappedExpression, context, "server", state);
|
|
41433
|
+
return clientResult !== null && serverResult !== null && clientResult === serverResult ? null : nestedMatch;
|
|
40392
41434
|
};
|
|
41435
|
+
const matchHydrationReturningStatement = (statement, context, state) => {
|
|
41436
|
+
if (isNodeOfType(statement, "ReturnStatement")) return statement.argument ? matchHydrationConditionInternal(statement.argument, context, state) : null;
|
|
41437
|
+
if (isNodeOfType(statement, "IfStatement")) {
|
|
41438
|
+
const conditionMatch = matchHydrationConditionInternal(statement.test, context, state);
|
|
41439
|
+
const consequentValues = getReturnedValues(statement.consequent);
|
|
41440
|
+
const alternateValues = statement.alternate ? getReturnedValues(statement.alternate) : findFollowingReturnedValues(statement);
|
|
41441
|
+
if (conditionMatch && consequentValues.length > 0 && alternateValues.length > 0 && doHelperReturnValuesDiffer(consequentValues, alternateValues, context)) return conditionMatch;
|
|
41442
|
+
return matchHydrationReturningStatement(statement.consequent, context, state) ?? (statement.alternate ? matchHydrationReturningStatement(statement.alternate, context, state) : null);
|
|
41443
|
+
}
|
|
41444
|
+
if (!isNodeOfType(statement, "BlockStatement")) return null;
|
|
41445
|
+
for (const childStatement of statement.body) {
|
|
41446
|
+
const match = matchHydrationReturningStatement(childStatement, context, state);
|
|
41447
|
+
if (match) return match;
|
|
41448
|
+
if (statementAlwaysExits(childStatement)) break;
|
|
41449
|
+
}
|
|
41450
|
+
return null;
|
|
41451
|
+
};
|
|
41452
|
+
const matchHydrationFunctionResult = (functionNode, context, state) => {
|
|
41453
|
+
if (!isFunctionLike$1(functionNode) || state.visitedFunctionNodes.has(functionNode)) return null;
|
|
41454
|
+
state.visitedFunctionNodes.add(functionNode);
|
|
41455
|
+
const match = isNodeOfType(functionNode.body, "BlockStatement") ? matchHydrationReturningStatement(functionNode.body, context, state) : matchHydrationConditionInternal(functionNode.body, context, state);
|
|
41456
|
+
state.visitedFunctionNodes.delete(functionNode);
|
|
41457
|
+
return match;
|
|
41458
|
+
};
|
|
41459
|
+
const matchHydrationCondition = (expression, context) => matchHydrationConditionInternal(expression, context, {
|
|
41460
|
+
parameterValuesBySymbolId: /* @__PURE__ */ new Map(),
|
|
41461
|
+
visitedFunctionNodes: /* @__PURE__ */ new Set(),
|
|
41462
|
+
visitedSymbolIds: /* @__PURE__ */ new Set()
|
|
41463
|
+
});
|
|
40393
41464
|
const areNodeArraysEquivalent = (leftNodes, rightNodes) => leftNodes.length === rightNodes.length && leftNodes.every((leftNode, index) => areRenderedBranchesEquivalent(leftNode, rightNodes[index]));
|
|
40394
41465
|
const areRenderedBranchesEquivalent = (leftNode, rightNode) => {
|
|
40395
41466
|
if (!leftNode || !rightNode) return leftNode === rightNode;
|
|
@@ -40532,17 +41603,17 @@ const noHydrationBranchOnBrowserGlobal = defineRule({
|
|
|
40532
41603
|
const { predicateMatch, predicateNode } = conditionMatch;
|
|
40533
41604
|
if (reportedNodes.has(predicateNode)) return;
|
|
40534
41605
|
if (rightBranch && areRenderedBranchesEquivalent(leftBranch, rightBranch)) return;
|
|
40535
|
-
const componentOrHookNode = findRenderPhaseComponentOrHook(
|
|
41606
|
+
const componentOrHookNode = findRenderPhaseComponentOrHook(conditionNode, context.scopes);
|
|
40536
41607
|
if (!componentOrHookNode) return;
|
|
40537
41608
|
if (!hasClientRenderEvidence(componentOrHookNode, fileHasUseClientDirective)) return;
|
|
40538
|
-
if (requiresRenderedContext && !isInRenderedOutput(
|
|
41609
|
+
if (requiresRenderedContext && !isInRenderedOutput(conditionNode, componentOrHookNode, context.scopes)) return;
|
|
40539
41610
|
if (!isRenderedValue(leftBranch) && (!rightBranch || !isRenderedValue(rightBranch))) {
|
|
40540
|
-
const attribute = findEnclosingJsxAttribute(
|
|
41611
|
+
const attribute = findEnclosingJsxAttribute(conditionNode);
|
|
40541
41612
|
if (!attribute || isEventHandlerAttribute(attribute)) return;
|
|
40542
41613
|
}
|
|
40543
|
-
if (fileIsEmailTemplate || isGatedByFalsyInitialState(
|
|
40544
|
-
if (isAfterClientOnlyEarlyReturn(
|
|
40545
|
-
const openingElement = findEnclosingJsxOpeningElement(
|
|
41614
|
+
if (fileIsEmailTemplate || isGatedByFalsyInitialState(conditionNode, context.scopes)) return;
|
|
41615
|
+
if (isAfterClientOnlyEarlyReturn(conditionNode, componentOrHookNode, context.scopes)) return;
|
|
41616
|
+
const openingElement = findEnclosingJsxOpeningElement(conditionNode);
|
|
40546
41617
|
if (hasSuppressHydrationWarningAttribute(openingElement) && !isStructuralRenderedValue(leftBranch) && !isStructuralRenderedValue(rightBranch)) return;
|
|
40547
41618
|
if (branchRootsSuppressSameElement(leftBranch, rightBranch)) return;
|
|
40548
41619
|
if (isGeneratedImageRenderContext(context, openingElement ?? leftBranch)) return;
|
|
@@ -40924,7 +41995,7 @@ const noInitializeState = defineRule({
|
|
|
40924
41995
|
if (!dependencies || !isNodeOfType(dependencies, "ArrayExpression") || (dependencies.elements ?? []).length !== 0) return;
|
|
40925
41996
|
const analysis = getProgramAnalysis(node);
|
|
40926
41997
|
if (!analysis) return;
|
|
40927
|
-
for (const fact of collectEffectStateWriteFacts(analysis, node, context.filename)) {
|
|
41998
|
+
for (const fact of collectEffectStateWriteFacts(analysis, context, node, context.filename)) {
|
|
40928
41999
|
if (!fact.isRenderKnownCopy || fact.matchesStateInitializer || fact.resetsSourceState) continue;
|
|
40929
42000
|
const stateName = getStateName(fact.stateDeclarator);
|
|
40930
42001
|
context.report({
|
|
@@ -41282,7 +42353,8 @@ const noJsxElementType = defineRule({
|
|
|
41282
42353
|
create: (context) => {
|
|
41283
42354
|
let isJsxImported = false;
|
|
41284
42355
|
const flaggedAnnotations = [];
|
|
41285
|
-
const
|
|
42356
|
+
const collectComponentReturnType = (functionNode, returnType) => {
|
|
42357
|
+
if (!(isNodeOfType(functionNode, "TSDeclareFunction") ? Boolean(functionNode.id && isReactComponentName(functionNode.id.name)) : isComponentFunction$1(functionNode))) return;
|
|
41286
42358
|
const typeAnnotation = extractReturnTypeAnnotation(returnType);
|
|
41287
42359
|
if (!typeAnnotation) return;
|
|
41288
42360
|
if (isJsxElementTypeReference(typeAnnotation)) flaggedAnnotations.push(typeAnnotation);
|
|
@@ -41292,19 +42364,16 @@ const noJsxElementType = defineRule({
|
|
|
41292
42364
|
if (isJsxImportBinding(node)) isJsxImported = true;
|
|
41293
42365
|
},
|
|
41294
42366
|
FunctionDeclaration(node) {
|
|
41295
|
-
|
|
42367
|
+
collectComponentReturnType(node, node.returnType);
|
|
41296
42368
|
},
|
|
41297
42369
|
ArrowFunctionExpression(node) {
|
|
41298
|
-
|
|
42370
|
+
collectComponentReturnType(node, node.returnType);
|
|
41299
42371
|
},
|
|
41300
42372
|
FunctionExpression(node) {
|
|
41301
|
-
|
|
42373
|
+
collectComponentReturnType(node, node.returnType);
|
|
41302
42374
|
},
|
|
41303
42375
|
TSDeclareFunction(node) {
|
|
41304
|
-
|
|
41305
|
-
},
|
|
41306
|
-
TSMethodSignature(node) {
|
|
41307
|
-
checkReturnType(node.returnType);
|
|
42376
|
+
collectComponentReturnType(node, node.returnType);
|
|
41308
42377
|
},
|
|
41309
42378
|
"Program:exit"() {
|
|
41310
42379
|
if (isJsxImported) return;
|
|
@@ -44331,6 +45400,114 @@ const DATA_SINK_METHOD_NAMES = new Set([
|
|
|
44331
45400
|
"deserialize"
|
|
44332
45401
|
]);
|
|
44333
45402
|
//#endregion
|
|
45403
|
+
//#region src/plugin/utils/get-transparent-react-callback-wrapper-argument.ts
|
|
45404
|
+
const getTransparentReactCallbackWrapperArgument = (initializer, resultSymbol, scopes) => {
|
|
45405
|
+
const callExpression = stripParenExpression(initializer);
|
|
45406
|
+
if (!isNodeOfType(callExpression, "CallExpression")) return null;
|
|
45407
|
+
const callbackArgument = callExpression.arguments[0];
|
|
45408
|
+
if (!callbackArgument) return null;
|
|
45409
|
+
if (resultSymbol && symbolHasReactUseEffectEventOrigin(resultSymbol, scopes)) return callbackArgument;
|
|
45410
|
+
return isReactApiCall(callExpression, "useCallback", scopes, {
|
|
45411
|
+
allowGlobalReactNamespace: true,
|
|
45412
|
+
allowUnboundBareCalls: true
|
|
45413
|
+
}) ? callbackArgument : null;
|
|
45414
|
+
};
|
|
45415
|
+
//#endregion
|
|
45416
|
+
//#region src/plugin/rules/state-and-effects/utils/resolve-parent-callback-provenance.ts
|
|
45417
|
+
const getDeclarationKind$1 = (declarator) => {
|
|
45418
|
+
const declaration = declarator.parent;
|
|
45419
|
+
return declaration && isNodeOfType(declaration, "VariableDeclaration") ? declaration.kind : null;
|
|
45420
|
+
};
|
|
45421
|
+
const hasMutableBindingWrite$2 = (reference) => Boolean(reference.resolved?.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init));
|
|
45422
|
+
const mergeRequiredBranches = (leftNames, rightNames) => {
|
|
45423
|
+
if (!leftNames || !rightNames) return null;
|
|
45424
|
+
return new Set([...leftNames, ...rightNames]);
|
|
45425
|
+
};
|
|
45426
|
+
const getPropReferenceName = (analysis, identifier) => {
|
|
45427
|
+
if (!isNodeOfType(identifier, "Identifier")) return null;
|
|
45428
|
+
const reference = getRef(analysis, identifier);
|
|
45429
|
+
if (!reference || !isProp(analysis, reference) || isWholePropsObjectReference(analysis, reference)) return null;
|
|
45430
|
+
const bindingIdentifier = (reference.resolved?.defs.find((definition) => definition.type === "Parameter"))?.name;
|
|
45431
|
+
return (bindingIdentifier && getDestructuredBindingPropertyName(bindingIdentifier)) ?? identifier.name;
|
|
45432
|
+
};
|
|
45433
|
+
const getSingleConstDeclarator = (reference) => {
|
|
45434
|
+
if (!reference.resolved || hasMutableBindingWrite$2(reference)) return null;
|
|
45435
|
+
const declarators = reference.resolved.defs.map((definition) => definition.node).filter((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
|
|
45436
|
+
if (declarators.length !== 1) return null;
|
|
45437
|
+
const declarator = declarators[0];
|
|
45438
|
+
if (!declarator || getDeclarationKind$1(declarator) !== "const") return null;
|
|
45439
|
+
return declarator;
|
|
45440
|
+
};
|
|
45441
|
+
const resolveParentCallbackPropNames = (analysis, expression, scopes, visitedReferences, allowFunctionForwarder = false) => {
|
|
45442
|
+
const unwrappedExpression = stripParenExpression(expression);
|
|
45443
|
+
if (isFunctionLike$1(unwrappedExpression)) {
|
|
45444
|
+
if (!allowFunctionForwarder || Boolean(unwrappedExpression.async)) return null;
|
|
45445
|
+
const callbackNames = /* @__PURE__ */ new Set();
|
|
45446
|
+
walkInsideStatementBlocks(unwrappedExpression.body, (child) => {
|
|
45447
|
+
if (!isNodeOfType(child, "CallExpression")) return;
|
|
45448
|
+
const resolvedNames = resolveParentCallbackPropNames(analysis, child.callee, scopes, new Set(visitedReferences), false);
|
|
45449
|
+
if (!resolvedNames) return;
|
|
45450
|
+
for (const resolvedName of resolvedNames) callbackNames.add(resolvedName);
|
|
45451
|
+
});
|
|
45452
|
+
return callbackNames.size > 0 ? callbackNames : null;
|
|
45453
|
+
}
|
|
45454
|
+
if (isNodeOfType(unwrappedExpression, "ConditionalExpression")) return mergeRequiredBranches(resolveParentCallbackPropNames(analysis, unwrappedExpression.consequent, scopes, new Set(visitedReferences), false), resolveParentCallbackPropNames(analysis, unwrappedExpression.alternate, scopes, new Set(visitedReferences), false));
|
|
45455
|
+
if (isNodeOfType(unwrappedExpression, "LogicalExpression")) return mergeRequiredBranches(resolveParentCallbackPropNames(analysis, unwrappedExpression.left, scopes, new Set(visitedReferences), false), resolveParentCallbackPropNames(analysis, unwrappedExpression.right, scopes, new Set(visitedReferences)));
|
|
45456
|
+
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
45457
|
+
const propName = getPropReferenceName(analysis, unwrappedExpression);
|
|
45458
|
+
if (propName) return new Set([propName]);
|
|
45459
|
+
const reference = getRef(analysis, unwrappedExpression);
|
|
45460
|
+
if (!reference?.resolved || visitedReferences.has(reference.resolved)) return null;
|
|
45461
|
+
const declarator = getSingleConstDeclarator(reference);
|
|
45462
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !declarator.init) return null;
|
|
45463
|
+
visitedReferences.add(reference.resolved);
|
|
45464
|
+
const wrappedArgument = getTransparentReactCallbackWrapperArgument(declarator.init, scopes.symbolFor(unwrappedExpression), scopes);
|
|
45465
|
+
const allowsFunctionForwarder = Boolean(wrappedArgument && !isReactApiCall(declarator.init, "useCallback", scopes, {
|
|
45466
|
+
allowGlobalReactNamespace: true,
|
|
45467
|
+
allowUnboundBareCalls: true
|
|
45468
|
+
}));
|
|
45469
|
+
return resolveParentCallbackPropNames(analysis, wrappedArgument ?? declarator.init, scopes, visitedReferences, allowsFunctionForwarder);
|
|
45470
|
+
}
|
|
45471
|
+
if (!isNodeOfType(unwrappedExpression, "MemberExpression")) return null;
|
|
45472
|
+
const propertyName = getStaticMemberPropertyName(unwrappedExpression);
|
|
45473
|
+
if (!propertyName) return null;
|
|
45474
|
+
const receiver = stripParenExpression(unwrappedExpression.object);
|
|
45475
|
+
if (!isNodeOfType(receiver, "Identifier")) return null;
|
|
45476
|
+
const receiverReference = getRef(analysis, receiver);
|
|
45477
|
+
if (!receiverReference?.resolved || visitedReferences.has(receiverReference.resolved)) return null;
|
|
45478
|
+
if (isWholePropsObjectReference(analysis, receiverReference)) return new Set([propertyName]);
|
|
45479
|
+
const declarator = getSingleConstDeclarator(receiverReference);
|
|
45480
|
+
if (!declarator || !isNodeOfType(declarator, "VariableDeclarator") || !declarator.init) return null;
|
|
45481
|
+
visitedReferences.add(receiverReference.resolved);
|
|
45482
|
+
const initializer = stripParenExpression(declarator.init);
|
|
45483
|
+
if (propertyName === "current" && isNodeOfType(initializer, "CallExpression")) {
|
|
45484
|
+
if (!isReactApiCall(initializer, "useRef", scopes, {
|
|
45485
|
+
allowGlobalReactNamespace: true,
|
|
45486
|
+
allowUnboundBareCalls: true
|
|
45487
|
+
})) return null;
|
|
45488
|
+
const callbackArgument = initializer.arguments[0];
|
|
45489
|
+
if (!callbackArgument) return null;
|
|
45490
|
+
let callbackNames = resolveParentCallbackPropNames(analysis, callbackArgument, scopes, new Set(visitedReferences), false);
|
|
45491
|
+
if (!callbackNames) return null;
|
|
45492
|
+
for (const candidateReference of receiverReference.resolved.references) {
|
|
45493
|
+
const candidateIdentifier = candidateReference.identifier;
|
|
45494
|
+
const candidateMember = candidateIdentifier.parent;
|
|
45495
|
+
if (!candidateMember || !isNodeOfType(candidateMember, "MemberExpression") || candidateMember.object !== candidateIdentifier || getStaticMemberPropertyName(candidateMember) !== "current") continue;
|
|
45496
|
+
const assignment = candidateMember.parent;
|
|
45497
|
+
if (!assignment || !isNodeOfType(assignment, "AssignmentExpression") || assignment.left !== candidateMember) continue;
|
|
45498
|
+
if (assignment.operator !== "=") return null;
|
|
45499
|
+
callbackNames = mergeRequiredBranches(callbackNames, resolveParentCallbackPropNames(analysis, assignment.right, scopes, new Set(visitedReferences), false));
|
|
45500
|
+
if (!callbackNames) return null;
|
|
45501
|
+
}
|
|
45502
|
+
return callbackNames;
|
|
45503
|
+
}
|
|
45504
|
+
if (!isNodeOfType(initializer, "ObjectExpression")) return null;
|
|
45505
|
+
const property = initializer.properties.find((candidateProperty) => isNodeOfType(candidateProperty, "Property") && getStaticPropertyKeyName(candidateProperty, { allowComputedString: true }) === propertyName);
|
|
45506
|
+
if (!property || !isNodeOfType(property, "Property")) return null;
|
|
45507
|
+
return resolveParentCallbackPropNames(analysis, property.value, scopes, visitedReferences, false);
|
|
45508
|
+
};
|
|
45509
|
+
const getParentCallbackPropNames = ({ analysis, expression, scopes }) => resolveParentCallbackPropNames(analysis, expression, scopes, /* @__PURE__ */ new Set(), false);
|
|
45510
|
+
//#endregion
|
|
44334
45511
|
//#region src/plugin/rules/state-and-effects/no-pass-data-to-parent.ts
|
|
44335
45512
|
const isUseStateIdentifier = (identifier) => {
|
|
44336
45513
|
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
@@ -44359,14 +45536,18 @@ const FUNCTION_WRAPPER_HOOK_NAMES$1 = new Set([
|
|
|
44359
45536
|
"useStableCallback",
|
|
44360
45537
|
"useCallbackRef"
|
|
44361
45538
|
]);
|
|
44362
|
-
const getWrapperHookWrappedFunction = (initializer) => {
|
|
45539
|
+
const getWrapperHookWrappedFunction = (initializer, resultSymbol, scopes) => {
|
|
44363
45540
|
if (!isNodeOfType(initializer, "CallExpression")) return null;
|
|
45541
|
+
const transparentReactArgument = getTransparentReactCallbackWrapperArgument(initializer, resultSymbol, scopes);
|
|
45542
|
+
if (transparentReactArgument) return transparentReactArgument;
|
|
44364
45543
|
const callee = initializer.callee;
|
|
44365
45544
|
const calleeName = isNodeOfType(callee, "Identifier") ? callee.name : isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier") ? callee.property.name : null;
|
|
44366
45545
|
if (!calleeName || !FUNCTION_WRAPPER_HOOK_NAMES$1.has(calleeName)) return null;
|
|
44367
45546
|
const wrapped = initializer.arguments?.[0];
|
|
44368
|
-
if (!wrapped
|
|
44369
|
-
return
|
|
45547
|
+
if (!wrapped) return null;
|
|
45548
|
+
if (calleeName === "useEffectEvent") return null;
|
|
45549
|
+
if (isFunctionLike$1(wrapped)) return wrapped;
|
|
45550
|
+
return null;
|
|
44370
45551
|
};
|
|
44371
45552
|
const HANDLER_NAMED_PROP_PATTERN = /^(on|handle)[A-Z]/;
|
|
44372
45553
|
const wrappedFunctionNotifiesParent = (analysis, wrappedFunction) => getDownstreamRefs(analysis, wrappedFunction).some((innerRef) => {
|
|
@@ -44376,16 +45557,29 @@ const wrappedFunctionNotifiesParent = (analysis, wrappedFunction) => getDownstre
|
|
|
44376
45557
|
const innerParent = innerIdentifier.parent;
|
|
44377
45558
|
return Boolean(innerParent && isNodeOfType(innerParent, "CallExpression") && innerParent.callee === innerIdentifier);
|
|
44378
45559
|
});
|
|
44379
|
-
const isDirectParentCallbackRef = (analysis, ref) => {
|
|
45560
|
+
const isDirectParentCallbackRef = (analysis, ref, scopes) => {
|
|
44380
45561
|
if (isProp(analysis, ref)) return true;
|
|
45562
|
+
if (hasMutableBindingWrite$1(ref)) {
|
|
45563
|
+
if (!(ref.resolved?.references.filter((candidateReference) => candidateReference.isWrite() && !candidateReference.init) ?? []).every((candidateReference) => {
|
|
45564
|
+
const candidateIdentifier = candidateReference.identifier;
|
|
45565
|
+
const assignment = candidateIdentifier.parent;
|
|
45566
|
+
if (!assignment || !isNodeOfType(assignment, "AssignmentExpression") || assignment.operator !== "=" || assignment.left !== candidateIdentifier) return false;
|
|
45567
|
+
const assignedReferences = getDownstreamRefs(analysis, assignment.right);
|
|
45568
|
+
return assignedReferences.length > 0 && assignedReferences.every((assignedReference) => isProp(analysis, assignedReference));
|
|
45569
|
+
})) return false;
|
|
45570
|
+
}
|
|
44381
45571
|
return Boolean(ref.resolved?.defs.some((def) => {
|
|
44382
45572
|
const node = def.node;
|
|
44383
45573
|
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
44384
45574
|
const initializer = unwrapChainExpression(node.init);
|
|
44385
|
-
const wrappedFunction = getWrapperHookWrappedFunction(initializer);
|
|
45575
|
+
const wrappedFunction = getWrapperHookWrappedFunction(initializer, isNodeOfType(node.id, "Identifier") ? scopes.symbolFor(node.id) ?? null : null, scopes);
|
|
44386
45576
|
if (wrappedFunction) {
|
|
44387
45577
|
if (wrappedFunction.async) return false;
|
|
44388
|
-
return wrappedFunctionNotifiesParent(analysis, wrappedFunction);
|
|
45578
|
+
if (isFunctionLike$1(wrappedFunction)) return wrappedFunctionNotifiesParent(analysis, wrappedFunction);
|
|
45579
|
+
const directName = getParentCallbackPropName(analysis, wrappedFunction);
|
|
45580
|
+
const downstreamReferences = getDownstreamRefs(analysis, wrappedFunction);
|
|
45581
|
+
if (directName !== null) return true;
|
|
45582
|
+
return downstreamReferences.some((wrappedReference) => !hasMutableBindingWrite$1(wrappedReference) && getUpstreamRefs(analysis, wrappedReference).some((upstreamReference) => isProp(analysis, upstreamReference)));
|
|
44389
45583
|
}
|
|
44390
45584
|
if (!isNodeOfType(initializer, "Identifier") && !isNodeOfType(initializer, "MemberExpression")) return false;
|
|
44391
45585
|
return getDownstreamRefs(analysis, initializer).some((initializerRef) => getUpstreamRefs(analysis, initializerRef).some((upstreamRef) => isProp(analysis, upstreamRef)));
|
|
@@ -44395,7 +45589,7 @@ const getDeclarationKind = (declarator) => {
|
|
|
44395
45589
|
const declaration = declarator.parent;
|
|
44396
45590
|
return declaration && isNodeOfType(declaration, "VariableDeclaration") ? declaration.kind : null;
|
|
44397
45591
|
};
|
|
44398
|
-
const hasMutableBindingWrite = (reference) => Boolean(reference.resolved?.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init));
|
|
45592
|
+
const hasMutableBindingWrite$1 = (reference) => Boolean(reference.resolved?.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init));
|
|
44399
45593
|
const getParentCallbackPropName = (analysis, expression, visitedVariables = /* @__PURE__ */ new Set()) => {
|
|
44400
45594
|
const unwrappedExpression = stripParenExpression(expression);
|
|
44401
45595
|
if (isNodeOfType(unwrappedExpression, "Identifier")) {
|
|
@@ -44407,7 +45601,7 @@ const getParentCallbackPropName = (analysis, expression, visitedVariables = /* @
|
|
|
44407
45601
|
const bindingIdentifier = callbackVariable.defs.find((definition) => definition.type === "Parameter")?.name;
|
|
44408
45602
|
return (bindingIdentifier && getDestructuredBindingPropertyName(bindingIdentifier)) ?? unwrappedExpression.name;
|
|
44409
45603
|
}
|
|
44410
|
-
if (hasMutableBindingWrite(callbackReference)) return null;
|
|
45604
|
+
if (hasMutableBindingWrite$1(callbackReference)) return null;
|
|
44411
45605
|
const definitions = callbackVariable.defs.map((definition) => definition.node).filter((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
|
|
44412
45606
|
if (definitions.length !== 1) return null;
|
|
44413
45607
|
const declarator = definitions[0];
|
|
@@ -44473,7 +45667,7 @@ const getRefAliasDeclarator = (identifier) => {
|
|
|
44473
45667
|
const getRefBindingProvenance = (analysis, receiver, isReactUseRefCall) => {
|
|
44474
45668
|
if (!isNodeOfType(receiver, "Identifier")) return null;
|
|
44475
45669
|
const receiverReference = getRef(analysis, receiver);
|
|
44476
|
-
if (!receiverReference?.resolved || hasMutableBindingWrite(receiverReference)) return null;
|
|
45670
|
+
if (!receiverReference?.resolved || hasMutableBindingWrite$1(receiverReference)) return null;
|
|
44477
45671
|
const variables = /* @__PURE__ */ new Set();
|
|
44478
45672
|
let currentVariable = receiverReference.resolved;
|
|
44479
45673
|
let refCall = null;
|
|
@@ -44489,7 +45683,7 @@ const getRefBindingProvenance = (analysis, receiver, isReactUseRefCall) => {
|
|
|
44489
45683
|
}
|
|
44490
45684
|
if (getDeclarationKind(declarator) !== "const" || !isNodeOfType(stripParenExpression(declarator.init), "Identifier")) return null;
|
|
44491
45685
|
const upstreamReference = getRef(analysis, stripParenExpression(declarator.init));
|
|
44492
|
-
if (!upstreamReference?.resolved || hasMutableBindingWrite(upstreamReference)) return null;
|
|
45686
|
+
if (!upstreamReference?.resolved || hasMutableBindingWrite$1(upstreamReference)) return null;
|
|
44493
45687
|
currentVariable = upstreamReference.resolved;
|
|
44494
45688
|
}
|
|
44495
45689
|
if (!refCall) return null;
|
|
@@ -44564,7 +45758,7 @@ const isParentPropsContextMerge = (analysis, expression) => {
|
|
|
44564
45758
|
while (isNodeOfType(currentExpression, "Identifier")) {
|
|
44565
45759
|
const currentReference = getRef(analysis, currentExpression);
|
|
44566
45760
|
const currentVariable = currentReference?.resolved;
|
|
44567
|
-
if (!currentReference || !currentVariable || visitedVariables.has(currentVariable) || hasMutableBindingWrite(currentReference)) return false;
|
|
45761
|
+
if (!currentReference || !currentVariable || visitedVariables.has(currentVariable) || hasMutableBindingWrite$1(currentReference)) return false;
|
|
44568
45762
|
visitedVariables.add(currentVariable);
|
|
44569
45763
|
const definitions = currentVariable.defs.filter((definition) => isNodeOfType(definition.node, "VariableDeclarator"));
|
|
44570
45764
|
if (definitions.length !== 1) return false;
|
|
@@ -44578,11 +45772,11 @@ const isParentPropsContextMerge = (analysis, expression) => {
|
|
|
44578
45772
|
const propsExpression = stripParenExpression(propsSpread.argument);
|
|
44579
45773
|
if (!isNodeOfType(propsExpression, "Identifier")) return false;
|
|
44580
45774
|
const propsReference = getRef(analysis, propsExpression);
|
|
44581
|
-
if (!propsReference?.resolved || !isWholePropsObjectReference(analysis, propsReference) || hasMutableBindingWrite(propsReference) || propsReference.resolved.references.some((candidateReference) => candidateReference !== propsReference)) return false;
|
|
45775
|
+
if (!propsReference?.resolved || !isWholePropsObjectReference(analysis, propsReference) || hasMutableBindingWrite$1(propsReference) || propsReference.resolved.references.some((candidateReference) => candidateReference !== propsReference)) return false;
|
|
44582
45776
|
const contextExpression = stripParenExpression(contextSpread.argument);
|
|
44583
45777
|
if (!isNodeOfType(contextExpression, "Identifier")) return false;
|
|
44584
45778
|
const contextReference = getRef(analysis, contextExpression);
|
|
44585
|
-
if (!contextReference?.resolved || hasMutableBindingWrite(contextReference) || contextReference.resolved.references.some((candidateReference) => !candidateReference.init && candidateReference !== contextReference)) return false;
|
|
45779
|
+
if (!contextReference?.resolved || hasMutableBindingWrite$1(contextReference) || contextReference.resolved.references.some((candidateReference) => !candidateReference.init && candidateReference !== contextReference)) return false;
|
|
44586
45780
|
const contextInitializer = contextReference.resolved?.defs.map((definition) => definition.node).find((definitionNode) => isNodeOfType(definitionNode, "VariableDeclarator"));
|
|
44587
45781
|
if (!contextInitializer || !isNodeOfType(contextInitializer, "VariableDeclarator") || getDeclarationKind(contextInitializer) !== "const" || !contextInitializer.init || !isNodeOfType(contextInitializer.init, "CallExpression")) return false;
|
|
44588
45782
|
const contextHook = stripParenExpression(contextInitializer.init.callee);
|
|
@@ -44596,7 +45790,7 @@ const getImmutableParentCallbackPropName = (analysis, expression) => {
|
|
|
44596
45790
|
while (isNodeOfType(currentExpression, "Identifier")) {
|
|
44597
45791
|
const currentReference = getRef(analysis, currentExpression);
|
|
44598
45792
|
const currentVariable = currentReference?.resolved;
|
|
44599
|
-
if (!currentReference || !currentVariable || visitedVariables.has(currentVariable) || hasMutableBindingWrite(currentReference)) return null;
|
|
45793
|
+
if (!currentReference || !currentVariable || visitedVariables.has(currentVariable) || hasMutableBindingWrite$1(currentReference)) return null;
|
|
44600
45794
|
visitedVariables.add(currentVariable);
|
|
44601
45795
|
const definition = currentVariable.defs.length === 1 ? currentVariable.defs[0] : null;
|
|
44602
45796
|
const bindingIdentifier = definition?.name;
|
|
@@ -44665,7 +45859,7 @@ const getCommandCallbackPropName = (analysis, expression, isReactUseRefCall) =>
|
|
|
44665
45859
|
while (isNodeOfType(currentExpression, "Identifier")) {
|
|
44666
45860
|
const callbackReference = getRef(analysis, currentExpression);
|
|
44667
45861
|
const callbackVariable = callbackReference?.resolved;
|
|
44668
|
-
if (!callbackReference || !callbackVariable || visitedVariables.has(callbackVariable) || hasMutableBindingWrite(callbackReference)) return null;
|
|
45862
|
+
if (!callbackReference || !callbackVariable || visitedVariables.has(callbackVariable) || hasMutableBindingWrite$1(callbackReference)) return null;
|
|
44669
45863
|
visitedVariables.add(callbackVariable);
|
|
44670
45864
|
const definition = callbackVariable.defs.length === 1 ? callbackVariable.defs[0] : null;
|
|
44671
45865
|
const declarator = definition?.node;
|
|
@@ -44685,10 +45879,11 @@ const getCommandCallbackPropName = (analysis, expression, isReactUseRefCall) =>
|
|
|
44685
45879
|
if (!propertyName || !COMMAND_PROP_NAME_PATTERN.test(propertyName)) return null;
|
|
44686
45880
|
return refCurrentObjectPreservesCallbackProperty(analysis, currentExpression.object, propertyName, isReactUseRefCall) ? propertyName : null;
|
|
44687
45881
|
};
|
|
44688
|
-
const isWrapperHookCallbackRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
45882
|
+
const isWrapperHookCallbackRef = (analysis, ref, scopes) => Boolean(ref.resolved?.defs.some((def) => {
|
|
44689
45883
|
const node = def.node;
|
|
44690
45884
|
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
44691
|
-
|
|
45885
|
+
const resultSymbol = isNodeOfType(node.id, "Identifier") ? scopes.symbolFor(node.id) ?? null : null;
|
|
45886
|
+
return getWrapperHookWrappedFunction(unwrapChainExpression(node.init), resultSymbol, scopes) !== null;
|
|
44692
45887
|
}));
|
|
44693
45888
|
const isHandlerBagArgument = (analysis, argument) => {
|
|
44694
45889
|
if (!isNodeOfType(argument, "ObjectExpression")) return false;
|
|
@@ -44707,14 +45902,25 @@ const isHandlerBagArgument = (analysis, argument) => {
|
|
|
44707
45902
|
};
|
|
44708
45903
|
const getFunctionalUpdaterDataRefs = (analysis, updater) => getDownstreamRefs(analysis, updater).filter((updaterRef) => !updaterRef.resolved?.defs.some((def) => def.type === "Parameter" && def.node === updater));
|
|
44709
45904
|
const HOOK_NAME_PATTERN$1 = /^use[A-Z0-9]/;
|
|
44710
|
-
const EXTERNAL_SUBSCRIPTION_HOOK_NAMES = new Set([
|
|
45905
|
+
const EXTERNAL_SUBSCRIPTION_HOOK_NAMES$1 = new Set([
|
|
44711
45906
|
"useIntersectionObserver",
|
|
44712
45907
|
"useMatchMedia",
|
|
45908
|
+
"useMediaJobProgress",
|
|
44713
45909
|
"useMediaQuery",
|
|
44714
45910
|
"useResizeObserver",
|
|
44715
45911
|
"useVisibility",
|
|
44716
45912
|
"useWindowSize"
|
|
44717
45913
|
]);
|
|
45914
|
+
const isCallbackPropReference = (analysis, ref) => {
|
|
45915
|
+
if (!isProp(analysis, ref)) return false;
|
|
45916
|
+
const identifier = ref.identifier;
|
|
45917
|
+
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
45918
|
+
if (!isWholePropsObjectReference(analysis, ref)) return HANDLER_NAMED_PROP_PATTERN.test(identifier.name);
|
|
45919
|
+
const member = identifier.parent;
|
|
45920
|
+
if (!member || !isNodeOfType(member, "MemberExpression") || member.object !== identifier) return false;
|
|
45921
|
+
const propertyName = getStaticMemberPropertyName(member);
|
|
45922
|
+
return Boolean(propertyName && HANDLER_NAMED_PROP_PATTERN.test(propertyName));
|
|
45923
|
+
};
|
|
44718
45924
|
const isParentWiredHookResultRef = (analysis, ref) => Boolean(ref.resolved?.defs.some((def) => {
|
|
44719
45925
|
const node = def.node;
|
|
44720
45926
|
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
@@ -44722,7 +45928,7 @@ const isParentWiredHookResultRef = (analysis, ref) => Boolean(ref.resolved?.defs
|
|
|
44722
45928
|
if (!isNodeOfType(init, "CallExpression")) return false;
|
|
44723
45929
|
const callee = init.callee;
|
|
44724
45930
|
if (!isNodeOfType(callee, "Identifier") || !HOOK_NAME_PATTERN$1.test(callee.name)) return false;
|
|
44725
|
-
return (init.arguments ?? []).some((hookArgument) => getDownstreamRefs(analysis, hookArgument).some((downstreamRef) =>
|
|
45931
|
+
return (init.arguments ?? []).some((hookArgument) => getDownstreamRefs(analysis, hookArgument).some((downstreamRef) => isCallbackPropReference(analysis, downstreamRef)));
|
|
44726
45932
|
}));
|
|
44727
45933
|
const isParentWiredHookResultArgument = (analysis, argument) => {
|
|
44728
45934
|
if (!isNodeOfType(argument, "Identifier")) return false;
|
|
@@ -44735,19 +45941,19 @@ const isParentWiredHookCalleeRef = (analysis, ref) => {
|
|
|
44735
45941
|
if (!isNodeOfType(identifier, "Identifier") || !HOOK_NAME_PATTERN$1.test(identifier.name)) return false;
|
|
44736
45942
|
const parent = identifier.parent;
|
|
44737
45943
|
if (!parent || !isNodeOfType(parent, "CallExpression") || parent.callee !== identifier) return false;
|
|
44738
|
-
return (parent.arguments ?? []).some((hookArgument) => getDownstreamRefs(analysis, hookArgument).some((downstreamRef) =>
|
|
45944
|
+
return (parent.arguments ?? []).some((hookArgument) => getDownstreamRefs(analysis, hookArgument).some((downstreamRef) => isCallbackPropReference(analysis, downstreamRef)));
|
|
44739
45945
|
};
|
|
44740
45946
|
const isExternalSubscriptionHookRef = (ref) => {
|
|
44741
45947
|
const identifier = ref.identifier;
|
|
44742
45948
|
if (!isNodeOfType(identifier, "Identifier")) return false;
|
|
44743
|
-
if (EXTERNAL_SUBSCRIPTION_HOOK_NAMES.has(identifier.name) && isCalleePosition(identifier)) return true;
|
|
45949
|
+
if (EXTERNAL_SUBSCRIPTION_HOOK_NAMES$1.has(identifier.name) && isCalleePosition(identifier)) return true;
|
|
44744
45950
|
return Boolean(ref.resolved?.defs.some((def) => {
|
|
44745
45951
|
const node = def.node;
|
|
44746
45952
|
if (!isNodeOfType(node, "VariableDeclarator") || !node.init) return false;
|
|
44747
45953
|
const initializer = stripParenExpression(node.init);
|
|
44748
45954
|
if (!isNodeOfType(initializer, "CallExpression")) return false;
|
|
44749
45955
|
const callee = stripParenExpression(initializer.callee);
|
|
44750
|
-
return isNodeOfType(callee, "Identifier") && EXTERNAL_SUBSCRIPTION_HOOK_NAMES.has(callee.name);
|
|
45956
|
+
return isNodeOfType(callee, "Identifier") && EXTERNAL_SUBSCRIPTION_HOOK_NAMES$1.has(callee.name);
|
|
44751
45957
|
}));
|
|
44752
45958
|
};
|
|
44753
45959
|
const isImportBindingRef = (ref) => Boolean(ref.resolved?.defs.some((def) => def.type === "ImportBinding"));
|
|
@@ -44783,16 +45989,22 @@ const noPassDataToParent = defineRule({
|
|
|
44783
45989
|
const callExpr = getCallExpr(ref);
|
|
44784
45990
|
if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) continue;
|
|
44785
45991
|
const callbackRefProvenance = getCallbackRefProvenance(analysis, node, callExpr, isReactUseRefCall, isReactUseEffectCall);
|
|
44786
|
-
if (isRefCall(analysis, ref) && !callbackRefProvenance) continue;
|
|
44787
45992
|
if (!isSynchronous(ref.identifier, effectFn)) continue;
|
|
44788
45993
|
const calleeNode = unwrapChainExpression(callExpr.callee);
|
|
44789
45994
|
const identifier = ref.identifier;
|
|
44790
|
-
|
|
44791
|
-
|
|
45995
|
+
const resolvedCallbackPropNames = isNodeOfType(calleeNode, "MemberExpression") && getStaticMemberPropertyName(calleeNode) === "current" ? null : getParentCallbackPropNames({
|
|
45996
|
+
analysis,
|
|
45997
|
+
expression: calleeNode,
|
|
45998
|
+
scopes: context.scopes
|
|
45999
|
+
});
|
|
46000
|
+
const callbackPropNames = callbackRefProvenance?.callbackPropNames ?? resolvedCallbackPropNames;
|
|
46001
|
+
if (isRefCall(analysis, ref) && !callbackPropNames) continue;
|
|
46002
|
+
if (callbackPropNames) {
|
|
46003
|
+
if ([...callbackPropNames].some((callbackPropName) => COMMAND_PROP_NAME_PATTERN.test(callbackPropName))) continue;
|
|
44792
46004
|
} else if (calleeNode === identifier) {
|
|
44793
46005
|
const callbackPropName = getCommandCallbackPropName(analysis, identifier, isReactUseRefCall);
|
|
44794
46006
|
if (callbackPropName && COMMAND_PROP_NAME_PATTERN.test(callbackPropName)) continue;
|
|
44795
|
-
if (!isDirectParentCallbackRef(analysis, ref)) continue;
|
|
46007
|
+
if (!isDirectParentCallbackRef(analysis, ref, context.scopes)) continue;
|
|
44796
46008
|
if (isNodeOfType(identifier, "Identifier") && COMMAND_PROP_NAME_PATTERN.test(identifier.name)) continue;
|
|
44797
46009
|
} else if (isNodeOfType(calleeNode, "MemberExpression") && stripParenExpression(calleeNode.object) === identifier) {
|
|
44798
46010
|
if (!isWholePropsObjectReference(analysis, ref)) continue;
|
|
@@ -44800,10 +46012,10 @@ const noPassDataToParent = defineRule({
|
|
|
44800
46012
|
} else continue;
|
|
44801
46013
|
const methodName = getCallMethodName(calleeNode);
|
|
44802
46014
|
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;
|
|
46015
|
+
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead && !callbackPropNames) continue;
|
|
44804
46016
|
if (methodName && COMMAND_PROP_NAME_PATTERN.test(methodName)) continue;
|
|
44805
|
-
if (!
|
|
44806
|
-
const isSetterNamedCallee =
|
|
46017
|
+
if (!callbackPropNames && isNamespacedApiCallee(calleeNode)) continue;
|
|
46018
|
+
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
46019
|
const isLeafRef = (argRef) => getUpstreamRefs(analysis, argRef).length === 1;
|
|
44808
46020
|
const argsUpstreamRefs = (callExpr.arguments ?? []).flatMap((argument) => {
|
|
44809
46021
|
if (isFunctionLike$1(argument)) {
|
|
@@ -44818,7 +46030,7 @@ const noPassDataToParent = defineRule({
|
|
|
44818
46030
|
}
|
|
44819
46031
|
return getDownstreamRefs(analysis, argument);
|
|
44820
46032
|
}).flatMap((argumentRef) => isExternallyDrivenState(analysis, argumentRef) ? [] : getUpstreamRefs(analysis, argumentRef)).filter(isLeafRef);
|
|
44821
|
-
if (calleeNode === identifier && isWrapperHookCallbackRef(analysis, ref)) argsUpstreamRefs.push(...getArgsUpstreamRefs(analysis, ref).filter(isLeafRef));
|
|
46033
|
+
if (calleeNode === identifier && isWrapperHookCallbackRef(analysis, ref, context.scopes)) argsUpstreamRefs.push(...getArgsUpstreamRefs(analysis, ref).filter(isLeafRef));
|
|
44822
46034
|
if (!argsUpstreamRefs.some((argRef) => {
|
|
44823
46035
|
if (isUseStateIdentifier(argRef.identifier)) return false;
|
|
44824
46036
|
if (isExternalSubscriptionHookRef(argRef)) return false;
|
|
@@ -44856,9 +46068,47 @@ const isCallResultConsumedAsArgument = (callExpression) => {
|
|
|
44856
46068
|
return false;
|
|
44857
46069
|
};
|
|
44858
46070
|
//#endregion
|
|
46071
|
+
//#region src/plugin/rules/state-and-effects/utils/is-custom-hook-state-result-reference.ts
|
|
46072
|
+
const NON_STATE_CUSTOM_HOOK_NAMES = new Set([
|
|
46073
|
+
"useCallbackRef",
|
|
46074
|
+
"useEffectEvent",
|
|
46075
|
+
"useEvent",
|
|
46076
|
+
"useEventCallback",
|
|
46077
|
+
"useLatest",
|
|
46078
|
+
"useMemoizedFn",
|
|
46079
|
+
"useStableCallback"
|
|
46080
|
+
]);
|
|
46081
|
+
const EXTERNAL_SUBSCRIPTION_HOOK_NAMES = new Set([
|
|
46082
|
+
"useIntersectionObserver",
|
|
46083
|
+
"useMatchMedia",
|
|
46084
|
+
"useMediaJobProgress",
|
|
46085
|
+
"useMediaQuery",
|
|
46086
|
+
"useResizeObserver",
|
|
46087
|
+
"useVisibility",
|
|
46088
|
+
"useWindowSize"
|
|
46089
|
+
]);
|
|
46090
|
+
const getHookCalleeName = (initializer) => {
|
|
46091
|
+
const unwrappedInitializer = stripParenExpression(initializer);
|
|
46092
|
+
if (!isNodeOfType(unwrappedInitializer, "CallExpression")) return null;
|
|
46093
|
+
const callee = stripParenExpression(unwrappedInitializer.callee);
|
|
46094
|
+
if (isNodeOfType(callee, "Identifier")) return callee.name;
|
|
46095
|
+
if (isNodeOfType(callee, "MemberExpression") && isNodeOfType(callee.property, "Identifier")) return callee.property.name;
|
|
46096
|
+
return null;
|
|
46097
|
+
};
|
|
46098
|
+
const isCustomHookStateResultReference = (analysis, reference) => Boolean(reference.resolved?.defs.some((definition) => {
|
|
46099
|
+
const declarator = definition.node;
|
|
46100
|
+
if (!isNodeOfType(declarator, "VariableDeclarator") || !declarator.init) return false;
|
|
46101
|
+
const calleeName = getHookCalleeName(declarator.init);
|
|
46102
|
+
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;
|
|
46103
|
+
const initializer = stripParenExpression(declarator.init);
|
|
46104
|
+
if (!isNodeOfType(initializer, "CallExpression")) return false;
|
|
46105
|
+
return initializer.arguments.some((argument) => getDownstreamRefs(analysis, argument).some((argumentReference) => isProp(analysis, argumentReference)));
|
|
46106
|
+
}));
|
|
46107
|
+
//#endregion
|
|
44859
46108
|
//#region src/plugin/rules/state-and-effects/no-pass-live-state-to-parent.ts
|
|
44860
46109
|
const SETTER_NAMED_CALLBACK_PATTERN = /^set[A-Z]/;
|
|
44861
46110
|
const DATA_FETCHING_CALLBACK_PATTERN = /^(fetch|refetch|load|query|request)([A-Z_]|$)/;
|
|
46111
|
+
const hasMutableBindingWrite = (reference) => Boolean(reference.resolved?.references.some((candidateReference) => candidateReference.isWrite() && !candidateReference.init));
|
|
44862
46112
|
const getCallCalleeName = (callExpr) => {
|
|
44863
46113
|
if (!isNodeOfType(callExpr, "CallExpression")) return null;
|
|
44864
46114
|
const callee = callExpr.callee;
|
|
@@ -44903,6 +46153,10 @@ const collectUpstreamStateRefs = (analysis, ref, stateRefs, visited) => {
|
|
|
44903
46153
|
stateRefs.push(ref);
|
|
44904
46154
|
return;
|
|
44905
46155
|
}
|
|
46156
|
+
if (isCustomHookStateResultReference(analysis, ref)) {
|
|
46157
|
+
stateRefs.push(ref);
|
|
46158
|
+
return;
|
|
46159
|
+
}
|
|
44906
46160
|
for (const def of ref.resolved?.defs ?? []) {
|
|
44907
46161
|
if (def.type === "ImportBinding" || def.type === "Parameter") continue;
|
|
44908
46162
|
const defNode = def.node;
|
|
@@ -44932,6 +46186,32 @@ const collectPropCallbackBoundStateRefs = (analysis, ref, isPropCallbackRef) =>
|
|
|
44932
46186
|
}
|
|
44933
46187
|
return stateRefs;
|
|
44934
46188
|
};
|
|
46189
|
+
const collectDirectCallStateRefs = (analysis, callExpression) => {
|
|
46190
|
+
const stateReferences = [];
|
|
46191
|
+
for (const argument of callExpression.arguments) {
|
|
46192
|
+
if (isFunctionLike$1(argument)) continue;
|
|
46193
|
+
for (const argumentReference of getDownstreamRefs(analysis, argument)) {
|
|
46194
|
+
if (resolveToFunction(argumentReference)) continue;
|
|
46195
|
+
collectUpstreamStateRefs(analysis, argumentReference, stateReferences, /* @__PURE__ */ new Set());
|
|
46196
|
+
}
|
|
46197
|
+
}
|
|
46198
|
+
return stateReferences;
|
|
46199
|
+
};
|
|
46200
|
+
const getTransparentWrapperPropReference = (analysis, reference, context) => {
|
|
46201
|
+
for (const definition of reference.resolved?.defs ?? []) {
|
|
46202
|
+
const declarator = definition.node;
|
|
46203
|
+
if (!isNodeOfType(declarator, "VariableDeclarator") || !isNodeOfType(declarator.id, "Identifier") || !declarator.init) continue;
|
|
46204
|
+
const resultSymbol = context.scopes.symbolFor(declarator.id);
|
|
46205
|
+
const callbackArgument = getTransparentReactCallbackWrapperArgument(declarator.init, resultSymbol, context.scopes);
|
|
46206
|
+
if (!callbackArgument) continue;
|
|
46207
|
+
const callbackReferences = getDownstreamRefs(analysis, callbackArgument);
|
|
46208
|
+
const callbackReference = callbackReferences.find((candidateReference) => isPropCallbackInvocationRef(analysis, candidateReference));
|
|
46209
|
+
if (callbackReference) return callbackReference;
|
|
46210
|
+
const propReference = callbackReferences.find((candidateReference) => isProp(analysis, candidateReference) && !candidateReference.resolved?.references.some((candidateUsage) => candidateUsage.isWrite() && !candidateUsage.init));
|
|
46211
|
+
if (propReference) return propReference;
|
|
46212
|
+
}
|
|
46213
|
+
return null;
|
|
46214
|
+
};
|
|
44935
46215
|
const isSetterNamedCallbackReceivingData = (callbackRef) => {
|
|
44936
46216
|
const callExpr = getCallExpr(callbackRef);
|
|
44937
46217
|
if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) return false;
|
|
@@ -44967,6 +46247,16 @@ const resolvesToLocalHookReturnBinding = (ref) => Boolean(ref?.resolved?.defs?.s
|
|
|
44967
46247
|
const calleeName = getInitializerCalleeName(node.init);
|
|
44968
46248
|
return calleeName !== null && isReactHookName(calleeName) && !FUNCTION_WRAPPER_HOOK_NAMES.has(calleeName);
|
|
44969
46249
|
}));
|
|
46250
|
+
const getDirectLocalEffectHelper = (callExpression, effectFunction, context) => {
|
|
46251
|
+
const helperFunction = resolveExactLocalFunction(callExpression.callee, context.scopes);
|
|
46252
|
+
if (!helperFunction) return null;
|
|
46253
|
+
let ancestor = callExpression.parent;
|
|
46254
|
+
while (ancestor && ancestor !== effectFunction) {
|
|
46255
|
+
if (isFunctionLike$1(ancestor)) return null;
|
|
46256
|
+
ancestor = ancestor.parent;
|
|
46257
|
+
}
|
|
46258
|
+
return ancestor === effectFunction ? helperFunction : null;
|
|
46259
|
+
};
|
|
44970
46260
|
const noPassLiveStateToParent = defineRule({
|
|
44971
46261
|
id: "no-pass-live-state-to-parent",
|
|
44972
46262
|
title: "Live state pushed to parent via effect",
|
|
@@ -44981,20 +46271,32 @@ const noPassLiveStateToParent = defineRule({
|
|
|
44981
46271
|
if (!effectFnRefs) return;
|
|
44982
46272
|
const effectFn = getEffectFn(analysis, node);
|
|
44983
46273
|
if (!effectFn) return;
|
|
46274
|
+
const effectFunctionBody = isNodeOfType(effectFn, "ArrowFunctionExpression") || isNodeOfType(effectFn, "FunctionExpression") || isNodeOfType(effectFn, "FunctionDeclaration") ? effectFn.body : null;
|
|
44984
46275
|
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
46276
|
const callExpr = getCallExpr(ref);
|
|
44990
|
-
if (!callExpr) continue;
|
|
46277
|
+
if (!callExpr || !isNodeOfType(callExpr, "CallExpression")) continue;
|
|
46278
|
+
const directLocalEffectHelper = getDirectLocalEffectHelper(callExpr, effectFn, context);
|
|
46279
|
+
const callGraphReferences = directLocalEffectHelper ? [ref, ...getDownstreamRefs(analysis, directLocalEffectHelper)] : [ref];
|
|
46280
|
+
const resolvedCallbackPropNames = getParentCallbackPropNames({
|
|
46281
|
+
analysis,
|
|
46282
|
+
expression: callExpr.callee,
|
|
46283
|
+
scopes: context.scopes
|
|
46284
|
+
});
|
|
46285
|
+
const callExpressionRoot = findTransparentExpressionRoot(callExpr);
|
|
46286
|
+
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;
|
|
46287
|
+
if (!notificationCallbackPropNames && hasMutableBindingWrite(ref)) continue;
|
|
46288
|
+
const propCallbackRefs = callGraphReferences.flatMap((callGraphReference) => getEventualCallRefsTo(analysis, callGraphReference, (innerRef) => isParentNotificationCallbackRef(analysis, innerRef)));
|
|
46289
|
+
const transparentPropReference = propCallbackRefs.length === 0 ? getTransparentWrapperPropReference(analysis, ref, context) : null;
|
|
46290
|
+
if (propCallbackRefs.length === 0 && !transparentPropReference && !notificationCallbackPropNames) continue;
|
|
46291
|
+
if (!notificationCallbackPropNames && resolvesToLocalHookReturnBinding(ref)) continue;
|
|
46292
|
+
if (!isSynchronous(ref.identifier, effectFn) && !directLocalEffectHelper) continue;
|
|
44991
46293
|
if (isCallResultConsumedAsArgument(callExpr)) continue;
|
|
44992
46294
|
const calleeNode = callExpr.callee;
|
|
44993
46295
|
const methodName = calleeNode ? getCallMethodName(calleeNode) : null;
|
|
44994
46296
|
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,
|
|
46297
|
+
if (methodName && DATA_SINK_METHOD_NAMES.has(methodName) && !isPropCallbackNamedLikeStringRead && !notificationCallbackPropNames) continue;
|
|
46298
|
+
if (!notificationCallbackPropNames && calleeNode && isNamespacedApiCallee(calleeNode)) continue;
|
|
46299
|
+
const stateArgRefs = transparentPropReference || notificationCallbackPropNames ? collectDirectCallStateRefs(analysis, callExpr) : callGraphReferences.flatMap((callGraphReference) => collectPropCallbackBoundStateRefs(analysis, callGraphReference, (innerRef) => isParentNotificationCallbackRef(analysis, innerRef)));
|
|
44998
46300
|
const handsSetterNamedCallbackData = propCallbackRefs.some(isSetterNamedCallbackReceivingData);
|
|
44999
46301
|
if (stateArgRefs.length === 0 && !handsSetterNamedCallbackData) continue;
|
|
45000
46302
|
context.report({
|
|
@@ -45387,6 +46689,7 @@ const isStateLikeDependency = (analysis, element, isPropName) => {
|
|
|
45387
46689
|
if (!analysis) return true;
|
|
45388
46690
|
const reference = getRef(analysis, element);
|
|
45389
46691
|
if (!reference) return true;
|
|
46692
|
+
if (isCustomHookStateResultReference(analysis, reference)) return true;
|
|
45390
46693
|
const upstreamReferences = getUpstreamRefs(analysis, reference);
|
|
45391
46694
|
if (upstreamReferences.some((upstreamReference) => isState(analysis, upstreamReference))) return true;
|
|
45392
46695
|
return !upstreamReferences.some((upstreamReference) => isProp(analysis, upstreamReference));
|
|
@@ -45403,6 +46706,22 @@ const getRefHeldPropCallbackName = (callExpression, isPropName) => {
|
|
|
45403
46706
|
if (!callbackArgument || !isNodeOfType(callbackArgument, "Identifier")) return null;
|
|
45404
46707
|
return isPropName(callbackArgument.name) ? callbackArgument.name : null;
|
|
45405
46708
|
};
|
|
46709
|
+
const getTransparentWrappedPropCallbackName = (callExpression, context, isPropName) => {
|
|
46710
|
+
const callee = stripParenExpression(callExpression.callee);
|
|
46711
|
+
if (!isNodeOfType(callee, "Identifier")) return null;
|
|
46712
|
+
const binding = findVariableInitializer(callExpression, callee.name);
|
|
46713
|
+
if (!binding?.initializer) return null;
|
|
46714
|
+
const resultSymbol = context.scopes.symbolFor(callee);
|
|
46715
|
+
const callbackArgument = getTransparentReactCallbackWrapperArgument(binding.initializer, resultSymbol, context.scopes);
|
|
46716
|
+
if (!callbackArgument) return null;
|
|
46717
|
+
const callbackSource = stripParenExpression(callbackArgument);
|
|
46718
|
+
if (isNodeOfType(callbackSource, "Identifier")) return isPropName(callbackSource.name, callbackSource) ? callbackSource.name : null;
|
|
46719
|
+
if (!isNodeOfType(callbackSource, "MemberExpression")) return null;
|
|
46720
|
+
const receiver = stripParenExpression(callbackSource.object);
|
|
46721
|
+
const propertyName = getStaticPropertyName(callbackSource);
|
|
46722
|
+
if (!isNodeOfType(receiver, "Identifier") || !propertyName) return null;
|
|
46723
|
+
return isPropName(receiver.name, receiver) ? propertyName : null;
|
|
46724
|
+
};
|
|
45406
46725
|
const noPropCallbackInEffect = defineRule({
|
|
45407
46726
|
id: "no-prop-callback-in-effect",
|
|
45408
46727
|
title: "Parent kept in sync with a callback effect",
|
|
@@ -45436,9 +46755,16 @@ const noPropCallbackInEffect = defineRule({
|
|
|
45436
46755
|
walkInsideStatementBlocks(callback.body, (child) => {
|
|
45437
46756
|
if (!isNodeOfType(child, "CallExpression")) return;
|
|
45438
46757
|
const directCallee = stripParenExpression(child.callee);
|
|
45439
|
-
const
|
|
46758
|
+
const resolvedCallbackPropNames = analysis && propStackTracker.getCurrentPropNames().size > 0 ? getParentCallbackPropNames({
|
|
46759
|
+
analysis,
|
|
46760
|
+
expression: directCallee,
|
|
46761
|
+
scopes: context.scopes
|
|
46762
|
+
}) : null;
|
|
46763
|
+
const calleeName = resolvedCallbackPropNames && [...resolvedCallbackPropNames][0] || isNodeOfType(directCallee, "Identifier") && propStackTracker.isPropName(directCallee.name) && directCallee.name || getRefHeldPropCallbackName(child, propStackTracker.isPropName) || getTransparentWrappedPropCallbackName(child, context, propStackTracker.isPropName);
|
|
45440
46764
|
if (!calleeName) return;
|
|
45441
|
-
|
|
46765
|
+
const callExpressionRoot = findTransparentExpressionRoot(child);
|
|
46766
|
+
const isDirectEffectReturn = isNodeOfType(callExpressionRoot.parent, "ReturnStatement") && callExpressionRoot.parent.parent === callback.body;
|
|
46767
|
+
if (!isResultDiscardedCall(child) && !isDirectEffectReturn) return;
|
|
45442
46768
|
if (reportedNodes.has(child)) return;
|
|
45443
46769
|
reportedNodes.add(child);
|
|
45444
46770
|
context.report({
|
|
@@ -46271,6 +47597,69 @@ const noRedundantShouldComponentUpdate = defineRule({
|
|
|
46271
47597
|
}
|
|
46272
47598
|
});
|
|
46273
47599
|
//#endregion
|
|
47600
|
+
//#region src/plugin/rules/correctness/no-ref-callback-cleanup-before-react-19.ts
|
|
47601
|
+
const resolveFunctionExpressions = (rawExpression, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
47602
|
+
const expression = stripParenExpression(rawExpression);
|
|
47603
|
+
if (isFunctionLike$1(expression)) return expression.async || expression.generator ? [] : [expression];
|
|
47604
|
+
if (isNodeOfType(expression, "ConditionalExpression")) {
|
|
47605
|
+
if (isNodeOfType(expression.test, "Literal")) return resolveFunctionExpressions(expression.test.value ? expression.consequent : expression.alternate, scopes, visitedSymbolIds);
|
|
47606
|
+
return [...resolveFunctionExpressions(expression.consequent, scopes, visitedSymbolIds), ...resolveFunctionExpressions(expression.alternate, scopes, visitedSymbolIds)];
|
|
47607
|
+
}
|
|
47608
|
+
if (isNodeOfType(expression, "LogicalExpression")) {
|
|
47609
|
+
if (isNodeOfType(expression.left, "Literal")) {
|
|
47610
|
+
const isLeftTruthy = Boolean(expression.left.value);
|
|
47611
|
+
if (expression.operator === "&&" && !isLeftTruthy) return [];
|
|
47612
|
+
if (expression.operator === "||" && isLeftTruthy) return [];
|
|
47613
|
+
if (expression.operator === "??" && expression.left.value !== null) return [];
|
|
47614
|
+
}
|
|
47615
|
+
if (expression.operator === "&&") return resolveFunctionExpressions(expression.right, scopes, visitedSymbolIds);
|
|
47616
|
+
return [...resolveFunctionExpressions(expression.left, scopes, visitedSymbolIds), ...resolveFunctionExpressions(expression.right, scopes, visitedSymbolIds)];
|
|
47617
|
+
}
|
|
47618
|
+
if (isNodeOfType(expression, "SequenceExpression")) {
|
|
47619
|
+
const finalExpression = expression.expressions.at(-1);
|
|
47620
|
+
return finalExpression ? resolveFunctionExpressions(finalExpression, scopes, visitedSymbolIds) : [];
|
|
47621
|
+
}
|
|
47622
|
+
if (isNodeOfType(expression, "CallExpression")) {
|
|
47623
|
+
if (!isReactApiCall(expression, "useCallback", scopes)) return [];
|
|
47624
|
+
const callback = expression.arguments[0];
|
|
47625
|
+
return callback && !isNodeOfType(callback, "SpreadElement") ? resolveFunctionExpressions(callback, scopes, visitedSymbolIds) : [];
|
|
47626
|
+
}
|
|
47627
|
+
if (!isNodeOfType(expression, "Identifier")) return [];
|
|
47628
|
+
const symbol = scopes.symbolFor(expression);
|
|
47629
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return [];
|
|
47630
|
+
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]));
|
|
47631
|
+
const initializer = getDirectConstInitializer(symbol);
|
|
47632
|
+
if (!initializer) return [];
|
|
47633
|
+
return resolveFunctionExpressions(initializer, scopes, new Set([...visitedSymbolIds, symbol.id]));
|
|
47634
|
+
};
|
|
47635
|
+
const functionReturnsCleanupFunction = (functionExpression, scopes) => {
|
|
47636
|
+
if (!isFunctionLike$1(functionExpression)) return false;
|
|
47637
|
+
if (!isNodeOfType(functionExpression.body, "BlockStatement")) return resolveFunctionExpressions(functionExpression.body, scopes).length > 0;
|
|
47638
|
+
return collectFunctionReturnStatements(functionExpression).some((returnStatement) => Boolean(returnStatement.argument && resolveFunctionExpressions(returnStatement.argument, scopes).length > 0));
|
|
47639
|
+
};
|
|
47640
|
+
const callbackReturnsCleanupFunction = (callback, scopes) => {
|
|
47641
|
+
return resolveFunctionExpressions(callback, scopes).some((functionExpression) => functionReturnsCleanupFunction(functionExpression, scopes));
|
|
47642
|
+
};
|
|
47643
|
+
const noRefCallbackCleanupBeforeReact19 = defineRule({
|
|
47644
|
+
id: "no-ref-callback-cleanup-before-react-19",
|
|
47645
|
+
title: "Ref cleanup requires React 19",
|
|
47646
|
+
requires: ["react:18"],
|
|
47647
|
+
disabledWhen: ["react:19"],
|
|
47648
|
+
severity: "warn",
|
|
47649
|
+
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.",
|
|
47650
|
+
create: (context) => ({ JSXAttribute(node) {
|
|
47651
|
+
if (getJsxAttributeName(node.name) !== "ref") return;
|
|
47652
|
+
if (!isNodeOfType(node.value, "JSXExpressionContainer")) return;
|
|
47653
|
+
const callback = node.value.expression;
|
|
47654
|
+
if (!callback || isNodeOfType(callback, "JSXEmptyExpression")) return;
|
|
47655
|
+
if (!callbackReturnsCleanupFunction(callback, context.scopes)) return;
|
|
47656
|
+
context.report({
|
|
47657
|
+
node,
|
|
47658
|
+
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."
|
|
47659
|
+
});
|
|
47660
|
+
} })
|
|
47661
|
+
});
|
|
47662
|
+
//#endregion
|
|
46274
47663
|
//#region src/plugin/rules/state-and-effects/no-ref-current-in-render.ts
|
|
46275
47664
|
const REPEATED_ANCESTOR_TYPES = new Set([
|
|
46276
47665
|
"DoWhileStatement",
|
|
@@ -46866,7 +48255,7 @@ const doConditionsImplyFormula = (conditions, target) => {
|
|
|
46866
48255
|
}
|
|
46867
48256
|
return facts.didConflict || evaluateBooleanFormula$1(target, facts.assignments) === true;
|
|
46868
48257
|
};
|
|
46869
|
-
const getFunctionBindingSymbol = (functionNode, scopes) => {
|
|
48258
|
+
const getFunctionBindingSymbol$1 = (functionNode, scopes) => {
|
|
46870
48259
|
if (isNodeOfType(functionNode, "FunctionDeclaration") && functionNode.id) return scopes.symbolFor(functionNode.id);
|
|
46871
48260
|
const parent = functionNode.parent;
|
|
46872
48261
|
if ((isNodeOfType(functionNode, "ArrowFunctionExpression") || isNodeOfType(functionNode, "FunctionExpression")) && isNodeOfType(parent, "VariableDeclarator") && parent.init === functionNode && isNodeOfType(parent.id, "Identifier")) return scopes.symbolFor(parent.id);
|
|
@@ -46899,7 +48288,7 @@ const isNodeEvaluatedDuringRender = (node, componentNode, scopes, visitedFunctio
|
|
|
46899
48288
|
const synchronousCallbackCall = getSynchronousCallbackCall(functionNode);
|
|
46900
48289
|
if (synchronousCallbackCall) return isNodeEvaluatedDuringRender(synchronousCallbackCall, componentNode, scopes, visitedFunctionSymbolIds);
|
|
46901
48290
|
if (executesDuringRender(functionNode, scopes)) return isNodeEvaluatedDuringRender(functionNode.parent ?? functionNode, componentNode, scopes, visitedFunctionSymbolIds);
|
|
46902
|
-
const functionSymbol = getFunctionBindingSymbol(functionNode, scopes);
|
|
48291
|
+
const functionSymbol = getFunctionBindingSymbol$1(functionNode, scopes);
|
|
46903
48292
|
if (!functionSymbol || visitedFunctionSymbolIds.has(functionSymbol.id)) return false;
|
|
46904
48293
|
visitedFunctionSymbolIds.add(functionSymbol.id);
|
|
46905
48294
|
let callCount = 0;
|
|
@@ -46948,7 +48337,7 @@ const collectExposureConditions = (analysis, context, node, componentNode, prote
|
|
|
46948
48337
|
parent = synchronousCallbackCall.parent;
|
|
46949
48338
|
continue;
|
|
46950
48339
|
}
|
|
46951
|
-
const functionSymbol = getFunctionBindingSymbol(parent, context.scopes);
|
|
48340
|
+
const functionSymbol = getFunctionBindingSymbol$1(parent, context.scopes);
|
|
46952
48341
|
if (functionSymbol?.references.length === 1) {
|
|
46953
48342
|
const callExpression = isReferenceDirectlyCalled(functionSymbol.references[0].identifier);
|
|
46954
48343
|
if (callExpression) {
|
|
@@ -47158,7 +48547,7 @@ const getSetterExposureConditions = (analysis, context, setterReference, compone
|
|
|
47158
48547
|
const functionNode = findEnclosingFunction$1(setterReference.identifier);
|
|
47159
48548
|
if (!functionNode) return null;
|
|
47160
48549
|
if (isInlineJsxCallback(functionNode)) return [collectExposureConditions(analysis, context, functionNode, componentNode, protectedSymbolIds)];
|
|
47161
|
-
const functionSymbol = getFunctionBindingSymbol(functionNode, context.scopes);
|
|
48550
|
+
const functionSymbol = getFunctionBindingSymbol$1(functionNode, context.scopes);
|
|
47162
48551
|
if (!functionSymbol || functionSymbol.references.length === 0) return null;
|
|
47163
48552
|
const conditionsByReference = [];
|
|
47164
48553
|
for (const reference of functionSymbol.references) {
|
|
@@ -53031,12 +54420,6 @@ const isInsideEs6Component$1 = (methodDefinition) => {
|
|
|
53031
54420
|
if (!owningClass) return false;
|
|
53032
54421
|
return isPreactOrReactComponentClass(owningClass);
|
|
53033
54422
|
};
|
|
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
54423
|
const preactNoRenderArguments = defineRule({
|
|
53041
54424
|
id: "preact-no-render-arguments",
|
|
53042
54425
|
title: "render() reads props from arguments",
|
|
@@ -56238,8 +57621,39 @@ const isUseStateSetterInScope = (node, setterName) => isHookBindingInScope(node,
|
|
|
56238
57621
|
destructureIndex: 1
|
|
56239
57622
|
});
|
|
56240
57623
|
//#endregion
|
|
57624
|
+
//#region src/plugin/utils/unwrap-return-expression.ts
|
|
57625
|
+
const unwrapReturnExpression = (node) => isNodeOfType(node, "ReturnStatement") && node.argument ? node.argument : node;
|
|
57626
|
+
//#endregion
|
|
56241
57627
|
//#region src/plugin/rules/performance/rendering-hydration-no-flicker.ts
|
|
56242
57628
|
const USE_EFFECT_ONLY = new Set(["useEffect"]);
|
|
57629
|
+
const USE_CALLBACK_ONLY = new Set(["useCallback"]);
|
|
57630
|
+
const USE_STATE_ONLY = new Set(["useState"]);
|
|
57631
|
+
const REACT_API_CALL_OPTIONS = {
|
|
57632
|
+
allowGlobalReactNamespace: true,
|
|
57633
|
+
allowUnboundBareCalls: true,
|
|
57634
|
+
resolveNamedAliases: true
|
|
57635
|
+
};
|
|
57636
|
+
const expressionReadsDerivedSymbol = (context, expression, stateDerivedSymbolIds) => {
|
|
57637
|
+
let readsDerivedSymbol = false;
|
|
57638
|
+
walkAst(expression, (node) => {
|
|
57639
|
+
if (readsDerivedSymbol) return false;
|
|
57640
|
+
if (node !== expression && isFunctionLike$1(node)) return false;
|
|
57641
|
+
if (isNodeOfType(node, "Identifier") && stateDerivedSymbolIds.has(context.scopes.symbolFor(node)?.id ?? -1)) readsDerivedSymbol = true;
|
|
57642
|
+
});
|
|
57643
|
+
return readsDerivedSymbol;
|
|
57644
|
+
};
|
|
57645
|
+
const getStaticObjectPropertyName = (property) => {
|
|
57646
|
+
if (!isNodeOfType(property, "Property") || property.computed || property.method || property.kind !== "init") return null;
|
|
57647
|
+
if (isNodeOfType(property.key, "Identifier")) return property.key.name;
|
|
57648
|
+
if (isNodeOfType(property.key, "Literal") && (typeof property.key.value === "string" || typeof property.key.value === "number")) return String(property.key.value);
|
|
57649
|
+
return null;
|
|
57650
|
+
};
|
|
57651
|
+
const isNonVisibleJsxSpreadProperty = (propertyName) => propertyName === "id" || propertyName.startsWith("aria-") || /^on[A-Z]/.test(propertyName);
|
|
57652
|
+
const isTransparentAssignmentTarget = (identifier) => {
|
|
57653
|
+
const expressionRoot = findTransparentExpressionRoot(identifier);
|
|
57654
|
+
const parent = expressionRoot.parent;
|
|
57655
|
+
return Boolean(isNodeOfType(parent, "AssignmentExpression") && parent.left === expressionRoot || isNodeOfType(parent, "UpdateExpression") && parent.argument === expressionRoot || isNodeOfType(parent, "UnaryExpression") && parent.operator === "delete" && parent.argument === expressionRoot);
|
|
57656
|
+
};
|
|
56243
57657
|
const argumentsReadRefCurrent = (callArguments) => callArguments.some((argument) => {
|
|
56244
57658
|
let readsCurrent = false;
|
|
56245
57659
|
walkAst(argument, (child) => {
|
|
@@ -56291,6 +57705,166 @@ const isStateUsedOnlyInIdOrAriaAttributes = (setterCall, setterName) => {
|
|
|
56291
57705
|
});
|
|
56292
57706
|
return referenceCount > 0 && !nonAriaReferenceFound;
|
|
56293
57707
|
};
|
|
57708
|
+
const isGlobalWindowMember = (context, node, propertyName) => {
|
|
57709
|
+
const member = stripParenExpression(node);
|
|
57710
|
+
if (!isNodeOfType(member, "MemberExpression") || member.computed) return false;
|
|
57711
|
+
const receiver = stripParenExpression(member.object);
|
|
57712
|
+
return isNodeOfType(receiver, "Identifier") && receiver.name === "window" && context.scopes.isGlobalReference(receiver) && isNodeOfType(member.property, "Identifier") && member.property.name === propertyName;
|
|
57713
|
+
};
|
|
57714
|
+
const getDirectWindowWidthSetter = (context, statement) => {
|
|
57715
|
+
const call = unwrapDiscardedExpression(statement);
|
|
57716
|
+
if (!isNodeOfType(call, "CallExpression") || call.arguments?.length !== 1) return null;
|
|
57717
|
+
if (!isNodeOfType(call.callee, "Identifier") || !isSetterCall(call)) return null;
|
|
57718
|
+
const argument = call.arguments[0];
|
|
57719
|
+
return isGlobalWindowMember(context, argument, "innerWidth") ? call : null;
|
|
57720
|
+
};
|
|
57721
|
+
const getResizeListenerHandler = (context, statement, methodName) => {
|
|
57722
|
+
const call = unwrapDiscardedExpression(statement);
|
|
57723
|
+
if (!isNodeOfType(call, "CallExpression") || call.arguments?.length !== 2) return null;
|
|
57724
|
+
if (!isGlobalWindowMember(context, call.callee, methodName)) return null;
|
|
57725
|
+
const eventName = call.arguments[0];
|
|
57726
|
+
const handler = call.arguments[1];
|
|
57727
|
+
if (!isNodeOfType(eventName, "Literal") || eventName.value !== "resize") return null;
|
|
57728
|
+
return isNodeOfType(handler, "Identifier") ? handler : null;
|
|
57729
|
+
};
|
|
57730
|
+
const getCleanupResizeHandler = (context, statement) => {
|
|
57731
|
+
if (!isNodeOfType(statement, "ReturnStatement") || !isFunctionLike$1(statement.argument)) return null;
|
|
57732
|
+
const cleanupStatements = getCallbackStatements(statement.argument);
|
|
57733
|
+
if (cleanupStatements.length !== 1) return null;
|
|
57734
|
+
return getResizeListenerHandler(context, unwrapReturnExpression(cleanupStatements[0]), "removeEventListener");
|
|
57735
|
+
};
|
|
57736
|
+
const findExactViewportState = (context, componentFunction, setterCall) => {
|
|
57737
|
+
if (!isFunctionLike$1(componentFunction) || !isNodeOfType(componentFunction.body, "BlockStatement")) return null;
|
|
57738
|
+
const componentBody = componentFunction.body;
|
|
57739
|
+
if (!isNodeOfType(setterCall.callee, "Identifier")) return null;
|
|
57740
|
+
const setterSymbol = context.scopes.symbolFor(setterCall.callee);
|
|
57741
|
+
if (!setterSymbol || setterSymbol.kind !== "const" || !isNodeOfType(setterSymbol.declarationNode, "VariableDeclarator")) return null;
|
|
57742
|
+
const declarator = setterSymbol.declarationNode;
|
|
57743
|
+
if (!isNodeOfType(declarator.id, "ArrayPattern")) return null;
|
|
57744
|
+
const stateIdentifier = declarator.id.elements?.[0];
|
|
57745
|
+
const setterIdentifier = declarator.id.elements?.[1];
|
|
57746
|
+
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;
|
|
57747
|
+
const initializer = declarator.init.arguments?.[0];
|
|
57748
|
+
if (!isNodeOfType(initializer, "Literal") || initializer.value !== 0) return null;
|
|
57749
|
+
const stateSymbol = context.scopes.symbolFor(stateIdentifier);
|
|
57750
|
+
if (!stateSymbol) return null;
|
|
57751
|
+
const stateDerivedSymbolIds = new Set([stateSymbol.id]);
|
|
57752
|
+
let didAddDerivedSymbol = true;
|
|
57753
|
+
while (didAddDerivedSymbol) {
|
|
57754
|
+
didAddDerivedSymbol = false;
|
|
57755
|
+
for (const statement of componentBody.body ?? []) {
|
|
57756
|
+
if (!isNodeOfType(statement, "VariableDeclaration")) continue;
|
|
57757
|
+
for (const candidateDeclarator of statement.declarations ?? []) {
|
|
57758
|
+
if (!isNodeOfType(candidateDeclarator.id, "Identifier") || !candidateDeclarator.init) continue;
|
|
57759
|
+
const candidateInitializer = stripParenExpression(candidateDeclarator.init);
|
|
57760
|
+
if (isFunctionLike$1(candidateInitializer) || isNodeOfType(candidateInitializer, "CallExpression") && isReactApiCall(candidateInitializer, USE_CALLBACK_ONLY, context.scopes, REACT_API_CALL_OPTIONS)) continue;
|
|
57761
|
+
if (!expressionReadsDerivedSymbol(context, candidateInitializer, stateDerivedSymbolIds)) continue;
|
|
57762
|
+
const candidateSymbol = context.scopes.symbolFor(candidateDeclarator.id);
|
|
57763
|
+
if (candidateSymbol?.kind === "const" && candidateSymbol.references.every((reference) => reference.flag === "read" && !isTransparentAssignmentTarget(reference.identifier)) && !stateDerivedSymbolIds.has(candidateSymbol.id)) {
|
|
57764
|
+
stateDerivedSymbolIds.add(candidateSymbol.id);
|
|
57765
|
+
didAddDerivedSymbol = true;
|
|
57766
|
+
}
|
|
57767
|
+
}
|
|
57768
|
+
}
|
|
57769
|
+
}
|
|
57770
|
+
const staticSpreadVisibilityBySymbolId = /* @__PURE__ */ new Map();
|
|
57771
|
+
const hasOnlyStaticObjectReferences = (identifier, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
57772
|
+
const symbol = context.scopes.symbolFor(identifier);
|
|
57773
|
+
if (!symbol) return false;
|
|
57774
|
+
if (visitedSymbolIds.has(symbol.id)) return true;
|
|
57775
|
+
const nextVisitedSymbolIds = new Set(visitedSymbolIds);
|
|
57776
|
+
nextVisitedSymbolIds.add(symbol.id);
|
|
57777
|
+
let hasUnknownReference = false;
|
|
57778
|
+
walkAst(componentBody, (node) => {
|
|
57779
|
+
if (hasUnknownReference || !isNodeOfType(node, "Identifier") || context.scopes.symbolFor(node)?.id !== symbol.id || node === symbol.bindingIdentifier) return;
|
|
57780
|
+
const referenceRoot = findTransparentExpressionRoot(node);
|
|
57781
|
+
const parent = referenceRoot.parent;
|
|
57782
|
+
if (isNodeOfType(parent, "JSXSpreadAttribute") && parent.argument === referenceRoot) return;
|
|
57783
|
+
if (isNodeOfType(parent, "VariableDeclarator") && parent.init === referenceRoot && isNodeOfType(parent.id, "Identifier") && isNodeOfType(parent.parent, "VariableDeclaration") && parent.parent.kind === "const" && hasOnlyStaticObjectReferences(parent.id, nextVisitedSymbolIds)) return;
|
|
57784
|
+
hasUnknownReference = true;
|
|
57785
|
+
return false;
|
|
57786
|
+
});
|
|
57787
|
+
return !hasUnknownReference;
|
|
57788
|
+
};
|
|
57789
|
+
const classifyStaticSpreadObject = (identifier, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
57790
|
+
const symbol = context.scopes.symbolFor(identifier);
|
|
57791
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return "unknown";
|
|
57792
|
+
const cachedVisibility = staticSpreadVisibilityBySymbolId.get(symbol.id);
|
|
57793
|
+
if (cachedVisibility) return cachedVisibility;
|
|
57794
|
+
if (symbol.kind !== "const" || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || !isNodeOfType(symbol.declarationNode.id, "Identifier") || symbol.declarationNode.id !== symbol.bindingIdentifier || !symbol.declarationNode.init) return "unknown";
|
|
57795
|
+
if (!hasOnlyStaticObjectReferences(identifier)) return "unknown";
|
|
57796
|
+
const initializer = stripParenExpression(symbol.declarationNode.init);
|
|
57797
|
+
const nextVisitedSymbolIds = new Set(visitedSymbolIds);
|
|
57798
|
+
nextVisitedSymbolIds.add(symbol.id);
|
|
57799
|
+
if (isNodeOfType(initializer, "Identifier")) {
|
|
57800
|
+
const visibility = classifyStaticSpreadObject(initializer, nextVisitedSymbolIds);
|
|
57801
|
+
staticSpreadVisibilityBySymbolId.set(symbol.id, visibility);
|
|
57802
|
+
return visibility;
|
|
57803
|
+
}
|
|
57804
|
+
if (!isNodeOfType(initializer, "ObjectExpression")) return "unknown";
|
|
57805
|
+
let visibility = "non-visible";
|
|
57806
|
+
for (const property of initializer.properties ?? []) {
|
|
57807
|
+
const propertyName = getStaticObjectPropertyName(property);
|
|
57808
|
+
if (!isNodeOfType(property, "Property") || !propertyName) {
|
|
57809
|
+
visibility = "unknown";
|
|
57810
|
+
break;
|
|
57811
|
+
}
|
|
57812
|
+
if (expressionReadsDerivedSymbol(context, property.value, stateDerivedSymbolIds) && !isNonVisibleJsxSpreadProperty(propertyName)) visibility = "visible";
|
|
57813
|
+
}
|
|
57814
|
+
staticSpreadVisibilityBySymbolId.set(symbol.id, visibility);
|
|
57815
|
+
return visibility;
|
|
57816
|
+
};
|
|
57817
|
+
let hasNonAriaReference = false;
|
|
57818
|
+
walkAst(componentBody, (node) => {
|
|
57819
|
+
if (hasNonAriaReference) return false;
|
|
57820
|
+
if (!isNodeOfType(node, "Identifier") || !stateDerivedSymbolIds.has(context.scopes.symbolFor(node)?.id ?? -1)) return;
|
|
57821
|
+
if (findEnclosingFunction$1(node) !== componentFunction) return;
|
|
57822
|
+
const parent = node.parent;
|
|
57823
|
+
if (parent && (isNodeOfType(parent, "MemberExpression") && parent.property === node && !parent.computed || isNodeOfType(parent, "Property") && parent.key === node && !parent.computed)) return;
|
|
57824
|
+
let cursor = parent;
|
|
57825
|
+
while (cursor && cursor !== componentBody) {
|
|
57826
|
+
if (isFunctionLike$1(cursor)) return;
|
|
57827
|
+
if (isNodeOfType(cursor, "JSXSpreadAttribute")) {
|
|
57828
|
+
if (isNodeOfType(node, "Identifier") && classifyStaticSpreadObject(node) === "visible") hasNonAriaReference = true;
|
|
57829
|
+
return;
|
|
57830
|
+
}
|
|
57831
|
+
if (isNodeOfType(cursor, "JSXAttribute")) {
|
|
57832
|
+
if (isEventHandlerAttribute(cursor)) return;
|
|
57833
|
+
if (!isInsideIdOrAriaAttribute(node)) hasNonAriaReference = true;
|
|
57834
|
+
return;
|
|
57835
|
+
}
|
|
57836
|
+
if (isNodeOfType(cursor, "ReturnStatement")) {
|
|
57837
|
+
hasNonAriaReference = true;
|
|
57838
|
+
return;
|
|
57839
|
+
}
|
|
57840
|
+
cursor = cursor.parent;
|
|
57841
|
+
}
|
|
57842
|
+
});
|
|
57843
|
+
return hasNonAriaReference ? stateIdentifier.name : null;
|
|
57844
|
+
};
|
|
57845
|
+
const isExactViewportSubscriptionEffect = (context, effectCall, callback) => {
|
|
57846
|
+
if (!isReactApiCall(effectCall, USE_EFFECT_ONLY, context.scopes, REACT_API_CALL_OPTIONS)) return false;
|
|
57847
|
+
if (!isFunctionLike$1(callback) || callback.async || !isNodeOfType(callback.body, "BlockStatement")) return false;
|
|
57848
|
+
const statements = getCallbackStatements(callback);
|
|
57849
|
+
if (statements.length !== 4) return false;
|
|
57850
|
+
const handlerDeclaration = statements[0];
|
|
57851
|
+
if (!isNodeOfType(handlerDeclaration, "VariableDeclaration") || handlerDeclaration.kind !== "const" || handlerDeclaration.declarations?.length !== 1) return false;
|
|
57852
|
+
const handlerDeclarator = handlerDeclaration.declarations[0];
|
|
57853
|
+
if (!isNodeOfType(handlerDeclarator.id, "Identifier") || !isFunctionLike$1(handlerDeclarator.init)) return false;
|
|
57854
|
+
const handlerStatements = getCallbackStatements(handlerDeclarator.init);
|
|
57855
|
+
if (handlerStatements.length !== 1) return false;
|
|
57856
|
+
const handlerSetter = getDirectWindowWidthSetter(context, unwrapReturnExpression(handlerStatements[0]));
|
|
57857
|
+
const subscribedHandler = getResizeListenerHandler(context, statements[1], "addEventListener");
|
|
57858
|
+
const immediateSetter = getDirectWindowWidthSetter(context, statements[2]);
|
|
57859
|
+
const cleanupHandler = getCleanupResizeHandler(context, statements[3]);
|
|
57860
|
+
if (!handlerSetter || !subscribedHandler || !immediateSetter || !cleanupHandler) return false;
|
|
57861
|
+
const handlerSymbol = context.scopes.symbolFor(handlerDeclarator.id);
|
|
57862
|
+
if (!handlerSymbol || context.scopes.symbolFor(subscribedHandler) !== handlerSymbol || context.scopes.symbolFor(cleanupHandler) !== handlerSymbol) return false;
|
|
57863
|
+
if (!isNodeOfType(handlerSetter.callee, "Identifier") || !isNodeOfType(immediateSetter.callee, "Identifier") || context.scopes.symbolFor(handlerSetter.callee) !== context.scopes.symbolFor(immediateSetter.callee)) return false;
|
|
57864
|
+
const componentFunction = findEnclosingFunction$1(effectCall);
|
|
57865
|
+
if (!isFunctionLike$1(componentFunction) || !isNodeOfType(componentFunction.body, "BlockStatement")) return false;
|
|
57866
|
+
return findExactViewportState(context, componentFunction, immediateSetter) !== null;
|
|
57867
|
+
};
|
|
56294
57868
|
const renderingHydrationNoFlicker = defineRule({
|
|
56295
57869
|
id: "rendering-hydration-no-flicker",
|
|
56296
57870
|
title: "useEffect setState flashes on mount",
|
|
@@ -56303,7 +57877,14 @@ const renderingHydrationNoFlicker = defineRule({
|
|
|
56303
57877
|
if (!isNodeOfType(depsNode, "ArrayExpression") || depsNode.elements?.length !== 0) return;
|
|
56304
57878
|
const callback = getEffectCallback(node);
|
|
56305
57879
|
if (!callback || !isNodeOfType(callback, "ArrowFunctionExpression") && !isNodeOfType(callback, "FunctionExpression")) return;
|
|
56306
|
-
|
|
57880
|
+
if (isExactViewportSubscriptionEffect(context, node, callback)) {
|
|
57881
|
+
context.report({
|
|
57882
|
+
node,
|
|
57883
|
+
message: "This flashes for your users because useEffect(setState, []) runs after the first paint, so use useSyncExternalStore, or add suppressHydrationWarning"
|
|
57884
|
+
});
|
|
57885
|
+
return;
|
|
57886
|
+
}
|
|
57887
|
+
const bodyStatements = getCallbackStatements(callback);
|
|
56307
57888
|
if (bodyStatements.length !== 1) return;
|
|
56308
57889
|
const soleStatement = bodyStatements[0];
|
|
56309
57890
|
if (!isNodeOfType(soleStatement, "ExpressionStatement")) return;
|
|
@@ -64945,6 +66526,109 @@ const isDeferrableSideEffectCall = (objectName, methodName) => {
|
|
|
64945
66526
|
if (ANALYTICS_DEFERRABLE_OBJECTS.has(objectName)) return ANALYTICS_DEFERRABLE_METHODS.has(methodName);
|
|
64946
66527
|
return false;
|
|
64947
66528
|
};
|
|
66529
|
+
const NEXT_SERVER_SOURCE = "next/server";
|
|
66530
|
+
const NEXT_AFTER_EXPORT_NAMES = new Set(["after", "unstable_after"]);
|
|
66531
|
+
const isNextAfterImportSymbol = (symbol, contextNode) => {
|
|
66532
|
+
if (symbol.kind !== "import") return false;
|
|
66533
|
+
const importBinding = getImportBindingForName(contextNode, symbol.name);
|
|
66534
|
+
return Boolean(importBinding && importBinding.source === NEXT_SERVER_SOURCE && !importBinding.isNamespace && importBinding.exportedName && NEXT_AFTER_EXPORT_NAMES.has(importBinding.exportedName));
|
|
66535
|
+
};
|
|
66536
|
+
const isDirectObjectPatternBinding = (symbol) => {
|
|
66537
|
+
if (!isNodeOfType(symbol.declarationNode, "VariableDeclarator")) return false;
|
|
66538
|
+
if (!isNodeOfType(symbol.declarationNode.id, "ObjectPattern")) return false;
|
|
66539
|
+
let bindingNode = symbol.bindingIdentifier;
|
|
66540
|
+
if (isNodeOfType(bindingNode.parent, "AssignmentPattern") && bindingNode.parent.left === bindingNode) bindingNode = bindingNode.parent;
|
|
66541
|
+
const property = bindingNode.parent;
|
|
66542
|
+
return Boolean(isNodeOfType(property, "Property") && property.value === bindingNode && property.parent === symbol.declarationNode.id);
|
|
66543
|
+
};
|
|
66544
|
+
const isNextServerNamespace = (expression, contextNode, scopes) => {
|
|
66545
|
+
let candidate = stripParenExpression(expression);
|
|
66546
|
+
const visitedSymbolIds = /* @__PURE__ */ new Set();
|
|
66547
|
+
while (isNodeOfType(candidate, "Identifier")) {
|
|
66548
|
+
const symbol = scopes.symbolFor(candidate);
|
|
66549
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
|
|
66550
|
+
if (symbol.kind === "import") {
|
|
66551
|
+
const importBinding = getImportBindingForName(contextNode, symbol.name);
|
|
66552
|
+
return Boolean(importBinding?.source === NEXT_SERVER_SOURCE && importBinding.isNamespace);
|
|
66553
|
+
}
|
|
66554
|
+
if (symbol.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return false;
|
|
66555
|
+
visitedSymbolIds.add(symbol.id);
|
|
66556
|
+
candidate = stripParenExpression(symbol.initializer);
|
|
66557
|
+
}
|
|
66558
|
+
return false;
|
|
66559
|
+
};
|
|
66560
|
+
const isNextAfterCallee = (callee, contextNode, scopes, visitedSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
66561
|
+
const candidate = stripParenExpression(callee);
|
|
66562
|
+
if (isNodeOfType(candidate, "MemberExpression")) {
|
|
66563
|
+
const propertyName = getStaticPropertyKeyName(candidate, { allowComputedString: true });
|
|
66564
|
+
return Boolean(propertyName && NEXT_AFTER_EXPORT_NAMES.has(propertyName) && isNextServerNamespace(candidate.object, contextNode, scopes));
|
|
66565
|
+
}
|
|
66566
|
+
if (!isNodeOfType(candidate, "Identifier")) return false;
|
|
66567
|
+
const symbol = scopes.symbolFor(candidate);
|
|
66568
|
+
if (!symbol || visitedSymbolIds.has(symbol.id)) return false;
|
|
66569
|
+
if (isNextAfterImportSymbol(symbol, contextNode)) return true;
|
|
66570
|
+
const destructuredPropertyName = getDestructuredBindingPropertyName(symbol.bindingIdentifier);
|
|
66571
|
+
if (symbol.kind === "const" && symbol.initializer && isDirectObjectPatternBinding(symbol) && destructuredPropertyName && NEXT_AFTER_EXPORT_NAMES.has(destructuredPropertyName)) return isNextServerNamespace(symbol.initializer, contextNode, scopes);
|
|
66572
|
+
if (symbol.kind !== "const" || !symbol.initializer || !isNodeOfType(symbol.declarationNode, "VariableDeclarator") || symbol.declarationNode.id !== symbol.bindingIdentifier) return false;
|
|
66573
|
+
visitedSymbolIds.add(symbol.id);
|
|
66574
|
+
return isNextAfterCallee(symbol.initializer, contextNode, scopes, visitedSymbolIds);
|
|
66575
|
+
};
|
|
66576
|
+
const getDirectArgumentCall = (expression) => {
|
|
66577
|
+
const expressionRoot = findTransparentExpressionRoot(expression);
|
|
66578
|
+
const parent = expressionRoot.parent;
|
|
66579
|
+
if (!isNodeOfType(parent, "CallExpression")) return null;
|
|
66580
|
+
return parent.arguments[0] === expressionRoot ? parent : null;
|
|
66581
|
+
};
|
|
66582
|
+
const isScheduledByNextAfter = (expression, scopes) => {
|
|
66583
|
+
const callExpression = getDirectArgumentCall(expression);
|
|
66584
|
+
return Boolean(callExpression && isNextAfterCallee(callExpression.callee, callExpression, scopes));
|
|
66585
|
+
};
|
|
66586
|
+
const getFunctionBindingSymbol = (functionNode, scopes) => {
|
|
66587
|
+
if (isNodeOfType(functionNode, "FunctionDeclaration") && functionNode.id) return scopes.scopeFor(functionNode).symbols.find((symbol) => symbol.declarationNode === functionNode) ?? null;
|
|
66588
|
+
const functionRoot = findTransparentExpressionRoot(functionNode);
|
|
66589
|
+
const parent = functionRoot.parent;
|
|
66590
|
+
if (!isNodeOfType(parent, "VariableDeclarator") || parent.init !== functionRoot || !isNodeOfType(parent.id, "Identifier")) return null;
|
|
66591
|
+
return scopes.symbolFor(parent.id);
|
|
66592
|
+
};
|
|
66593
|
+
const isDirectlyExported = (symbol) => {
|
|
66594
|
+
let declaration = symbol.declarationNode;
|
|
66595
|
+
if (isNodeOfType(declaration, "VariableDeclarator")) declaration = declaration.parent;
|
|
66596
|
+
return Boolean(declaration?.parent && (isNodeOfType(declaration.parent, "ExportNamedDeclaration") || isNodeOfType(declaration.parent, "ExportDefaultDeclaration")));
|
|
66597
|
+
};
|
|
66598
|
+
const isLexicallyInsideFunction = (node, functionNode) => {
|
|
66599
|
+
let enclosingFunction = findEnclosingFunction$1(node);
|
|
66600
|
+
while (enclosingFunction) {
|
|
66601
|
+
if (enclosingFunction === functionNode) return true;
|
|
66602
|
+
enclosingFunction = findEnclosingFunction$1(enclosingFunction);
|
|
66603
|
+
}
|
|
66604
|
+
return false;
|
|
66605
|
+
};
|
|
66606
|
+
const isExclusivelyScheduledByNextAfter = (functionNode, scopes, visitedFunctionSymbolIds) => {
|
|
66607
|
+
if (isScheduledByNextAfter(functionNode, scopes)) return true;
|
|
66608
|
+
const functionSymbol = getFunctionBindingSymbol(functionNode, scopes);
|
|
66609
|
+
if (!functionSymbol || isDirectlyExported(functionSymbol) || visitedFunctionSymbolIds.has(functionSymbol.id)) return false;
|
|
66610
|
+
const nextVisitedFunctionSymbolIds = new Set(visitedFunctionSymbolIds).add(functionSymbol.id);
|
|
66611
|
+
let hasAfterUse = false;
|
|
66612
|
+
for (const reference of functionSymbol.references) {
|
|
66613
|
+
if (reference.flag !== "read") return false;
|
|
66614
|
+
if (isLexicallyInsideFunction(reference.identifier, functionNode)) continue;
|
|
66615
|
+
if (isScheduledByNextAfter(reference.identifier, scopes)) {
|
|
66616
|
+
hasAfterUse = true;
|
|
66617
|
+
continue;
|
|
66618
|
+
}
|
|
66619
|
+
if (!isInsideNextAfterCallback(reference.identifier, scopes, nextVisitedFunctionSymbolIds)) return false;
|
|
66620
|
+
hasAfterUse = true;
|
|
66621
|
+
}
|
|
66622
|
+
return hasAfterUse;
|
|
66623
|
+
};
|
|
66624
|
+
const isInsideNextAfterCallback = (node, scopes, visitedFunctionSymbolIds = /* @__PURE__ */ new Set()) => {
|
|
66625
|
+
let enclosingFunction = findEnclosingFunction$1(node);
|
|
66626
|
+
while (enclosingFunction) {
|
|
66627
|
+
if (isExclusivelyScheduledByNextAfter(enclosingFunction, scopes, visitedFunctionSymbolIds)) return true;
|
|
66628
|
+
enclosingFunction = findEnclosingFunction$1(enclosingFunction);
|
|
66629
|
+
}
|
|
66630
|
+
return false;
|
|
66631
|
+
};
|
|
64948
66632
|
const serverAfterNonblocking = defineRule({
|
|
64949
66633
|
id: "server-after-nonblocking",
|
|
64950
66634
|
title: "Blocking side effect before response",
|
|
@@ -64979,6 +66663,7 @@ const serverAfterNonblocking = defineRule({
|
|
|
64979
66663
|
if (!objectName) return;
|
|
64980
66664
|
const methodName = node.callee.property.name;
|
|
64981
66665
|
if (!isDeferrableSideEffectCall(objectName, methodName)) return;
|
|
66666
|
+
if (isInsideNextAfterCallback(node, context.scopes)) return;
|
|
64982
66667
|
context.report({
|
|
64983
66668
|
node,
|
|
64984
66669
|
message: `${objectName}.${methodName}() runs before the response, so your users wait longer for it.`
|
|
@@ -66031,14 +67716,6 @@ const isStateKey = (key) => {
|
|
|
66031
67716
|
if (isNodeOfType(key, "Literal") && typeof key.value === "string") return key.value === "state";
|
|
66032
67717
|
return false;
|
|
66033
67718
|
};
|
|
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
67719
|
const isInConstructor = (node) => {
|
|
66043
67720
|
let ancestor = node.parent;
|
|
66044
67721
|
while (ancestor) {
|
|
@@ -66253,17 +67930,34 @@ const stylePropObject = defineRule({
|
|
|
66253
67930
|
};
|
|
66254
67931
|
}
|
|
66255
67932
|
});
|
|
67933
|
+
//#endregion
|
|
67934
|
+
//#region src/plugin/rules/security-scan/utils/has-use-server-directive-in-content.ts
|
|
67935
|
+
const hasUseServerDirectiveInContent = (content, relativePath = "source.tsx") => {
|
|
67936
|
+
const programNode = parseSourceText({
|
|
67937
|
+
filename: relativePath,
|
|
67938
|
+
sourceText: content,
|
|
67939
|
+
shouldAttachParentReferences: false
|
|
67940
|
+
});
|
|
67941
|
+
return programNode === null ? false : hasDirective(programNode, "use server");
|
|
67942
|
+
};
|
|
67943
|
+
//#endregion
|
|
67944
|
+
//#region src/plugin/rules/security-scan/supabase-client-owned-authz-field.ts
|
|
67945
|
+
const scanSupabaseClientOwnedAuthzField = scanByPattern({
|
|
67946
|
+
shouldScan: (file) => isClientSourcePath(file.relativePath),
|
|
67947
|
+
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/,
|
|
67948
|
+
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],
|
|
67949
|
+
message: "Client Supabase code appears to write user, tenant, owner, or role fields that should be enforced by RLS."
|
|
67950
|
+
});
|
|
66256
67951
|
const supabaseClientOwnedAuthzField = defineRule({
|
|
66257
67952
|
id: "supabase-client-owned-authz-field",
|
|
66258
67953
|
title: "Client writes Supabase authorization field",
|
|
66259
67954
|
severity: "error",
|
|
66260
67955
|
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
|
-
})
|
|
67956
|
+
scan: (file) => {
|
|
67957
|
+
const findings = scanSupabaseClientOwnedAuthzField(file);
|
|
67958
|
+
if (findings.length === 0) return findings;
|
|
67959
|
+
return hasUseServerDirectiveInContent(file.content, file.relativePath) ? [] : findings;
|
|
67960
|
+
}
|
|
66267
67961
|
});
|
|
66268
67962
|
//#endregion
|
|
66269
67963
|
//#region src/plugin/rules/security-scan/utils/is-supabase-migration-path.ts
|
|
@@ -70733,6 +72427,17 @@ const reactDoctorRules = [
|
|
|
70733
72427
|
requires: [...new Set(["react", ...noRedundantShouldComponentUpdate.requires ?? []])]
|
|
70734
72428
|
}
|
|
70735
72429
|
},
|
|
72430
|
+
{
|
|
72431
|
+
key: "react-doctor/no-ref-callback-cleanup-before-react-19",
|
|
72432
|
+
id: "no-ref-callback-cleanup-before-react-19",
|
|
72433
|
+
source: "react-doctor",
|
|
72434
|
+
originallyExternal: false,
|
|
72435
|
+
rule: {
|
|
72436
|
+
...noRefCallbackCleanupBeforeReact19,
|
|
72437
|
+
framework: "global",
|
|
72438
|
+
category: "Bugs"
|
|
72439
|
+
}
|
|
72440
|
+
},
|
|
70736
72441
|
{
|
|
70737
72442
|
key: "react-doctor/no-ref-current-in-render",
|
|
70738
72443
|
id: "no-ref-current-in-render",
|