oxlint-plugin-react-doctor 0.7.9-dev.6c5c91b → 0.7.9-dev.71892af
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +212 -117
- 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
|
|
@@ -9083,119 +9285,6 @@ const findReExportTargetsForName = (programRoot, exportedName) => {
|
|
|
9083
9285
|
return exportAllTargets;
|
|
9084
9286
|
};
|
|
9085
9287
|
//#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
9288
|
//#region src/plugin/utils/resolve-relative-import-path.ts
|
|
9200
9289
|
const MODULE_FILE_EXTENSIONS = [
|
|
9201
9290
|
".ts",
|
|
@@ -15016,6 +15105,11 @@ const doesResourceResultEscape = (resourceNode, allowReturnedResourceEscape, all
|
|
|
15016
15105
|
parentNode = currentNode.parent;
|
|
15017
15106
|
continue;
|
|
15018
15107
|
}
|
|
15108
|
+
if (isNodeOfType(parentNode, "ConditionalExpression") && (parentNode.consequent === currentNode || parentNode.alternate === currentNode) || isNodeOfType(parentNode, "LogicalExpression") && (parentNode.right === currentNode || parentNode.left === currentNode && parentNode.operator !== "&&")) {
|
|
15109
|
+
currentNode = parentNode;
|
|
15110
|
+
parentNode = currentNode.parent;
|
|
15111
|
+
continue;
|
|
15112
|
+
}
|
|
15019
15113
|
if (isNodeOfType(parentNode, "VariableDeclarator") && parentNode.init === currentNode && isNodeOfType(parentNode.id, "Identifier") && isNodeOfType(parentNode.parent, "VariableDeclaration") && parentNode.parent.kind === "const") {
|
|
15020
15114
|
const ownerFunction = findEnclosingFunction$1(resourceNode);
|
|
15021
15115
|
const resourceSymbol = context.scopes.symbolFor(parentNode.id);
|
|
@@ -34082,6 +34176,7 @@ const noChainStateUpdates = defineRule({
|
|
|
34082
34176
|
id: "no-chain-state-updates",
|
|
34083
34177
|
title: "State updates chained through effects",
|
|
34084
34178
|
severity: "warn",
|
|
34179
|
+
disabledWhen: ["react:18"],
|
|
34085
34180
|
tags: ["test-noise"],
|
|
34086
34181
|
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",
|
|
34087
34182
|
create: (context) => ({ CallExpression(node) {
|