mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 03:01:35 +00:00
4516 lines
155 KiB
JavaScript
4516 lines
155 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
// Guards database-first state ownership by blocking legacy store writes in runtime code.
|
|
import { promises as fs } from "node:fs";
|
|
import path from "node:path";
|
|
import ts from "typescript";
|
|
import {
|
|
explicitUndefinedLegacyObjectPropertyValue,
|
|
mergeConditionalLegacyObjectPropertyValue,
|
|
mergeConditionalLiteralTexts,
|
|
mergeExhaustiveLiteralTexts,
|
|
mergeLegacyObjectPropertyValues,
|
|
mergeLegacyPathBranchAssignments,
|
|
} from "./lib/legacy-store-path-domain.mjs";
|
|
import { resolveRepoRoot, runAsScript, toLine, unwrapExpression } from "./lib/ts-guard-utils.mjs";
|
|
|
|
const databaseFirstLegacyStoreSourceRoots = ["src", "extensions", "packages"];
|
|
const databaseFirstNativeSourceRoots = ["apps/macos/Sources/OpenClaw"];
|
|
const nativeLegacyPortGuardianMigrationPath =
|
|
"apps/macos/Sources/OpenClaw/PortGuardianRecordStore.swift";
|
|
const nativeLegacyPortGuardianFilenamePattern = /\bport-guard\.(?:json|lock)\b/u;
|
|
const unknownComputedPropertyName = "\0";
|
|
|
|
const legacyWriteCallees = new Set([
|
|
"appendFile",
|
|
"appendFileSync",
|
|
"cp",
|
|
"cpSync",
|
|
"copyFile",
|
|
"copyFileSync",
|
|
"createWriteStream",
|
|
"open",
|
|
"openSync",
|
|
"rm",
|
|
"rmSync",
|
|
"unlink",
|
|
"unlinkSync",
|
|
"rename",
|
|
"renameSync",
|
|
"writeFile",
|
|
"writeFileSync",
|
|
]);
|
|
|
|
const fsModuleSpecifiers = new Set(["node:fs", "node:fs/promises", "fs", "fs/promises"]);
|
|
|
|
const helperWriteCallees = new Set([
|
|
"acquireFileLock",
|
|
"appendRegularFile",
|
|
"appendRegularFileSync",
|
|
"replaceFileAtomic",
|
|
"replaceFileAtomicSync",
|
|
"saveJsonFile",
|
|
"writeJson",
|
|
"writeJsonAtomic",
|
|
"writeJsonFileAtomically",
|
|
"writeJsonSync",
|
|
"writeTextAtomic",
|
|
"withFileLock",
|
|
]);
|
|
|
|
const fsSafeStoreFactoryCallees = new Set([
|
|
"fileStore",
|
|
"fileStoreSync",
|
|
"privateFileStore",
|
|
"privateFileStoreSync",
|
|
"root",
|
|
]);
|
|
const fsSafeJsonStoreFactoryCallees = new Set(["jsonStore"]);
|
|
|
|
const fsSafeStoreWriteMethods = new Set([
|
|
"append",
|
|
"copyIn",
|
|
"create",
|
|
"createJson",
|
|
"mkdir",
|
|
"move",
|
|
"openWritable",
|
|
"remove",
|
|
"write",
|
|
"writeJson",
|
|
"writeStream",
|
|
"writeText",
|
|
]);
|
|
const fsSafeJsonStoreWriteMethods = new Set(["update", "updateOr", "write"]);
|
|
|
|
const helperWriteModulePattern =
|
|
/(?:^|\/)(?:file-lock|fs-safe|json-files|json-store|private-file-store|replace-file)(?:\.[cm]?[jt]s)?$/u;
|
|
const fsSafePackageModulePattern = /^@openclaw\/fs-safe(?:\/(?:root|store))?$/u;
|
|
|
|
const bridgeMarkerPattern = /\btranscriptLocator\b|sqlite-transcript:\/\//u;
|
|
|
|
// The restart handoff must survive its one cutover migration without leaving
|
|
// filesystem fallback imports in the steady-state runtime owner.
|
|
const legacyRestartSentinelMigrationPath = "src/infra/state-migrations.restart-sentinel.ts";
|
|
const legacyRestartSentinelPreflightPath = "src/cli/program/config-guard.ts";
|
|
const legacyRestartSentinelRuntimePath = "src/infra/restart-sentinel.ts";
|
|
const legacyRestartSentinelPreflightFilenames = new Set([
|
|
"restart-sentinel.json",
|
|
"restart-sentinel.json.doctor-importing",
|
|
]);
|
|
const legacyRestartSentinelFilenamePattern =
|
|
/(?:^|[/\\])restart-sentinel\.json(?:\.doctor-importing)?$/u;
|
|
const legacyRestartSentinelRuntimeImportSpecifiers = new Set([
|
|
"fs",
|
|
"fs/promises",
|
|
"node:fs",
|
|
"node:fs/promises",
|
|
"node:path",
|
|
"path",
|
|
]);
|
|
const legacyExecApprovalsMigrationPath = "src/infra/state-migrations.exec-approvals.ts";
|
|
const legacyExecApprovalsConfigPath = "src/infra/exec-approvals-config.ts";
|
|
const legacyExecApprovalsRuntimePath = "src/infra/exec-approvals-store.ts";
|
|
// Stable oc:// URI identity, not a filesystem path; see the owning module.
|
|
const stableExecApprovalsPolicyUriPath = "extensions/policy/src/exec-approvals-uri.ts";
|
|
const legacyExecApprovalsFilenamePattern =
|
|
/(?:^|[/\\])exec-approvals\.json(?:\.doctor-importing)?$/u;
|
|
|
|
const legacyStorePatterns = [
|
|
/\bsessions\.json\b/u,
|
|
/\.trajectory\.jsonl\b/u,
|
|
/\.acp-stream\.jsonl\b/u,
|
|
/\bacp\/event-ledger\.json\b/u,
|
|
/\bcache\/[^"'`]*\.json\b/u,
|
|
/\bagents\/[^"'`]+\/agent\/(?:auth|models)\.json\b/u,
|
|
/\b(?:credentials\/oauth|github-copilot\.token|openrouter-models|auth-profiles|auth-state|exec-approvals|(?:openclaw-)?workspace-state)\.json\b/u,
|
|
// Dynamic template spans resolve to `*`, so the start alternative also
|
|
// catches `${workspaceKey}.attested` and `${workspaceDir}.attested`.
|
|
/(?:^|[/\\])[^/\\"'`]+\.attested\b/u,
|
|
/\btui\/last-session\.json\b/u,
|
|
/\bcommitments\/commitments\.json\b/u,
|
|
/\bmedia\/outgoing\/records\/[^"'`]*\.json\b/u,
|
|
/\bpush\/(?:apns-registrations|web-push-subscriptions|vapid-keys)\.json\b/u,
|
|
/\bmcp-oauth\/[^"'`]*\.json\b/u,
|
|
/\bnode\.json\b/u,
|
|
/\bidentity\/device\.json\b/u,
|
|
/\bsubagents\/runs\.json\b/u,
|
|
/\btmp\/skill-uploads\b/u,
|
|
/\b(?:crestodian|openclaw)\/rescue-pending\/[^"'`]*\.json\b/u,
|
|
/\bcron\/(?:runs\/[^"'`]+\.jsonl|jobs\.json|jobs-state\.json)\b/u,
|
|
/\b(?:process-leases|session-toggles|known-users|msteams-conversations|msteams-polls|msteams-sso-tokens|bot-storage|sync-store|thread-bindings|inbound-dedupe|startup-verification|storage-meta|crypto-idb-snapshot|command-deploy-cache|plugin-binding-approvals|plugins\/installs|config-health|port-guard|restart-sentinel|gateway-restart-intent|gateway-supervisor-restart-handoff)\.json\b/u,
|
|
/\b(?:calls|ref-index|config-audit|audit\/(?:file-transfer|openclaw|system-agent|crestodian))\.jsonl\b/u,
|
|
/\b(?:reply-cache|sent-echoes|events|claims)\.jsonl\b/u,
|
|
/\bplugin-state\/state\.sqlite\b/u,
|
|
/\btasks\/(?:runs\.sqlite|flows\/registry\.sqlite)\b/u,
|
|
/\bopenclaw-state\.sqlite\b/u,
|
|
/\bopenclaw-native-hook-relays\b/u,
|
|
/(?:^|\/)(?:meta|file-meta)\.json$/u,
|
|
/(?:^|\/)viewer\.html$/u,
|
|
/(?:^|\/)qmd\/embed\.lock(?:\.lock)?$/u,
|
|
/(?:^|\/)qmd-write\.lock(?:\.lock)?$/u,
|
|
];
|
|
|
|
const allowedRuntimeMigrationPaths = [
|
|
"src/commands/doctor/",
|
|
"src/commands/doctor-usage-cost-cache.ts",
|
|
"src/infra/session-state-migration.ts",
|
|
"src/infra/state-migrations.ts",
|
|
"src/infra/state-migrations.acp-replay.ts",
|
|
"src/infra/state-migrations.tui-last-session.ts",
|
|
"src/infra/state-migrations.commitments.ts",
|
|
"src/infra/state-migrations.managed-outgoing-images.ts",
|
|
"src/infra/state-migrations.apns.ts",
|
|
"src/infra/state-migrations.mcp-oauth.ts",
|
|
legacyExecApprovalsMigrationPath,
|
|
legacyRestartSentinelMigrationPath,
|
|
"src/infra/state-migrations.workspace-setup.ts",
|
|
"src/infra/state-migrations.web-push.ts",
|
|
"src/infra/state-migrations.node-host.ts",
|
|
"src/infra/state-migrations.device-identity.ts",
|
|
"src/infra/state-migrations.subagent-registry.ts",
|
|
"src/infra/state-migrations.rescue-pending.ts",
|
|
"src/commands/session-state-migration.ts",
|
|
"src/commands/doctor-state-migrations.test.ts",
|
|
];
|
|
|
|
const allowedFixturePaths = new Set(["extensions/qa-lab/src/providers/shared/auth-store.ts"]);
|
|
|
|
const allowedCurrentLegacyWriteViolations = [];
|
|
|
|
const sourceFileExtensions = new Set([".cjs", ".cts", ".js", ".mjs", ".mts", ".ts", ".tsx"]);
|
|
|
|
const sourceTestSuffixes = [
|
|
".e2e-harness.js",
|
|
".e2e-harness.mjs",
|
|
".e2e-harness.ts",
|
|
".test-fixtures.js",
|
|
".test-fixtures.mjs",
|
|
".test-fixtures.ts",
|
|
".test-helper.js",
|
|
".test-helper.mjs",
|
|
".test-helper.ts",
|
|
".test-helpers.js",
|
|
".test-helpers.mjs",
|
|
".test-helpers.ts",
|
|
".test-harness.js",
|
|
".test-harness.mjs",
|
|
".test-harness.ts",
|
|
".test-mocks.js",
|
|
".test-mocks.mjs",
|
|
".test-mocks.ts",
|
|
".test-support.js",
|
|
".test-support.mjs",
|
|
".test-support.ts",
|
|
".test-utils.js",
|
|
".test-utils.mjs",
|
|
".test-utils.ts",
|
|
".test.js",
|
|
".test.mjs",
|
|
".test.ts",
|
|
"test-fixtures.js",
|
|
"test-fixtures.mjs",
|
|
"test-fixtures.ts",
|
|
"test-helper.js",
|
|
"test-helper.mjs",
|
|
"test-helper.ts",
|
|
"test-helpers.js",
|
|
"test-helpers.mjs",
|
|
"test-helpers.ts",
|
|
"test-harness.js",
|
|
"test-harness.mjs",
|
|
"test-harness.ts",
|
|
"test-mocks.js",
|
|
"test-mocks.mjs",
|
|
"test-mocks.ts",
|
|
"test-support.js",
|
|
"test-support.mjs",
|
|
"test-support.ts",
|
|
"test-utils.js",
|
|
"test-utils.mjs",
|
|
"test-utils.ts",
|
|
];
|
|
|
|
function isAllowedLegacyOwnerPath(relativePath) {
|
|
return (
|
|
allowedFixturePaths.has(relativePath) ||
|
|
allowedRuntimeMigrationPaths.some((allowed) => relativePath.startsWith(allowed)) ||
|
|
/^extensions\/[^/]+\/(?:doctor-contract-api|legacy-state-migrations-api)\.ts$/u.test(
|
|
relativePath,
|
|
)
|
|
);
|
|
}
|
|
|
|
function normalizedSourceText(sourceFile, node) {
|
|
return node.getText(sourceFile).replace(/\s+/gu, " ");
|
|
}
|
|
|
|
function lastScope(scopes) {
|
|
return scopes[scopes.length - 1];
|
|
}
|
|
|
|
function scopeForRead(scopes, name) {
|
|
return scopes.findLast((scope) => scope.has(name));
|
|
}
|
|
|
|
function scopeForWrite(scopes, name) {
|
|
return scopeForRead(scopes, name) ?? lastScope(scopes);
|
|
}
|
|
|
|
function currentLegacyWriteViolationAllowances(relativePath = null) {
|
|
const allowances = new Map();
|
|
const relativePrefix = typeof relativePath === "string" ? relativePath.concat(":") : null;
|
|
for (const fingerprint of allowedCurrentLegacyWriteViolations) {
|
|
if (relativePrefix !== null && !fingerprint.startsWith(relativePrefix)) {
|
|
continue;
|
|
}
|
|
allowances.set(fingerprint, (allowances.get(fingerprint) ?? 0) + 1);
|
|
}
|
|
return allowances;
|
|
}
|
|
|
|
function currentLegacyWriteViolationPath(fingerprint) {
|
|
const marker = ":legacy store filesystem write:";
|
|
const markerIndex = fingerprint.indexOf(marker);
|
|
return markerIndex === -1 ? null : fingerprint.slice(0, markerIndex);
|
|
}
|
|
|
|
function consumeAllowedCurrentLegacyViolation(
|
|
allowances,
|
|
relativePath,
|
|
sourceFile,
|
|
fingerprintNode,
|
|
kind,
|
|
) {
|
|
const fingerprint = `${relativePath}:${kind}:${normalizedSourceText(sourceFile, fingerprintNode)}`;
|
|
const remaining = allowances.get(fingerprint) ?? 0;
|
|
if (remaining === 0) {
|
|
return false;
|
|
}
|
|
if (remaining === 1) {
|
|
allowances.delete(fingerprint);
|
|
} else {
|
|
allowances.set(fingerprint, remaining - 1);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function isSourceFile(filePath) {
|
|
return sourceFileExtensions.has(path.extname(filePath));
|
|
}
|
|
|
|
function isGeneratedAssetSourceFile(filePath) {
|
|
const normalized = filePath.replaceAll(path.sep, "/");
|
|
return (
|
|
/(?:^|\/)extensions\/[^/]+\/(?:assets|dist)\/.+\.[cm]?js$/u.test(normalized) ||
|
|
/(?:^|\/)packages\/[^/]+\/dist\/.+\.[cm]?js$/u.test(normalized)
|
|
);
|
|
}
|
|
|
|
function isGeneratedAssetSourcePath(filePath) {
|
|
return (
|
|
/(?:^|\/)extensions\/[^/]+\/(?:assets|dist)(?:\/|$)/u.test(
|
|
filePath.replaceAll(path.sep, "/"),
|
|
) || /(?:^|\/)packages\/[^/]+\/dist(?:\/|$)/u.test(filePath.replaceAll(path.sep, "/"))
|
|
);
|
|
}
|
|
|
|
function isTestLikeSourceFile(filePath) {
|
|
return sourceTestSuffixes.some((suffix) => filePath.endsWith(suffix));
|
|
}
|
|
|
|
async function collectSourceFiles(targetPath) {
|
|
let stat;
|
|
try {
|
|
stat = await fs.stat(targetPath);
|
|
} catch (error) {
|
|
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
return [];
|
|
}
|
|
throw error;
|
|
}
|
|
|
|
if (stat.isFile()) {
|
|
return isSourceFile(targetPath) &&
|
|
!isTestLikeSourceFile(targetPath) &&
|
|
!isGeneratedAssetSourceFile(targetPath)
|
|
? [targetPath]
|
|
: [];
|
|
}
|
|
|
|
const entries = await fs.readdir(targetPath, { withFileTypes: true });
|
|
const files = [];
|
|
for (const entry of entries) {
|
|
if (entry.name === "node_modules") {
|
|
continue;
|
|
}
|
|
const entryPath = path.join(targetPath, entry.name);
|
|
if (isGeneratedAssetSourcePath(entryPath)) {
|
|
continue;
|
|
}
|
|
if (entry.isDirectory()) {
|
|
files.push(...(await collectSourceFiles(entryPath)));
|
|
continue;
|
|
}
|
|
if (
|
|
entry.isFile() &&
|
|
isSourceFile(entryPath) &&
|
|
!isTestLikeSourceFile(entryPath) &&
|
|
!isGeneratedAssetSourceFile(entryPath)
|
|
) {
|
|
files.push(entryPath);
|
|
}
|
|
}
|
|
return files;
|
|
}
|
|
|
|
export async function collectDatabaseFirstLegacyStoreSourceFiles(sourceRoots) {
|
|
return (await Promise.all(sourceRoots.map((root) => collectSourceFiles(root)))).flat();
|
|
}
|
|
|
|
async function collectNativeSourceFiles(targetPath) {
|
|
let stat;
|
|
try {
|
|
stat = await fs.stat(targetPath);
|
|
} catch (error) {
|
|
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
return [];
|
|
}
|
|
throw error;
|
|
}
|
|
if (stat.isFile()) {
|
|
return path.extname(targetPath) === ".swift" ? [targetPath] : [];
|
|
}
|
|
const entries = await fs.readdir(targetPath, { withFileTypes: true });
|
|
const files = [];
|
|
for (const entry of entries) {
|
|
const entryPath = path.join(targetPath, entry.name);
|
|
if (entry.isDirectory()) {
|
|
files.push(...(await collectNativeSourceFiles(entryPath)));
|
|
} else if (entry.isFile() && path.extname(entryPath) === ".swift") {
|
|
files.push(entryPath);
|
|
}
|
|
}
|
|
return files;
|
|
}
|
|
|
|
export function collectDatabaseFirstNativeLegacyStoreViolations(content, relativePath) {
|
|
if (relativePath === nativeLegacyPortGuardianMigrationPath) {
|
|
return [];
|
|
}
|
|
return content
|
|
.split("\n")
|
|
.flatMap((line, index) =>
|
|
nativeLegacyPortGuardianFilenamePattern.test(line)
|
|
? [{ kind: "legacy PortGuardian file reference", line: index + 1 }]
|
|
: [],
|
|
);
|
|
}
|
|
|
|
function importSource(node) {
|
|
const moduleSpecifier = node.moduleSpecifier;
|
|
return ts.isStringLiteral(moduleSpecifier) ? moduleSpecifier.text : "";
|
|
}
|
|
|
|
function isLegacyRestartSentinelPreflightDetection(node, relativePath) {
|
|
if (
|
|
relativePath !== legacyRestartSentinelPreflightPath ||
|
|
!legacyRestartSentinelPreflightFilenames.has(node.text)
|
|
) {
|
|
return false;
|
|
}
|
|
const joinCall = node.parent;
|
|
if (
|
|
!ts.isCallExpression(joinCall) ||
|
|
joinCall.arguments.length !== 2 ||
|
|
joinCall.arguments[1] !== node ||
|
|
!ts.isPropertyAccessExpression(joinCall.expression) ||
|
|
!ts.isIdentifier(joinCall.expression.expression) ||
|
|
joinCall.expression.expression.text !== "path" ||
|
|
joinCall.expression.name.text !== "join" ||
|
|
!ts.isIdentifier(joinCall.arguments[0]) ||
|
|
joinCall.arguments[0].text !== "stateDir"
|
|
) {
|
|
return false;
|
|
}
|
|
const paths = joinCall.parent;
|
|
if (!ts.isArrayLiteralExpression(paths)) {
|
|
return false;
|
|
}
|
|
const someAccess = paths.parent;
|
|
if (
|
|
!ts.isPropertyAccessExpression(someAccess) ||
|
|
someAccess.expression !== paths ||
|
|
someAccess.name.text !== "some"
|
|
) {
|
|
return false;
|
|
}
|
|
const someCall = someAccess.parent;
|
|
return (
|
|
ts.isCallExpression(someCall) &&
|
|
someCall.arguments.length === 1 &&
|
|
ts.isIdentifier(someCall.arguments[0]) &&
|
|
someCall.arguments[0].text === "fileOrDirExists"
|
|
);
|
|
}
|
|
|
|
function collectLegacyRestartSentinelBoundaryViolations(sourceFile, relativePath) {
|
|
if (relativePath === legacyRestartSentinelMigrationPath) {
|
|
return [];
|
|
}
|
|
|
|
const violations = [];
|
|
const seen = new Set();
|
|
function add(node, kind) {
|
|
const line = toLine(sourceFile, node);
|
|
const key = `${line}:${kind}`;
|
|
if (seen.has(key)) {
|
|
return;
|
|
}
|
|
seen.add(key);
|
|
violations.push({ kind, line });
|
|
}
|
|
|
|
function visit(node) {
|
|
if (
|
|
ts.isStringLiteralLike(node) &&
|
|
legacyRestartSentinelFilenamePattern.test(node.text) &&
|
|
!isLegacyRestartSentinelPreflightDetection(node, relativePath)
|
|
) {
|
|
add(node, "legacy restart sentinel reference");
|
|
}
|
|
if (
|
|
relativePath === legacyRestartSentinelRuntimePath &&
|
|
ts.isImportDeclaration(node) &&
|
|
legacyRestartSentinelRuntimeImportSpecifiers.has(importSource(node))
|
|
) {
|
|
add(node, "legacy restart sentinel filesystem import");
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
}
|
|
visit(sourceFile);
|
|
return violations;
|
|
}
|
|
|
|
function collectLegacyExecApprovalsBoundaryViolations(sourceFile, relativePath) {
|
|
if (
|
|
relativePath === legacyExecApprovalsMigrationPath ||
|
|
relativePath === legacyExecApprovalsConfigPath ||
|
|
relativePath === stableExecApprovalsPolicyUriPath
|
|
) {
|
|
return [];
|
|
}
|
|
|
|
const violations = [];
|
|
function visit(node) {
|
|
if (ts.isStringLiteralLike(node) && legacyExecApprovalsFilenamePattern.test(node.text)) {
|
|
violations.push({
|
|
kind: "legacy exec approvals reference",
|
|
line: toLine(sourceFile, node),
|
|
});
|
|
}
|
|
if (
|
|
relativePath === legacyExecApprovalsRuntimePath &&
|
|
ts.isImportDeclaration(node) &&
|
|
legacyRestartSentinelRuntimeImportSpecifiers.has(importSource(node))
|
|
) {
|
|
violations.push({
|
|
kind: "legacy exec approvals filesystem import",
|
|
line: toLine(sourceFile, node),
|
|
});
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
}
|
|
visit(sourceFile);
|
|
return violations;
|
|
}
|
|
|
|
function isHelperWriteModuleSource(source) {
|
|
return (
|
|
source === "openclaw/plugin-sdk/file-access-runtime" ||
|
|
source === "openclaw/plugin-sdk/security-runtime" ||
|
|
fsSafePackageModulePattern.test(source) ||
|
|
helperWriteModulePattern.test(source)
|
|
);
|
|
}
|
|
|
|
function collectCreateRequireBindings(sourceFile) {
|
|
const bindings = new Set();
|
|
function visit(node) {
|
|
if (ts.isImportDeclaration(node) && ["node:module", "module"].includes(importSource(node))) {
|
|
const namedBindings = node.importClause?.namedBindings;
|
|
if (namedBindings && ts.isNamedImports(namedBindings)) {
|
|
for (const element of namedBindings.elements) {
|
|
const importedName = element.propertyName?.text ?? element.name.text;
|
|
if (importedName === "createRequire") {
|
|
bindings.add(element.name.text);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
}
|
|
visit(sourceFile);
|
|
return bindings;
|
|
}
|
|
|
|
function isFsRequireExpression(expression, isRequireName = (name) => name === "require") {
|
|
const call = unwrapExpression(expression);
|
|
if (!ts.isCallExpression(call) || !ts.isIdentifier(unwrapExpression(call.expression))) {
|
|
return false;
|
|
}
|
|
const requireName = unwrapExpression(call.expression).text;
|
|
const [specifier] = call.arguments;
|
|
return (
|
|
isRequireName(requireName) &&
|
|
specifier &&
|
|
ts.isStringLiteralLike(specifier) &&
|
|
fsModuleSpecifiers.has(specifier.text)
|
|
);
|
|
}
|
|
|
|
function unwrapAwaitExpression(expression) {
|
|
const unwrapped = unwrapExpression(expression);
|
|
return ts.isAwaitExpression(unwrapped) ? unwrapExpression(unwrapped.expression) : unwrapped;
|
|
}
|
|
|
|
function isFsDynamicImportExpression(expression) {
|
|
const call = unwrapAwaitExpression(expression);
|
|
if (!ts.isCallExpression(call) || call.expression.kind !== ts.SyntaxKind.ImportKeyword) {
|
|
return false;
|
|
}
|
|
const [specifier] = call.arguments;
|
|
return (
|
|
specifier !== undefined &&
|
|
ts.isStringLiteralLike(specifier) &&
|
|
fsModuleSpecifiers.has(specifier.text)
|
|
);
|
|
}
|
|
|
|
function collectFsBindings(sourceFile) {
|
|
const fsModuleBindings = new Set();
|
|
const fsWriteAliases = new Map();
|
|
const fsSafeStoreFactoryAliases = new Map();
|
|
|
|
for (const statement of sourceFile.statements) {
|
|
if (!ts.isImportDeclaration(statement)) {
|
|
continue;
|
|
}
|
|
const source = importSource(statement);
|
|
const clause = statement.importClause;
|
|
if (!clause) {
|
|
continue;
|
|
}
|
|
if (clause.name && fsModuleSpecifiers.has(source)) {
|
|
fsModuleBindings.add(clause.name.text);
|
|
}
|
|
const namedBindings = clause.namedBindings;
|
|
if (!namedBindings) {
|
|
continue;
|
|
}
|
|
if (ts.isNamespaceImport(namedBindings)) {
|
|
if (fsModuleSpecifiers.has(source)) {
|
|
fsModuleBindings.add(namedBindings.name.text);
|
|
}
|
|
if (isHelperWriteModuleSource(source)) {
|
|
for (const helperName of helperWriteCallees) {
|
|
fsWriteAliases.set(`${namedBindings.name.text}.${helperName}`, helperName);
|
|
}
|
|
for (const factoryName of [
|
|
...fsSafeStoreFactoryCallees,
|
|
...fsSafeJsonStoreFactoryCallees,
|
|
]) {
|
|
fsSafeStoreFactoryAliases.set(`${namedBindings.name.text}.${factoryName}`, factoryName);
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
for (const element of namedBindings.elements) {
|
|
const importedName = element.propertyName?.text ?? element.name.text;
|
|
if (fsModuleSpecifiers.has(source) && importedName === "promises") {
|
|
fsModuleBindings.add(element.name.text);
|
|
}
|
|
if (fsModuleSpecifiers.has(source) && legacyWriteCallees.has(importedName)) {
|
|
fsWriteAliases.set(element.name.text, importedName);
|
|
}
|
|
if (isHelperWriteModuleSource(source) && helperWriteCallees.has(importedName)) {
|
|
fsWriteAliases.set(element.name.text, importedName);
|
|
}
|
|
if (
|
|
isHelperWriteModuleSource(source) &&
|
|
(fsSafeStoreFactoryCallees.has(importedName) ||
|
|
fsSafeJsonStoreFactoryCallees.has(importedName))
|
|
) {
|
|
fsSafeStoreFactoryAliases.set(element.name.text, importedName);
|
|
}
|
|
}
|
|
}
|
|
|
|
return { fsModuleBindings, fsWriteAliases, fsSafeStoreFactoryAliases };
|
|
}
|
|
|
|
function templateCandidateText(current) {
|
|
let text = current.head.text;
|
|
for (const span of current.templateSpans) {
|
|
text += `*${span.literal.text}`;
|
|
}
|
|
return text || "*";
|
|
}
|
|
|
|
function legacyCandidateTexts(sourceFile, node) {
|
|
const candidates = node.pos >= 0 && node.end >= 0 ? [node.getText(sourceFile)] : [];
|
|
const stringSegments = [];
|
|
|
|
function binaryExpressionCandidateText(current) {
|
|
if (current.operatorToken.kind !== ts.SyntaxKind.PlusToken) {
|
|
return null;
|
|
}
|
|
const left = pathSegmentCandidateText(current.left);
|
|
const right = pathSegmentCandidateText(current.right);
|
|
if (!left && !right) {
|
|
return null;
|
|
}
|
|
return `${left ?? "*"}${right ?? "*"}`;
|
|
}
|
|
|
|
function pathSegmentCandidateText(current) {
|
|
const unwrapped = unwrapExpression(current);
|
|
if (ts.isStringLiteralLike(unwrapped)) {
|
|
return unwrapped.text;
|
|
}
|
|
if (ts.isTemplateExpression(unwrapped)) {
|
|
return templateCandidateText(unwrapped);
|
|
}
|
|
if (ts.isBinaryExpression(unwrapped)) {
|
|
return binaryExpressionCandidateText(unwrapped);
|
|
}
|
|
return "*";
|
|
}
|
|
|
|
if (candidates.length === 0) {
|
|
const syntheticPathSegment = pathSegmentCandidateText(node);
|
|
if (syntheticPathSegment !== "*") {
|
|
candidates.push(syntheticPathSegment);
|
|
}
|
|
}
|
|
|
|
function maybeAddCallPathCandidate(current) {
|
|
if (!ts.isCallExpression(current) || current.arguments.length < 2) {
|
|
return;
|
|
}
|
|
const segments = current.arguments.map((argument) => pathSegmentCandidateText(argument));
|
|
if (!segments.some((segment) => segment !== "*")) {
|
|
return;
|
|
}
|
|
candidates.push(segments.join("/"));
|
|
}
|
|
|
|
function visit(current) {
|
|
maybeAddCallPathCandidate(current);
|
|
if (ts.isStringLiteralLike(current)) {
|
|
stringSegments.push(current.text);
|
|
}
|
|
ts.forEachChild(current, visit);
|
|
}
|
|
visit(node);
|
|
if (stringSegments.length > 1) {
|
|
candidates.push(stringSegments.join("/"));
|
|
}
|
|
return candidates;
|
|
}
|
|
|
|
/**
|
|
* Finds database-first legacy-store violations in one TypeScript/JavaScript source file.
|
|
*/
|
|
export function collectDatabaseFirstLegacyStoreViolations(
|
|
content,
|
|
inputRelativePath = "source.ts",
|
|
scanOptions = {},
|
|
) {
|
|
const relativePath = inputRelativePath.replaceAll("\\", "/");
|
|
const sourceFile = ts.createSourceFile(relativePath, content, ts.ScriptTarget.Latest, true);
|
|
const boundaryViolations = [
|
|
...collectLegacyRestartSentinelBoundaryViolations(sourceFile, relativePath),
|
|
...collectLegacyExecApprovalsBoundaryViolations(sourceFile, relativePath),
|
|
];
|
|
if (isAllowedLegacyOwnerPath(relativePath)) {
|
|
return boundaryViolations;
|
|
}
|
|
|
|
const currentLegacyWriteAllowances =
|
|
scanOptions.currentLegacyWriteAllowances ?? currentLegacyWriteViolationAllowances(relativePath);
|
|
const createRequireBindings = collectCreateRequireBindings(sourceFile);
|
|
const { fsModuleBindings, fsWriteAliases, fsSafeStoreFactoryAliases } =
|
|
collectFsBindings(sourceFile);
|
|
const violations = [...boundaryViolations];
|
|
const seenViolations = new Set(
|
|
boundaryViolations.map((violation) => `${violation.line}:${violation.kind}`),
|
|
);
|
|
const fsModuleBindingScopes = [new Map([...fsModuleBindings].map((name) => [name, true]))];
|
|
const fsModulePropertyScopes = [new Map()];
|
|
const fsWriteAliasScopes = [fsWriteAliases];
|
|
const fsSafeStoreFactoryAliasScopes = [fsSafeStoreFactoryAliases];
|
|
const fsSafeStoreScopes = [new Map()];
|
|
const fsSafeJsonStoreScopes = [new Map()];
|
|
const requireAliasScopes = [new Map([["require", true]])];
|
|
const createRequireShadowScopes = [new Set()];
|
|
const legacyPathScopes = [new Map()];
|
|
const literalTextScopes = [new Map()];
|
|
const staticExpressionScopes = [new Map()];
|
|
const knownUndefinedScopes = [new Map()];
|
|
const legacyKnownObjectLiteralScopes = [new Map()];
|
|
const legacyObjectPropertyScopes = [new Map()];
|
|
const wrapperFunctionScopes = [new Map()];
|
|
const conditionalExecutionScopes = [false];
|
|
const branchEffectScopes = [];
|
|
const activeWrapperNodes = new Set();
|
|
const intrinsicWrapperCalls = new WeakSet();
|
|
const wrapperCallSites = [];
|
|
const wrapperExecutionScopeIndexes = [];
|
|
let definitionScanDepth = 0;
|
|
const lexicalScopeStacks = [
|
|
[fsWriteAliasScopes, Map],
|
|
[fsSafeStoreFactoryAliasScopes, Map],
|
|
[fsSafeStoreScopes, Map],
|
|
[fsSafeJsonStoreScopes, Map],
|
|
[fsModuleBindingScopes, Map],
|
|
[fsModulePropertyScopes, Map],
|
|
[requireAliasScopes, Map],
|
|
[createRequireShadowScopes, Set],
|
|
[legacyPathScopes, Map],
|
|
[literalTextScopes, Map],
|
|
[staticExpressionScopes, Map],
|
|
[knownUndefinedScopes, Map],
|
|
[legacyKnownObjectLiteralScopes, Map],
|
|
[legacyObjectPropertyScopes, Map],
|
|
[wrapperFunctionScopes, Map],
|
|
];
|
|
|
|
function withLexicalScope(isConditional, visitScope) {
|
|
for (const [scopes, Scope] of lexicalScopeStacks) {
|
|
scopes.push(new Scope());
|
|
}
|
|
conditionalExecutionScopes.push(isConditional);
|
|
try {
|
|
return visitScope();
|
|
} finally {
|
|
conditionalExecutionScopes.pop();
|
|
for (let index = lexicalScopeStacks.length - 1; index >= 0; index--) {
|
|
lexicalScopeStacks[index][0].pop();
|
|
}
|
|
}
|
|
}
|
|
|
|
function addViolation(node, kind, fingerprintNode = node) {
|
|
const callSite = kind === "legacy store filesystem write" ? wrapperCallSites[0] : null;
|
|
const reportedNode = callSite ?? node;
|
|
const line = toLine(sourceFile, reportedNode);
|
|
if (
|
|
consumeAllowedCurrentLegacyViolation(
|
|
currentLegacyWriteAllowances,
|
|
relativePath,
|
|
sourceFile,
|
|
callSite ?? fingerprintNode,
|
|
kind,
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
const key = `${line}:${kind}`;
|
|
if (seenViolations.has(key)) {
|
|
return;
|
|
}
|
|
seenViolations.add(key);
|
|
violations.push({ kind, line });
|
|
}
|
|
|
|
function resolveRequireAlias(name) {
|
|
return scopeForRead(requireAliasScopes, name)?.get(name) === true;
|
|
}
|
|
|
|
function isNodeRequireName(name) {
|
|
return resolveRequireAlias(name);
|
|
}
|
|
|
|
function isCreateRequireShadowed(name) {
|
|
return createRequireShadowScopes.some((scope) => scope.has(name));
|
|
}
|
|
|
|
function isCreateRequireExpression(expression) {
|
|
const call = unwrapExpression(expression);
|
|
return (
|
|
ts.isCallExpression(call) &&
|
|
ts.isIdentifier(unwrapExpression(call.expression)) &&
|
|
createRequireBindings.has(unwrapExpression(call.expression).text) &&
|
|
!isCreateRequireShadowed(unwrapExpression(call.expression).text)
|
|
);
|
|
}
|
|
|
|
function isRequireAliasExpression(expression) {
|
|
const value = unwrapExpression(expression);
|
|
return (
|
|
isCreateRequireExpression(value) ||
|
|
(ts.isIdentifier(value) && resolveRequireAlias(value.text))
|
|
);
|
|
}
|
|
|
|
function resolveFsModuleBinding(name) {
|
|
return scopeForRead(fsModuleBindingScopes, name)?.get(name) === true;
|
|
}
|
|
|
|
function resolveFsModuleProperty(pathParts) {
|
|
const fullPath = pathParts.join(".");
|
|
const prefixes = pathParts.map((_, index) => pathParts.slice(0, index + 1).join("."));
|
|
for (let index = fsModulePropertyScopes.length - 1; index >= 0; index--) {
|
|
const scope = fsModulePropertyScopes[index];
|
|
if (scope.has(fullPath)) {
|
|
return scope.get(fullPath) === true;
|
|
}
|
|
for (const prefix of prefixes) {
|
|
if (scope.get(prefix) === false) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function resolveFsWriteAlias(name) {
|
|
return scopeForRead(fsWriteAliasScopes, name)?.get(name) ?? null;
|
|
}
|
|
|
|
function resolveFsSafeStoreFactoryAlias(name) {
|
|
return scopeForRead(fsSafeStoreFactoryAliasScopes, name)?.get(name) ?? null;
|
|
}
|
|
|
|
function resolveFsSafeStore(name) {
|
|
const value = lookupFsSafeStore(name);
|
|
return value === true;
|
|
}
|
|
|
|
function lookupFsSafeStore(name) {
|
|
const scope = scopeForRead(fsSafeStoreScopes, name);
|
|
return scope ? scope.get(name) === true : null;
|
|
}
|
|
|
|
function resolveFsSafeJsonStore(name) {
|
|
const value = lookupFsSafeJsonStore(name);
|
|
return value === true;
|
|
}
|
|
|
|
function lookupFsSafeJsonStore(name) {
|
|
const scope = scopeForRead(fsSafeJsonStoreScopes, name);
|
|
return scope ? scope.get(name) === true : null;
|
|
}
|
|
|
|
function lookupKnownLegacyObjectLiteral(name) {
|
|
for (let index = legacyKnownObjectLiteralScopes.length - 1; index >= 0; index--) {
|
|
const scope = legacyKnownObjectLiteralScopes[index];
|
|
if (scope.has(name)) {
|
|
return scope.get(name) === true;
|
|
}
|
|
if (legacyPathScopes[index].has(name)) {
|
|
return false;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function isKnownLegacyObjectLiteralExpression(expression) {
|
|
const unwrapped = unwrapExpression(expression);
|
|
return (
|
|
ts.isObjectLiteralExpression(unwrapped) ||
|
|
(ts.isIdentifier(unwrapped) && lookupKnownLegacyObjectLiteral(unwrapped.text))
|
|
);
|
|
}
|
|
|
|
function markKnownLegacyObjectLiteral(
|
|
name,
|
|
initializer,
|
|
targetScope = lastScope(legacyKnownObjectLiteralScopes),
|
|
) {
|
|
targetScope.set(name, isKnownLegacyObjectLiteralExpression(initializer));
|
|
}
|
|
|
|
function createBranchEffects() {
|
|
return {
|
|
fsIdentifierAssignments: new Map(),
|
|
fsSafePropertyAssignments: new Map(),
|
|
identifierAssignments: new Map(),
|
|
propertyAssignments: new Map(),
|
|
wrapperAssignments: new Map(),
|
|
};
|
|
}
|
|
|
|
function objectPropertyKey(objectName, propertyName) {
|
|
return `${objectName}.${propertyName}`;
|
|
}
|
|
|
|
function shadowObjectPropertyScopes(scopes, objectName, value, targetScope = lastScope(scopes)) {
|
|
const prefix = `${objectName}.`;
|
|
for (const scope of scopes) {
|
|
for (const name of scope.keys()) {
|
|
if (name.startsWith(prefix)) {
|
|
targetScope.set(name, value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function visitObjectLiteralProperties(objectName, initializer, onProperty, onSpread = null) {
|
|
const objectLiteral = unwrapExpression(initializer);
|
|
if (!ts.isObjectLiteralExpression(objectLiteral)) {
|
|
return;
|
|
}
|
|
for (const property of objectLiteral.properties) {
|
|
if (ts.isSpreadAssignment(property)) {
|
|
onSpread?.(unwrapExpression(property.expression));
|
|
continue;
|
|
}
|
|
const isAssignment = ts.isPropertyAssignment(property);
|
|
const name = isAssignment
|
|
? propertyNameText(property.name)
|
|
: ts.isShorthandPropertyAssignment(property)
|
|
? property.name.text
|
|
: null;
|
|
if (name) {
|
|
onProperty(`${objectName}.${name}`, isAssignment ? property.initializer : property.name);
|
|
}
|
|
}
|
|
}
|
|
|
|
function resolveLegacyPathIdentifier(name) {
|
|
return scopeForRead(legacyPathScopes, name)?.get(name) === true;
|
|
}
|
|
|
|
function resolveLiteralTextIdentifier(name) {
|
|
return scopeForRead(literalTextScopes, name)?.get(name) ?? [];
|
|
}
|
|
|
|
function resolveStaticExpression(name) {
|
|
return scopeForRead(staticExpressionScopes, name)?.get(name) ?? null;
|
|
}
|
|
|
|
function resolveKnownUndefinedIdentifier(name) {
|
|
return scopeForRead(knownUndefinedScopes, name)?.get(name) === true;
|
|
}
|
|
|
|
function requireAliasWriteTarget(name) {
|
|
for (let index = requireAliasScopes.length - 1; index >= 0; index--) {
|
|
const scope = requireAliasScopes[index];
|
|
if (scope.has(name)) {
|
|
return { index, scope };
|
|
}
|
|
}
|
|
return { index: requireAliasScopes.length - 1, scope: lastScope(requireAliasScopes) };
|
|
}
|
|
|
|
function expressionLiteralCandidateTexts(node) {
|
|
const candidates = legacyCandidateTexts(sourceFile, node);
|
|
const segmentOptions = [];
|
|
|
|
function combineSegmentOptions(left, right) {
|
|
const joined = left.flatMap((leftOption) =>
|
|
right.map((rightOption) => `${leftOption}${rightOption}`),
|
|
);
|
|
return joined.length > 32 ? joined.slice(0, 32) : joined;
|
|
}
|
|
|
|
function expressionSegmentOptions(current) {
|
|
const unwrapped = unwrapExpression(current);
|
|
if (ts.isStringLiteralLike(unwrapped)) {
|
|
return [unwrapped.text];
|
|
}
|
|
if (ts.isTemplateExpression(unwrapped)) {
|
|
let joined = [unwrapped.head.text];
|
|
for (const span of unwrapped.templateSpans) {
|
|
joined = combineSegmentOptions(joined, expressionSegmentOptions(span.expression));
|
|
joined = combineSegmentOptions(joined, [span.literal.text]);
|
|
}
|
|
return joined.length > 0 ? joined : ["*"];
|
|
}
|
|
if (
|
|
ts.isBinaryExpression(unwrapped) &&
|
|
unwrapped.operatorToken.kind === ts.SyntaxKind.PlusToken
|
|
) {
|
|
return combineSegmentOptions(
|
|
expressionSegmentOptions(unwrapped.left),
|
|
expressionSegmentOptions(unwrapped.right),
|
|
);
|
|
}
|
|
if (ts.isIdentifier(unwrapped)) {
|
|
const texts = resolveLiteralTextIdentifier(unwrapped.text);
|
|
return texts.length > 0 ? texts : ["*"];
|
|
}
|
|
return ["*"];
|
|
}
|
|
|
|
function maybeAddCallLiteralCandidate(current) {
|
|
if (!ts.isCallExpression(current) || current.arguments.length < 2) {
|
|
return;
|
|
}
|
|
const argumentOptions = current.arguments.map((argument) =>
|
|
expressionSegmentOptions(argument),
|
|
);
|
|
if (!argumentOptions.some((options) => options.some((option) => option !== "*"))) {
|
|
return;
|
|
}
|
|
let joined = [""];
|
|
for (const options of argumentOptions) {
|
|
joined = joined.flatMap((prefix) =>
|
|
options.map((option) => (prefix.length === 0 ? option : `${prefix}/${option}`)),
|
|
);
|
|
if (joined.length > 32) {
|
|
joined = joined.slice(0, 32);
|
|
}
|
|
}
|
|
candidates.push(...joined);
|
|
}
|
|
|
|
function visitCandidate(current) {
|
|
maybeAddCallLiteralCandidate(current);
|
|
if (ts.isStringLiteralLike(current)) {
|
|
segmentOptions.push([current.text]);
|
|
return;
|
|
}
|
|
if (ts.isIdentifier(current)) {
|
|
const texts = resolveLiteralTextIdentifier(current.text);
|
|
if (texts.length > 0) {
|
|
segmentOptions.push(texts);
|
|
}
|
|
}
|
|
ts.forEachChild(current, visitCandidate);
|
|
}
|
|
const expressionOptions = expressionSegmentOptions(node);
|
|
if (expressionOptions.some((option) => option !== "*")) {
|
|
candidates.push(...expressionOptions);
|
|
}
|
|
visitCandidate(node);
|
|
if (segmentOptions.length > 1) {
|
|
let joined = [""];
|
|
for (const options of segmentOptions) {
|
|
joined = joined.flatMap((prefix) =>
|
|
options.map((option) => (prefix.length === 0 ? option : `${prefix}/${option}`)),
|
|
);
|
|
if (joined.length > 32) {
|
|
joined = joined.slice(0, 32);
|
|
}
|
|
}
|
|
candidates.push(...joined);
|
|
}
|
|
return candidates;
|
|
}
|
|
|
|
function expressionTextContainsLegacyStore(node) {
|
|
return expressionLiteralCandidateTexts(node).some((text) =>
|
|
legacyStorePatterns.some((pattern) => pattern.test(text)),
|
|
);
|
|
}
|
|
|
|
function literalTextsFromExpression(expression) {
|
|
const unwrapped = unwrapExpression(expression);
|
|
if (ts.isStringLiteralLike(unwrapped)) {
|
|
return [unwrapped.text];
|
|
}
|
|
if (ts.isIdentifier(unwrapped)) {
|
|
return resolveLiteralTextIdentifier(unwrapped.text);
|
|
}
|
|
return [];
|
|
}
|
|
|
|
function arrayLiteralElementAt(expression, index) {
|
|
const unwrapped = unwrapExpression(expression);
|
|
if (!ts.isArrayLiteralExpression(unwrapped)) {
|
|
return null;
|
|
}
|
|
const element = unwrapped.elements[index];
|
|
return element && !ts.isSpreadElement(element) ? element : null;
|
|
}
|
|
|
|
function legacyObjectPropertyRewriteValues(objectName, initializer, existingScope) {
|
|
const values = new Map();
|
|
markLegacyObjectProperties(objectName, initializer, values, null);
|
|
if (isKnownLegacyObjectLiteralExpression(initializer)) {
|
|
const descendantPrefix = `${objectName}.`;
|
|
for (const key of existingScope.keys()) {
|
|
if (key.startsWith(descendantPrefix) && !values.has(key)) {
|
|
values.set(key, explicitUndefinedLegacyObjectPropertyValue);
|
|
}
|
|
}
|
|
}
|
|
return values;
|
|
}
|
|
|
|
function lookupLegacyObjectProperty(
|
|
objectName,
|
|
propertyName,
|
|
maxScopeIndex = legacyObjectPropertyScopes.length - 1,
|
|
) {
|
|
const result = lookupLegacyObjectPropertyEntry(objectName, propertyName, maxScopeIndex);
|
|
if (result.found) {
|
|
return result.value === true;
|
|
}
|
|
if (result.objectKnown) {
|
|
return result.objectValue ? null : false;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function lookupLegacyObjectPropertyEntry(
|
|
objectName,
|
|
propertyName,
|
|
maxScopeIndex = legacyObjectPropertyScopes.length - 1,
|
|
) {
|
|
const key = objectPropertyKey(objectName, propertyName);
|
|
for (
|
|
let index = Math.min(maxScopeIndex, legacyObjectPropertyScopes.length - 1);
|
|
index >= 0;
|
|
index--
|
|
) {
|
|
const propertyScope = legacyObjectPropertyScopes[index];
|
|
if (propertyScope.has(key)) {
|
|
return { found: true, value: propertyScope.get(key) };
|
|
}
|
|
if (legacyPathScopes[index].has(objectName)) {
|
|
return {
|
|
found: false,
|
|
objectKnown: true,
|
|
objectValue: legacyPathScopes[index].get(objectName) === true,
|
|
};
|
|
}
|
|
}
|
|
return { found: false, objectKnown: false, objectValue: false };
|
|
}
|
|
|
|
function lookupScopedLegacyObjectPropertyEntry(
|
|
objectName,
|
|
propertyPath,
|
|
propertyScope,
|
|
knownObjectLiteralScope,
|
|
) {
|
|
const propertyName = propertyPath.join(".");
|
|
const key = objectPropertyKey(objectName, propertyName);
|
|
if (propertyScope.has(key)) {
|
|
return { found: true, value: propertyScope.get(key) };
|
|
}
|
|
const parentPath = propertyPath.slice(0, -1).join(".");
|
|
const parentKey = parentPath ? objectPropertyKey(objectName, parentPath) : objectName;
|
|
if (knownObjectLiteralScope.get(parentKey) === true) {
|
|
return { found: false, objectKnown: true, objectValue: false };
|
|
}
|
|
return { found: false, objectKnown: false, objectValue: false };
|
|
}
|
|
|
|
function legacyObjectPropertyValueFromExpression(expression) {
|
|
return isKnownUndefinedExpression(expression)
|
|
? explicitUndefinedLegacyObjectPropertyValue
|
|
: expressionContainsLegacyStore(expression);
|
|
}
|
|
|
|
function elementAccessNames(expression) {
|
|
const argument = unwrapExpression(expression);
|
|
if (ts.isStringLiteral(argument) || ts.isNumericLiteral(argument)) {
|
|
return [argument.text];
|
|
}
|
|
return ts.isIdentifier(argument) ? resolveLiteralTextIdentifier(argument.text) : [];
|
|
}
|
|
|
|
function elementAccessName(expression) {
|
|
const names = elementAccessNames(expression);
|
|
return names.length === 1 ? names[0] : null;
|
|
}
|
|
|
|
function propertyAccessPath(expression) {
|
|
const unwrapped = unwrapExpression(expression);
|
|
if (ts.isIdentifier(unwrapped)) {
|
|
return [unwrapped.text];
|
|
}
|
|
if (ts.isPropertyAccessExpression(unwrapped)) {
|
|
const parentPath = propertyAccessPath(unwrapped.expression);
|
|
return parentPath ? [...parentPath, unwrapped.name.text] : null;
|
|
}
|
|
if (ts.isElementAccessExpression(unwrapped)) {
|
|
const propertyName = elementAccessName(unwrapped.argumentExpression);
|
|
if (!propertyName) {
|
|
return null;
|
|
}
|
|
const parentPath = propertyAccessPath(unwrapped.expression);
|
|
return parentPath ? [...parentPath, propertyName] : null;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function legacyObjectPropertyWriteTarget(objectName, propertyName) {
|
|
const key = objectPropertyKey(objectName, propertyName);
|
|
for (let index = legacyObjectPropertyScopes.length - 1; index >= 0; index--) {
|
|
const propertyScope = legacyObjectPropertyScopes[index];
|
|
if (propertyScope.has(key) || legacyPathScopes[index].has(objectName)) {
|
|
return { index, scope: propertyScope };
|
|
}
|
|
}
|
|
return {
|
|
index: legacyObjectPropertyScopes.length - 1,
|
|
scope: lastScope(legacyObjectPropertyScopes),
|
|
};
|
|
}
|
|
|
|
function legacyIdentifierWriteScopes(name) {
|
|
for (let index = legacyPathScopes.length - 1; index >= 0; index--) {
|
|
if (legacyPathScopes[index].has(name)) {
|
|
return {
|
|
index,
|
|
pathScope: legacyPathScopes[index],
|
|
propertyScope: legacyObjectPropertyScopes[index],
|
|
wrapperScope: wrapperFunctionScopes[index],
|
|
};
|
|
}
|
|
}
|
|
return {
|
|
index: legacyPathScopes.length - 1,
|
|
pathScope: lastScope(legacyPathScopes),
|
|
propertyScope: lastScope(legacyObjectPropertyScopes),
|
|
wrapperScope: lastScope(wrapperFunctionScopes),
|
|
};
|
|
}
|
|
|
|
function isConditionallyExecutedScope(node) {
|
|
const parent = node.parent;
|
|
return Boolean(
|
|
(ts.isBlock(node) &&
|
|
parent &&
|
|
((ts.isIfStatement(parent) &&
|
|
(parent.thenStatement === node || parent.elseStatement === node)) ||
|
|
(ts.isIterationStatement(parent, false) && parent.statement === node) ||
|
|
(ts.isTryStatement(parent) && parent.tryBlock === node))) ||
|
|
ts.isCaseBlock(node) ||
|
|
ts.isCatchClause(node),
|
|
);
|
|
}
|
|
|
|
function expressionContainsLegacyStore(node) {
|
|
if (expressionTextContainsLegacyStore(node)) {
|
|
return true;
|
|
}
|
|
let found = false;
|
|
function visitExpression(current) {
|
|
if (found) {
|
|
return;
|
|
}
|
|
if (
|
|
ts.isIdentifier(current) &&
|
|
!(ts.isPropertyAccessExpression(current.parent) && current.parent.name === current) &&
|
|
resolveLegacyPathIdentifier(current.text)
|
|
) {
|
|
found = true;
|
|
return;
|
|
}
|
|
const propertyAccess = rootedPropertyAccessCandidates(current);
|
|
if (propertyAccess?.propertyPaths.some((propertyPath) => propertyPath.length > 0)) {
|
|
const propertyValue = lookupLegacyObjectPropertyCandidates(propertyAccess);
|
|
if (propertyValue !== null) {
|
|
found = propertyValue;
|
|
return;
|
|
}
|
|
}
|
|
ts.forEachChild(current, visitExpression);
|
|
}
|
|
visitExpression(node);
|
|
return found;
|
|
}
|
|
|
|
function visitWithChildScope(node) {
|
|
withLexicalScope(
|
|
lastScope(conditionalExecutionScopes) || isConditionallyExecutedScope(node),
|
|
() => {
|
|
if ("statements" in node) {
|
|
registerHoistedWrapperFunctions(node.statements);
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
},
|
|
);
|
|
}
|
|
|
|
function registerFsBindingParameter(name) {
|
|
if (ts.isIdentifier(name)) {
|
|
lastScope(fsModuleBindingScopes).set(name.text, true);
|
|
return;
|
|
}
|
|
if (!ts.isObjectBindingPattern(name)) {
|
|
return;
|
|
}
|
|
for (const element of name.elements) {
|
|
const importedName = element.propertyName
|
|
? propertyNameText(element.propertyName)
|
|
: ts.isIdentifier(element.name)
|
|
? element.name.text
|
|
: null;
|
|
if (importedName === "promises") {
|
|
if (ts.isIdentifier(element.name)) {
|
|
lastScope(fsModuleBindingScopes).set(element.name.text, true);
|
|
} else if (ts.isObjectBindingPattern(element.name)) {
|
|
registerFsPromisesBindingParameter(element.name);
|
|
}
|
|
}
|
|
if (importedName && legacyWriteCallees.has(importedName) && ts.isIdentifier(element.name)) {
|
|
lastScope(fsWriteAliasScopes).set(element.name.text, importedName);
|
|
}
|
|
}
|
|
}
|
|
|
|
function registerFsPromisesBindingParameter(name) {
|
|
if (!ts.isObjectBindingPattern(name)) {
|
|
return;
|
|
}
|
|
for (const element of name.elements) {
|
|
const importedName = element.propertyName
|
|
? propertyNameText(element.propertyName)
|
|
: ts.isIdentifier(element.name)
|
|
? element.name.text
|
|
: null;
|
|
if (importedName && legacyWriteCallees.has(importedName) && ts.isIdentifier(element.name)) {
|
|
lastScope(fsWriteAliasScopes).set(element.name.text, importedName);
|
|
}
|
|
if (ts.isObjectBindingPattern(element.name)) {
|
|
registerFsPromisesBindingParameter(element.name);
|
|
}
|
|
}
|
|
}
|
|
|
|
function visitFunctionLike(node, fsBindingParameterIndexes = new Set()) {
|
|
withLexicalScope(false, () => {
|
|
if (node.name && ts.isIdentifier(node.name)) {
|
|
markFsWriteAliasShadows(node.name);
|
|
markFsSafeStoreShadows(node.name);
|
|
markFsModuleBindingShadows(node.name);
|
|
markFsModulePropertyShadows(node.name);
|
|
markCreateRequireShadows(node.name);
|
|
lastScope(wrapperFunctionScopes).set(node.name.text, wrapperRecordForNode(node));
|
|
}
|
|
node.parameters.forEach((parameter, index) => {
|
|
for (const name of bindingPatternNames(parameter.name)) {
|
|
lastScope(legacyPathScopes).set(name, false);
|
|
lastScope(legacyKnownObjectLiteralScopes).set(name, false);
|
|
lastScope(knownUndefinedScopes).set(name, false);
|
|
lastScope(literalTextScopes).set(name, null);
|
|
lastScope(wrapperFunctionScopes).set(name, null);
|
|
lastScope(requireAliasScopes).set(name, false);
|
|
}
|
|
markFsWriteAliasShadows(parameter.name);
|
|
markFsSafeStoreShadows(parameter.name);
|
|
markFsModuleBindingShadows(parameter.name);
|
|
markFsModulePropertyShadows(parameter.name);
|
|
markCreateRequireShadows(parameter.name);
|
|
registerFsModuleTypeProperties(parameter.name, parameter.type);
|
|
if (fsBindingParameterIndexes.has(index)) {
|
|
registerFsBindingParameter(parameter.name);
|
|
}
|
|
});
|
|
definitionScanDepth += 1;
|
|
try {
|
|
ts.forEachChild(node, visit);
|
|
} finally {
|
|
definitionScanDepth -= 1;
|
|
}
|
|
});
|
|
}
|
|
|
|
function dynamicFsImportThenCallback(node) {
|
|
const callee = unwrapExpression(node.expression);
|
|
if (
|
|
!ts.isPropertyAccessExpression(callee) ||
|
|
callee.name.text !== "then" ||
|
|
!isFsDynamicImportExpression(callee.expression)
|
|
) {
|
|
return null;
|
|
}
|
|
const [callback] = node.arguments;
|
|
return callback && ts.isFunctionLike(callback) ? callback : null;
|
|
}
|
|
|
|
function isFsModuleExpression(expression) {
|
|
const receiver = unwrapExpression(expression);
|
|
if (
|
|
isFsRequireExpression(receiver, isNodeRequireName) ||
|
|
isFsDynamicImportExpression(receiver)
|
|
) {
|
|
return true;
|
|
}
|
|
if (ts.isIdentifier(receiver)) {
|
|
return resolveFsModuleBinding(receiver.text);
|
|
}
|
|
const receiverPath = propertyAccessPath(receiver);
|
|
if (receiverPath && resolveFsModuleProperty(receiverPath)) {
|
|
return true;
|
|
}
|
|
return (
|
|
ts.isPropertyAccessExpression(receiver) &&
|
|
receiver.name.text === "promises" &&
|
|
(isFsRequireExpression(receiver.expression, isNodeRequireName) ||
|
|
isFsDynamicImportExpression(receiver.expression) ||
|
|
(ts.isIdentifier(receiver.expression) &&
|
|
resolveFsModuleBinding(receiver.expression.text)) ||
|
|
(propertyAccessPath(receiver.expression) &&
|
|
resolveFsModuleProperty(propertyAccessPath(receiver.expression))))
|
|
);
|
|
}
|
|
|
|
function legacyFsWriteName(expression, aliases = null) {
|
|
const callee = unwrapExpression(expression);
|
|
if (ts.isPropertyAccessExpression(callee)) {
|
|
const aliasedName = callExpressionName(callee);
|
|
const writeAlias = aliasedName ? resolveFsWriteAlias(aliasedName) : null;
|
|
if (writeAlias) {
|
|
return writeAlias;
|
|
}
|
|
return legacyWriteCallees.has(callee.name.text) && isFsModuleExpression(callee.expression)
|
|
? callee.name.text
|
|
: null;
|
|
}
|
|
if (ts.isElementAccessExpression(callee)) {
|
|
const aliasedName = callExpressionName(callee);
|
|
const writeAlias = aliasedName ? resolveFsWriteAlias(aliasedName) : null;
|
|
if (writeAlias) {
|
|
return writeAlias;
|
|
}
|
|
const writeName = elementAccessName(callee.argumentExpression);
|
|
return writeName &&
|
|
legacyWriteCallees.has(writeName) &&
|
|
isFsModuleExpression(callee.expression)
|
|
? writeName
|
|
: null;
|
|
}
|
|
if (!ts.isIdentifier(callee)) {
|
|
return null;
|
|
}
|
|
return aliases && aliases.has(callee.text)
|
|
? aliases.get(callee.text)
|
|
: resolveFsWriteAlias(callee.text);
|
|
}
|
|
|
|
function fsSafeStoreFactoryAliasName(expression) {
|
|
const callee = unwrapExpression(expression);
|
|
if (ts.isIdentifier(callee)) {
|
|
return resolveFsSafeStoreFactoryAlias(callee.text);
|
|
}
|
|
const name = callExpressionName(callee);
|
|
return name ? resolveFsSafeStoreFactoryAlias(name) : null;
|
|
}
|
|
|
|
function isFsSafeStoreFactoryCall(expression) {
|
|
const unwrapped = unwrapExpression(expression);
|
|
const call = ts.isAwaitExpression(unwrapped)
|
|
? unwrapExpression(unwrapped.expression)
|
|
: unwrapped;
|
|
if (!ts.isCallExpression(call)) {
|
|
return false;
|
|
}
|
|
const callee = unwrapExpression(call.expression);
|
|
if (ts.isPropertyAccessExpression(callee) || ts.isElementAccessExpression(callee)) {
|
|
const methodName = ts.isPropertyAccessExpression(callee)
|
|
? callee.name.text
|
|
: elementAccessName(callee.argumentExpression);
|
|
if (methodName === "root" && isFsSafeStoreExpression(callee.expression)) {
|
|
return true;
|
|
}
|
|
}
|
|
const name = callExpressionName(call.expression);
|
|
const factoryName = name ? resolveFsSafeStoreFactoryAlias(name) : null;
|
|
return Boolean(factoryName && fsSafeStoreFactoryCallees.has(factoryName));
|
|
}
|
|
|
|
function isFsSafeStoreExpression(expression) {
|
|
const unwrapped = unwrapExpression(expression);
|
|
if (isFsSafeStoreFactoryCall(unwrapped)) {
|
|
return true;
|
|
}
|
|
if (ts.isIdentifier(unwrapped)) {
|
|
return resolveFsSafeStore(unwrapped.text);
|
|
}
|
|
const receiverPath = propertyAccessPath(unwrapped);
|
|
if (receiverPath) {
|
|
return resolveFsSafeStore(receiverPath.join("."));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function objectFilePathContainsLegacyStore(expression) {
|
|
const unwrapped = unwrapExpression(expression);
|
|
if (ts.isIdentifier(unwrapped)) {
|
|
return lookupLegacyObjectProperty(unwrapped.text, "filePath") === true;
|
|
}
|
|
if (!ts.isObjectLiteralExpression(unwrapped)) {
|
|
return expressionContainsLegacyStore(unwrapped);
|
|
}
|
|
return objectLiteralPropertyContainsLegacyStore(unwrapped, "filePath");
|
|
}
|
|
|
|
function expressionContainsFsSafeJsonStoreLegacyPath(expression) {
|
|
const unwrapped = unwrapExpression(expression);
|
|
if (ts.isIdentifier(unwrapped)) {
|
|
return resolveFsSafeJsonStore(unwrapped.text);
|
|
}
|
|
const receiverPath = propertyAccessPath(unwrapped);
|
|
if (receiverPath && resolveFsSafeJsonStore(receiverPath.join("."))) {
|
|
return true;
|
|
}
|
|
if (!ts.isCallExpression(unwrapped)) {
|
|
return false;
|
|
}
|
|
const callName = callExpressionName(unwrapped.expression);
|
|
const factoryName = callName ? resolveFsSafeStoreFactoryAlias(callName) : null;
|
|
if (factoryName && fsSafeJsonStoreFactoryCallees.has(factoryName)) {
|
|
const options = unwrapped.arguments[0];
|
|
return options ? objectFilePathContainsLegacyStore(options) : false;
|
|
}
|
|
const callee = unwrapExpression(unwrapped.expression);
|
|
if (!ts.isPropertyAccessExpression(callee) && !ts.isElementAccessExpression(callee)) {
|
|
return false;
|
|
}
|
|
const methodName = ts.isPropertyAccessExpression(callee)
|
|
? callee.name.text
|
|
: elementAccessName(callee.argumentExpression);
|
|
if (methodName !== "json" || !isFsSafeStoreExpression(callee.expression)) {
|
|
return false;
|
|
}
|
|
const pathArgument = unwrapped.arguments[0];
|
|
return pathArgument ? pathArgumentContainsLegacyStore(pathArgument) : false;
|
|
}
|
|
|
|
function fsSafeJsonStoreWriteContainsLegacyStore(call) {
|
|
const callee = unwrapExpression(call.expression);
|
|
if (!ts.isPropertyAccessExpression(callee) && !ts.isElementAccessExpression(callee)) {
|
|
return false;
|
|
}
|
|
const methodName = ts.isPropertyAccessExpression(callee)
|
|
? callee.name.text
|
|
: elementAccessName(callee.argumentExpression);
|
|
if (!methodName || !fsSafeJsonStoreWriteMethods.has(methodName)) {
|
|
return false;
|
|
}
|
|
return expressionContainsFsSafeJsonStoreLegacyPath(callee.expression);
|
|
}
|
|
|
|
function fsSafeStoreWritePathArguments(call) {
|
|
const callee = unwrapExpression(call.expression);
|
|
if (!ts.isPropertyAccessExpression(callee) && !ts.isElementAccessExpression(callee)) {
|
|
return [];
|
|
}
|
|
const methodName = ts.isPropertyAccessExpression(callee)
|
|
? callee.name.text
|
|
: elementAccessName(callee.argumentExpression);
|
|
if (!methodName || !fsSafeStoreWriteMethods.has(methodName)) {
|
|
return [];
|
|
}
|
|
if (!isFsSafeStoreExpression(callee.expression)) {
|
|
return [];
|
|
}
|
|
if (methodName === "move") {
|
|
return [...call.arguments].slice(0, 2);
|
|
}
|
|
return call.arguments[0] ? [call.arguments[0]] : [];
|
|
}
|
|
|
|
function markFsWriteAliasShadows(name) {
|
|
for (const bindingName of bindingPatternNames(name)) {
|
|
if (resolveFsWriteAlias(bindingName)) {
|
|
lastScope(fsWriteAliasScopes).set(bindingName, null);
|
|
}
|
|
shadowVisibleFsWriteObjectAliases(bindingName);
|
|
}
|
|
}
|
|
|
|
function markFsSafeStoreShadows(name) {
|
|
for (const bindingName of bindingPatternNames(name)) {
|
|
if (resolveFsSafeStoreFactoryAlias(bindingName)) {
|
|
lastScope(fsSafeStoreFactoryAliasScopes).set(bindingName, null);
|
|
}
|
|
shadowObjectPropertyScopes(fsSafeStoreFactoryAliasScopes, bindingName, null);
|
|
if (resolveFsSafeStore(bindingName)) {
|
|
lastScope(fsSafeStoreScopes).set(bindingName, false);
|
|
}
|
|
if (resolveFsSafeJsonStore(bindingName)) {
|
|
lastScope(fsSafeJsonStoreScopes).set(bindingName, false);
|
|
}
|
|
shadowObjectPropertyScopes(fsSafeStoreScopes, bindingName, false);
|
|
shadowObjectPropertyScopes(fsSafeJsonStoreScopes, bindingName, false);
|
|
}
|
|
}
|
|
|
|
function markFsModuleBindingShadows(name) {
|
|
for (const bindingName of bindingPatternNames(name)) {
|
|
if (resolveFsModuleBinding(bindingName)) {
|
|
lastScope(fsModuleBindingScopes).set(bindingName, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
function markFsModulePropertyShadows(name) {
|
|
for (const bindingName of bindingPatternNames(name)) {
|
|
clearFsModuleObjectProperties(lastScope(fsModulePropertyScopes), bindingName);
|
|
}
|
|
}
|
|
|
|
function markCreateRequireShadows(name) {
|
|
for (const bindingName of bindingPatternNames(name)) {
|
|
if (createRequireBindings.has(bindingName)) {
|
|
createRequireShadowScopes[createRequireShadowScopes.length - 1].add(bindingName);
|
|
}
|
|
}
|
|
}
|
|
|
|
function isFsModuleTypeNode(type) {
|
|
return Boolean(
|
|
type &&
|
|
/\btypeof\s+import\s*\(\s*["'](?:node:fs|node:fs\/promises|fs|fs\/promises)["']\s*\)/u.test(
|
|
type.getText(sourceFile),
|
|
),
|
|
);
|
|
}
|
|
|
|
function fsModulePropertyPathsFromType(type) {
|
|
const paths = [];
|
|
if (!type || !ts.isTypeLiteralNode(type)) {
|
|
return paths;
|
|
}
|
|
for (const member of type.members) {
|
|
if (!ts.isPropertySignature(member) || !member.type) {
|
|
continue;
|
|
}
|
|
const propertyName = propertyNameText(member.name);
|
|
if (!propertyName) {
|
|
continue;
|
|
}
|
|
if (isFsModuleTypeNode(member.type)) {
|
|
paths.push([propertyName]);
|
|
}
|
|
for (const nestedPath of fsModulePropertyPathsFromType(member.type)) {
|
|
paths.push([propertyName, ...nestedPath]);
|
|
}
|
|
}
|
|
return paths;
|
|
}
|
|
|
|
function registerFsModuleTypeProperties(name, type) {
|
|
if (!ts.isIdentifier(name) || !type) {
|
|
return;
|
|
}
|
|
if (isFsModuleTypeNode(type)) {
|
|
lastScope(fsModuleBindingScopes).set(name.text, true);
|
|
}
|
|
for (const pathParts of fsModulePropertyPathsFromType(type)) {
|
|
lastScope(fsModulePropertyScopes).set([name.text, ...pathParts].join("."), true);
|
|
}
|
|
}
|
|
|
|
function collectFsWriteAliasesFromBinding(node) {
|
|
collectFsWriteAliasesFromBindingInto(node, lastScope(fsWriteAliasScopes));
|
|
}
|
|
|
|
function clearFsWriteObjectAliases(scope, objectName) {
|
|
shadowObjectPropertyScopes([scope], objectName, null, scope);
|
|
}
|
|
|
|
function shadowVisibleFsWriteObjectAliases(objectName) {
|
|
shadowObjectPropertyScopes(fsWriteAliasScopes, objectName, null);
|
|
}
|
|
|
|
function setFsWriteObjectAlias(scope, name, writeName, conditionalWrite) {
|
|
if (writeName) {
|
|
scope.set(name, writeName);
|
|
} else if (!conditionalWrite) {
|
|
scope.set(name, null);
|
|
}
|
|
}
|
|
|
|
function registerFsWriteObjectAliases(
|
|
objectName,
|
|
initializer,
|
|
scope = lastScope(fsWriteAliasScopes),
|
|
conditionalWrite = false,
|
|
) {
|
|
visitObjectLiteralProperties(objectName, initializer, (name, value) => {
|
|
setFsWriteObjectAlias(scope, name, legacyFsWriteName(value), conditionalWrite);
|
|
});
|
|
}
|
|
|
|
function clearFsSafeStoreObjectAliases(storeScope, jsonStoreScope, objectName) {
|
|
shadowObjectPropertyScopes([storeScope], objectName, false, storeScope);
|
|
shadowObjectPropertyScopes([jsonStoreScope], objectName, false, jsonStoreScope);
|
|
}
|
|
|
|
function shadowVisibleFsSafeStoreObjectAliases(objectName) {
|
|
shadowObjectPropertyScopes(fsSafeStoreScopes, objectName, false);
|
|
shadowObjectPropertyScopes(fsSafeJsonStoreScopes, objectName, false);
|
|
}
|
|
|
|
function setFsSafeStoreObjectAlias(
|
|
storeScope,
|
|
jsonStoreScope,
|
|
name,
|
|
isStore,
|
|
isJsonStore,
|
|
conditionalWrite,
|
|
) {
|
|
if (isStore) {
|
|
storeScope.set(name, true);
|
|
} else if (!conditionalWrite) {
|
|
storeScope.set(name, false);
|
|
}
|
|
if (isJsonStore) {
|
|
jsonStoreScope.set(name, true);
|
|
} else if (!conditionalWrite) {
|
|
jsonStoreScope.set(name, false);
|
|
}
|
|
}
|
|
|
|
function copyFsSafeStoreObjectAliases(
|
|
targetName,
|
|
sourceName,
|
|
storeScope = lastScope(fsSafeStoreScopes),
|
|
jsonStoreScope = lastScope(fsSafeJsonStoreScopes),
|
|
) {
|
|
const sourcePrefix = `${sourceName}.`;
|
|
for (let index = fsSafeStoreScopes.length - 1; index >= 0; index--) {
|
|
const sourceStoreScope = fsSafeStoreScopes[index];
|
|
const sourceJsonStoreScope = fsSafeJsonStoreScopes[index];
|
|
let copied = false;
|
|
for (const [key, value] of sourceStoreScope) {
|
|
if (key.startsWith(sourcePrefix)) {
|
|
storeScope.set(`${targetName}.${key.slice(sourcePrefix.length)}`, value);
|
|
copied = true;
|
|
}
|
|
}
|
|
for (const [key, value] of sourceJsonStoreScope) {
|
|
if (key.startsWith(sourcePrefix)) {
|
|
jsonStoreScope.set(`${targetName}.${key.slice(sourcePrefix.length)}`, value);
|
|
copied = true;
|
|
}
|
|
}
|
|
if (copied || sourceStoreScope.has(sourceName) || sourceJsonStoreScope.has(sourceName)) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
function registerFsSafeStoreObjectAliases(
|
|
objectName,
|
|
initializer,
|
|
storeScope = lastScope(fsSafeStoreScopes),
|
|
jsonStoreScope = lastScope(fsSafeJsonStoreScopes),
|
|
conditionalWrite = false,
|
|
) {
|
|
visitObjectLiteralProperties(
|
|
objectName,
|
|
initializer,
|
|
(name, value) => {
|
|
setFsSafeStoreObjectAlias(
|
|
storeScope,
|
|
jsonStoreScope,
|
|
name,
|
|
isFsSafeStoreExpression(value),
|
|
expressionContainsFsSafeJsonStoreLegacyPath(value),
|
|
conditionalWrite,
|
|
);
|
|
if (ts.isObjectLiteralExpression(unwrapExpression(value))) {
|
|
registerFsSafeStoreObjectAliases(
|
|
name,
|
|
value,
|
|
storeScope,
|
|
jsonStoreScope,
|
|
conditionalWrite,
|
|
);
|
|
}
|
|
},
|
|
(spreadExpression) => {
|
|
if (ts.isIdentifier(spreadExpression)) {
|
|
copyFsSafeStoreObjectAliases(
|
|
objectName,
|
|
spreadExpression.text,
|
|
storeScope,
|
|
jsonStoreScope,
|
|
);
|
|
} else if (ts.isObjectLiteralExpression(spreadExpression)) {
|
|
registerFsSafeStoreObjectAliases(
|
|
objectName,
|
|
spreadExpression,
|
|
storeScope,
|
|
jsonStoreScope,
|
|
conditionalWrite,
|
|
);
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
function setFsModuleObjectProperty(scope, name, isFsModule, conditionalWrite) {
|
|
if (isFsModule) {
|
|
scope.set(name, true);
|
|
} else if (!conditionalWrite) {
|
|
scope.set(name, false);
|
|
}
|
|
}
|
|
|
|
function clearFsModuleObjectProperties(scope, objectName) {
|
|
scope.set(objectName, false);
|
|
shadowObjectPropertyScopes([scope], objectName, false, scope);
|
|
}
|
|
|
|
function registerFsModuleObjectProperties(
|
|
objectName,
|
|
initializer,
|
|
scope = lastScope(fsModulePropertyScopes),
|
|
conditionalWrite = false,
|
|
) {
|
|
visitObjectLiteralProperties(objectName, initializer, (name, value) => {
|
|
setFsModuleObjectProperty(scope, name, isFsModuleExpression(value), conditionalWrite);
|
|
});
|
|
}
|
|
|
|
function collectFsModuleBindingsFromBinding(node) {
|
|
if (
|
|
!ts.isVariableDeclaration(node) ||
|
|
!ts.isObjectBindingPattern(node.name) ||
|
|
!node.initializer ||
|
|
!isFsBindingExpression(node.initializer)
|
|
) {
|
|
return;
|
|
}
|
|
for (const element of node.name.elements) {
|
|
const propertyName = element.propertyName;
|
|
const bindingName = element.name;
|
|
const importedName = propertyName
|
|
? propertyNameText(propertyName)
|
|
: ts.isIdentifier(bindingName)
|
|
? bindingName.text
|
|
: null;
|
|
if (importedName === "promises" && ts.isIdentifier(bindingName)) {
|
|
lastScope(fsModuleBindingScopes).set(bindingName.text, true);
|
|
}
|
|
}
|
|
}
|
|
|
|
function isFsBindingExpression(expression) {
|
|
const initializer = unwrapExpression(expression);
|
|
if (
|
|
isFsRequireExpression(initializer, isNodeRequireName) ||
|
|
isFsDynamicImportExpression(initializer)
|
|
) {
|
|
return true;
|
|
}
|
|
if (ts.isIdentifier(initializer)) {
|
|
return resolveFsModuleBinding(initializer.text);
|
|
}
|
|
return (
|
|
ts.isPropertyAccessExpression(initializer) &&
|
|
initializer.name.text === "promises" &&
|
|
(isFsRequireExpression(initializer.expression, isNodeRequireName) ||
|
|
isFsDynamicImportExpression(initializer.expression) ||
|
|
(ts.isIdentifier(initializer.expression) &&
|
|
resolveFsModuleBinding(initializer.expression.text)))
|
|
);
|
|
}
|
|
|
|
function collectFsWriteAliasesFromBindingInto(
|
|
node,
|
|
aliases,
|
|
isFsBinding = isFsBindingExpression,
|
|
) {
|
|
if (
|
|
!ts.isVariableDeclaration(node) ||
|
|
!ts.isObjectBindingPattern(node.name) ||
|
|
!node.initializer
|
|
) {
|
|
return;
|
|
}
|
|
if (!isFsBinding(node.initializer)) {
|
|
return;
|
|
}
|
|
collectFsWriteAliasesFromPattern(node.name, aliases);
|
|
}
|
|
|
|
function collectFsWriteAliasesFromPattern(pattern, aliases) {
|
|
for (const element of pattern.elements) {
|
|
const propertyName = element.propertyName;
|
|
const bindingName = element.name;
|
|
const importedName = propertyName
|
|
? propertyNameText(propertyName)
|
|
: ts.isIdentifier(bindingName)
|
|
? bindingName.text
|
|
: null;
|
|
if (!importedName) {
|
|
continue;
|
|
}
|
|
if (legacyWriteCallees.has(importedName) && ts.isIdentifier(bindingName)) {
|
|
aliases.set(bindingName.text, importedName);
|
|
}
|
|
if (importedName === "promises" && ts.isObjectBindingPattern(bindingName)) {
|
|
collectFsWriteAliasesFromPattern(bindingName, aliases);
|
|
}
|
|
}
|
|
}
|
|
|
|
function markArrayBindingPatternFromForOf(initializer, expression) {
|
|
if (!ts.isVariableDeclarationList(initializer)) {
|
|
return;
|
|
}
|
|
const declaration = initializer.declarations[0];
|
|
if (!declaration || !ts.isArrayBindingPattern(declaration.name)) {
|
|
return;
|
|
}
|
|
const iterable = unwrapExpression(expression);
|
|
if (!ts.isArrayLiteralExpression(iterable)) {
|
|
return;
|
|
}
|
|
|
|
declaration.name.elements.forEach((bindingElement, index) => {
|
|
if (ts.isOmittedExpression(bindingElement) || !ts.isIdentifier(bindingElement.name)) {
|
|
return;
|
|
}
|
|
|
|
const elementsAtIndex = iterable.elements
|
|
.map((element) => arrayLiteralElementAt(element, index))
|
|
.filter(Boolean);
|
|
if (elementsAtIndex.length === 0) {
|
|
return;
|
|
}
|
|
|
|
lastScope(legacyPathScopes).set(
|
|
bindingElement.name.text,
|
|
elementsAtIndex.some((element) => expressionContainsLegacyStore(element)),
|
|
);
|
|
lastScope(literalTextScopes).set(
|
|
bindingElement.name.text,
|
|
mergeExhaustiveLiteralTexts(
|
|
[],
|
|
elementsAtIndex.flatMap((element) => literalTextsFromExpression(element)),
|
|
),
|
|
);
|
|
});
|
|
}
|
|
|
|
function pathArgumentsForFsWrite(name, args) {
|
|
if (
|
|
name === "appendRegularFile" ||
|
|
name === "appendRegularFileSync" ||
|
|
name === "replaceFileAtomic" ||
|
|
name === "replaceFileAtomicSync"
|
|
) {
|
|
const first = args[0];
|
|
if (!first || !ts.isObjectLiteralExpression(unwrapExpression(first))) {
|
|
return first ? [first] : [];
|
|
}
|
|
const objectArg = unwrapExpression(first);
|
|
return objectArg.properties.flatMap((property) => {
|
|
if (ts.isPropertyAssignment(property)) {
|
|
const key = property.name;
|
|
const propertyName =
|
|
ts.isIdentifier(key) || ts.isStringLiteral(key) || ts.isNumericLiteral(key)
|
|
? key.text
|
|
: null;
|
|
return propertyName === "filePath" ? [property.initializer] : [];
|
|
}
|
|
if (ts.isShorthandPropertyAssignment(property) && property.name.text === "filePath") {
|
|
return [property.name];
|
|
}
|
|
return [];
|
|
});
|
|
}
|
|
if (
|
|
name === "saveJsonFile" ||
|
|
name === "writeJson" ||
|
|
name === "writeJsonAtomic" ||
|
|
name === "writeJsonFileAtomically" ||
|
|
name === "writeJsonSync" ||
|
|
name === "writeTextAtomic"
|
|
) {
|
|
return args.slice(0, 1);
|
|
}
|
|
if (name === "copyFile" || name === "copyFileSync" || name === "cp" || name === "cpSync") {
|
|
return args.slice(1, 2);
|
|
}
|
|
if (name === "rename" || name === "renameSync") {
|
|
return args.slice(0, 2);
|
|
}
|
|
return args.slice(0, 1);
|
|
}
|
|
|
|
function openFlagsMayWrite(flags) {
|
|
if (!flags) {
|
|
return false;
|
|
}
|
|
const unwrapped = unwrapExpression(flags);
|
|
if (ts.isStringLiteralLike(unwrapped)) {
|
|
return /[wa+]/u.test(unwrapped.text);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function fsWriteCallMayWrite(name, args) {
|
|
if (name === "open" || name === "openSync") {
|
|
return openFlagsMayWrite(args[1]);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function propertyNameText(name) {
|
|
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
|
|
return name.text;
|
|
}
|
|
if (ts.isComputedPropertyName(name)) {
|
|
return elementAccessName(name.expression);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function isVarVariableDeclaration(node) {
|
|
return (
|
|
ts.isVariableDeclarationList(node.parent) &&
|
|
(node.parent.flags & ts.NodeFlags.BlockScoped) === 0
|
|
);
|
|
}
|
|
|
|
function isAmbientVariableDeclaration(node) {
|
|
let current = node.parent;
|
|
while (current && !ts.isSourceFile(current)) {
|
|
const modifiers = ts.canHaveModifiers(current) ? (ts.getModifiers(current) ?? []) : [];
|
|
if (modifiers.some((modifier) => modifier.kind === ts.SyntaxKind.DeclareKeyword)) {
|
|
return true;
|
|
}
|
|
current = current.parent;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function isTypeSyntaxNode(node) {
|
|
return node.kind >= ts.SyntaxKind.FirstTypeNode && node.kind <= ts.SyntaxKind.LastTypeNode;
|
|
}
|
|
|
|
function objectLiteralPropertyLegacyValue(objectLiteral, propertyName) {
|
|
let result = null;
|
|
for (const property of objectLiteral.properties) {
|
|
if (ts.isSpreadAssignment(property)) {
|
|
const spreadExpression = unwrapExpression(property.expression);
|
|
if (ts.isIdentifier(spreadExpression)) {
|
|
const propertyValue = lookupLegacyObjectProperty(spreadExpression.text, propertyName);
|
|
if (propertyValue !== null) {
|
|
result = propertyValue;
|
|
}
|
|
continue;
|
|
}
|
|
if (ts.isObjectLiteralExpression(spreadExpression)) {
|
|
const propertyValue = objectLiteralPropertyLegacyValue(spreadExpression, propertyName);
|
|
if (propertyValue !== null) {
|
|
result = propertyValue;
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
if (ts.isPropertyAssignment(property) && propertyNameText(property.name) === propertyName) {
|
|
result = expressionContainsLegacyStore(property.initializer);
|
|
continue;
|
|
}
|
|
if (ts.isShorthandPropertyAssignment(property) && property.name.text === propertyName) {
|
|
result = expressionContainsLegacyStore(property.name);
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function objectLiteralPropertyContainsLegacyStore(objectLiteral, propertyName) {
|
|
return objectLiteralPropertyLegacyValue(objectLiteral, propertyName) === true;
|
|
}
|
|
|
|
function clearLegacyObjectProperties(scope, objectName) {
|
|
const prefix = `${objectName}.`;
|
|
for (const key of scope.keys()) {
|
|
if (key.startsWith(prefix)) {
|
|
scope.delete(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
function clearKnownLegacyObjectLiterals(scope, objectName) {
|
|
const prefix = `${objectName}.`;
|
|
for (const key of scope.keys()) {
|
|
if (key.startsWith(prefix)) {
|
|
scope.delete(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
function legacyObjectPropertiesFromAssignment(
|
|
objectName,
|
|
initializer,
|
|
existingScope = new Map(),
|
|
) {
|
|
return legacyObjectPropertyRewriteValues(objectName, initializer, existingScope);
|
|
}
|
|
|
|
function legacyKnownObjectLiteralsFromAssignment(objectName, initializer) {
|
|
const knownObjectLiterals = new Map();
|
|
markLegacyObjectProperties(objectName, initializer, new Map(), knownObjectLiterals);
|
|
return knownObjectLiterals;
|
|
}
|
|
|
|
function branchIdentifierAssignmentKey(index, name) {
|
|
return `${index}:${name}`;
|
|
}
|
|
|
|
function branchPropertyAssignmentKey(index, objectName, propertyName) {
|
|
return `${index}:${objectPropertyKey(objectName, propertyName)}`;
|
|
}
|
|
|
|
function branchWrapperAssignmentKey(index, name) {
|
|
return `${index}:${name}`;
|
|
}
|
|
|
|
function recordBranchIdentifierAssignment(
|
|
index,
|
|
name,
|
|
value,
|
|
initializer,
|
|
literalTexts,
|
|
objectProperties = null,
|
|
knownUndefined = isKnownUndefinedExpression(initializer),
|
|
) {
|
|
const effects = lastScope(branchEffectScopes);
|
|
if (!effects) {
|
|
return;
|
|
}
|
|
effects.identifierAssignments.set(branchIdentifierAssignmentKey(index, name), {
|
|
index,
|
|
knownUndefined,
|
|
knownObjectLiteral: isKnownLegacyObjectLiteralExpression(initializer),
|
|
knownObjectLiterals: legacyKnownObjectLiteralsFromAssignment(name, initializer),
|
|
literalTexts,
|
|
name,
|
|
value,
|
|
objectProperties: objectProperties ?? legacyObjectPropertiesFromAssignment(name, initializer),
|
|
});
|
|
const prefix = `${index}:${name}.`;
|
|
for (const key of effects.propertyAssignments.keys()) {
|
|
if (key.startsWith(prefix)) {
|
|
effects.propertyAssignments.delete(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
function recordBranchPropertyAssignment(
|
|
index,
|
|
objectName,
|
|
propertyName,
|
|
value,
|
|
knownObjectLiteral = false,
|
|
) {
|
|
const effects = lastScope(branchEffectScopes);
|
|
if (!effects) {
|
|
return;
|
|
}
|
|
const identifierAssignment = effects.identifierAssignments.get(
|
|
branchIdentifierAssignmentKey(index, objectName),
|
|
);
|
|
const propertyKey = objectPropertyKey(objectName, propertyName);
|
|
if (identifierAssignment) {
|
|
identifierAssignment.objectProperties.set(propertyKey, value);
|
|
identifierAssignment.knownObjectLiterals.set(propertyKey, knownObjectLiteral);
|
|
return;
|
|
}
|
|
effects.propertyAssignments.set(branchPropertyAssignmentKey(index, objectName, propertyName), {
|
|
index,
|
|
objectName,
|
|
propertyName,
|
|
value,
|
|
knownObjectLiteral,
|
|
});
|
|
}
|
|
|
|
function recordBranchWrapperAssignment(index, name, value) {
|
|
const effects = lastScope(branchEffectScopes);
|
|
if (!effects) {
|
|
return;
|
|
}
|
|
effects.wrapperAssignments.set(branchWrapperAssignmentKey(index, name), {
|
|
index,
|
|
name,
|
|
value: cloneWrapperFunctionValue(value),
|
|
});
|
|
}
|
|
|
|
function recordBranchWrapperObjectRewrite(index, objectName, initializer) {
|
|
const fact = factFromExpression(initializer);
|
|
if (!fact.knownObject) {
|
|
return;
|
|
}
|
|
const retained = new Set(
|
|
[...fact.wrappers.keys()].map((key) => `${objectName}${key.slice(fact.root.length)}`),
|
|
);
|
|
const prefix = `${objectName}.`;
|
|
for (const scope of wrapperFunctionScopes) {
|
|
for (const name of scope.keys()) {
|
|
if (name.startsWith(prefix) && !retained.has(name)) {
|
|
recordBranchWrapperAssignment(index, name, null);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function recordBranchFsIdentifierAssignment(
|
|
index,
|
|
name,
|
|
moduleValue,
|
|
writeAlias,
|
|
fsSafeFactoryAlias,
|
|
fsSafeStoreValue,
|
|
fsSafeJsonStoreValue,
|
|
requireAlias,
|
|
) {
|
|
const effects = lastScope(branchEffectScopes);
|
|
if (!effects) {
|
|
return;
|
|
}
|
|
effects.fsIdentifierAssignments.set(branchIdentifierAssignmentKey(index, name), {
|
|
fsSafeFactoryAlias,
|
|
fsSafeJsonStoreValue,
|
|
fsSafeStoreValue,
|
|
index,
|
|
moduleValue,
|
|
name,
|
|
requireAlias,
|
|
writeAlias,
|
|
});
|
|
}
|
|
|
|
function recordBranchFsSafePropertyAssignment(
|
|
index,
|
|
objectName,
|
|
propertyName,
|
|
storeValue,
|
|
jsonStoreValue,
|
|
) {
|
|
const effects = lastScope(branchEffectScopes);
|
|
if (!effects) {
|
|
return;
|
|
}
|
|
effects.fsSafePropertyAssignments.set(
|
|
branchPropertyAssignmentKey(index, objectName, propertyName),
|
|
{
|
|
index,
|
|
jsonStoreValue,
|
|
objectName,
|
|
propertyName,
|
|
storeValue,
|
|
},
|
|
);
|
|
}
|
|
|
|
function recordBranchFsSafeObjectPropertyAssignment(
|
|
index,
|
|
objectName,
|
|
propertyName,
|
|
initializer,
|
|
storeValue,
|
|
jsonStoreValue,
|
|
) {
|
|
const assignmentRoot = objectPropertyKey(objectName, propertyName);
|
|
const storeAssignments = new Map([[assignmentRoot, storeValue]]);
|
|
const jsonStoreAssignments = new Map([[assignmentRoot, jsonStoreValue]]);
|
|
const descendantPrefix = `${assignmentRoot}.`;
|
|
for (const scope of fsSafeStoreScopes) {
|
|
for (const key of scope.keys()) {
|
|
if (key.startsWith(descendantPrefix)) {
|
|
storeAssignments.set(key, false);
|
|
}
|
|
}
|
|
}
|
|
for (const scope of fsSafeJsonStoreScopes) {
|
|
for (const key of scope.keys()) {
|
|
if (key.startsWith(descendantPrefix)) {
|
|
jsonStoreAssignments.set(key, false);
|
|
}
|
|
}
|
|
}
|
|
registerFsSafeStoreObjectAliases(
|
|
assignmentRoot,
|
|
initializer,
|
|
storeAssignments,
|
|
jsonStoreAssignments,
|
|
);
|
|
const assignmentKeys = new Set([...storeAssignments.keys(), ...jsonStoreAssignments.keys()]);
|
|
for (const key of assignmentKeys) {
|
|
recordBranchFsSafePropertyAssignment(
|
|
index,
|
|
objectName,
|
|
key.slice(`${objectName}.`.length),
|
|
storeAssignments.get(key) === true,
|
|
jsonStoreAssignments.get(key) === true,
|
|
);
|
|
}
|
|
}
|
|
|
|
function mergeWrapperAssignmentValues(left, right) {
|
|
const records = [
|
|
...wrapperRecords(left).map(cloneWrapperRecord),
|
|
...wrapperRecords(right).map(cloneWrapperRecord),
|
|
];
|
|
if (records.length === 0) {
|
|
return null;
|
|
}
|
|
return records.length === 1 ? records[0] : records;
|
|
}
|
|
|
|
function mergeExhaustiveBranchEffects(thenEffects, elseEffects) {
|
|
const mergedIdentifierNames = new Set();
|
|
const parentEffect = lastScope(branchEffectScopes);
|
|
const applyToTargetScopes = !lastScope(conditionalExecutionScopes) && !parentEffect;
|
|
for (const [key, thenAssignment] of thenEffects.fsIdentifierAssignments) {
|
|
const elseAssignment = elseEffects.fsIdentifierAssignments.get(key);
|
|
if (!elseAssignment || thenAssignment.index >= legacyPathScopes.length) {
|
|
continue;
|
|
}
|
|
const { index, name } = thenAssignment;
|
|
const mergedModuleValue =
|
|
thenAssignment.moduleValue === true || elseAssignment.moduleValue === true;
|
|
const mergedWriteAlias = thenAssignment.writeAlias ?? elseAssignment.writeAlias;
|
|
const mergedFsSafeFactoryAlias =
|
|
thenAssignment.fsSafeFactoryAlias ?? elseAssignment.fsSafeFactoryAlias;
|
|
const mergedFsSafeStoreValue =
|
|
thenAssignment.fsSafeStoreValue === true || elseAssignment.fsSafeStoreValue === true;
|
|
const mergedFsSafeJsonStoreValue =
|
|
thenAssignment.fsSafeJsonStoreValue === true ||
|
|
elseAssignment.fsSafeJsonStoreValue === true;
|
|
const mergedRequireAlias =
|
|
thenAssignment.requireAlias === true || elseAssignment.requireAlias === true;
|
|
if (applyToTargetScopes) {
|
|
fsModuleBindingScopes[index].set(name, mergedModuleValue);
|
|
fsWriteAliasScopes[index].set(name, mergedWriteAlias);
|
|
fsSafeStoreFactoryAliasScopes[index].set(name, mergedFsSafeFactoryAlias);
|
|
fsSafeStoreScopes[index].set(name, mergedFsSafeStoreValue);
|
|
fsSafeJsonStoreScopes[index].set(name, mergedFsSafeJsonStoreValue);
|
|
requireAliasScopes[index].set(name, mergedRequireAlias);
|
|
}
|
|
lastScope(fsModuleBindingScopes).set(name, mergedModuleValue);
|
|
lastScope(fsWriteAliasScopes).set(name, mergedWriteAlias);
|
|
lastScope(fsSafeStoreFactoryAliasScopes).set(name, mergedFsSafeFactoryAlias);
|
|
lastScope(fsSafeStoreScopes).set(name, mergedFsSafeStoreValue);
|
|
lastScope(fsSafeJsonStoreScopes).set(name, mergedFsSafeJsonStoreValue);
|
|
lastScope(requireAliasScopes).set(name, mergedRequireAlias);
|
|
if (parentEffect) {
|
|
parentEffect.fsIdentifierAssignments.set(branchIdentifierAssignmentKey(index, name), {
|
|
fsSafeFactoryAlias: mergedFsSafeFactoryAlias,
|
|
fsSafeJsonStoreValue: mergedFsSafeJsonStoreValue,
|
|
fsSafeStoreValue: mergedFsSafeStoreValue,
|
|
index,
|
|
moduleValue: mergedModuleValue,
|
|
name,
|
|
requireAlias: mergedRequireAlias,
|
|
writeAlias: mergedWriteAlias,
|
|
});
|
|
}
|
|
}
|
|
for (const [key, thenAssignment] of thenEffects.identifierAssignments) {
|
|
const elseAssignment = elseEffects.identifierAssignments.get(key);
|
|
if (!elseAssignment || thenAssignment.index >= legacyPathScopes.length) {
|
|
continue;
|
|
}
|
|
const { index, name } = thenAssignment;
|
|
mergedIdentifierNames.add(branchIdentifierAssignmentKey(index, name));
|
|
const merged = mergeLegacyPathBranchAssignments(thenAssignment, elseAssignment);
|
|
if (applyToTargetScopes) {
|
|
const pathScope = legacyPathScopes[index];
|
|
const literalScope = literalTextScopes[index];
|
|
const knownUndefinedScope = knownUndefinedScopes[index];
|
|
const propertyScope = legacyObjectPropertyScopes[index];
|
|
const knownObjectLiteralScope = legacyKnownObjectLiteralScopes[index];
|
|
clearKnownLegacyObjectLiterals(knownObjectLiteralScope, name);
|
|
knownObjectLiteralScope.set(name, merged.knownObjectLiteral);
|
|
for (const [knownObjectLiteralKey, value] of merged.knownObjectLiterals) {
|
|
knownObjectLiteralScope.set(knownObjectLiteralKey, value);
|
|
}
|
|
pathScope.set(name, merged.value);
|
|
knownUndefinedScope.set(name, merged.knownUndefined);
|
|
literalScope.set(name, merged.literalTexts);
|
|
clearLegacyObjectProperties(propertyScope, name);
|
|
for (const [propertyKey, value] of merged.objectProperties) {
|
|
propertyScope.set(propertyKey, value);
|
|
}
|
|
}
|
|
clearKnownLegacyObjectLiterals(lastScope(legacyKnownObjectLiteralScopes), name);
|
|
lastScope(legacyKnownObjectLiteralScopes).set(name, merged.knownObjectLiteral);
|
|
for (const [knownObjectLiteralKey, value] of merged.knownObjectLiterals) {
|
|
lastScope(legacyKnownObjectLiteralScopes).set(knownObjectLiteralKey, value);
|
|
}
|
|
lastScope(legacyPathScopes).set(name, merged.value);
|
|
lastScope(knownUndefinedScopes).set(name, merged.knownUndefined);
|
|
lastScope(literalTextScopes).set(name, merged.literalTexts);
|
|
clearLegacyObjectProperties(lastScope(legacyObjectPropertyScopes), name);
|
|
for (const [propertyKey, value] of merged.objectProperties) {
|
|
lastScope(legacyObjectPropertyScopes).set(propertyKey, value);
|
|
}
|
|
if (parentEffect) {
|
|
parentEffect.identifierAssignments.set(branchIdentifierAssignmentKey(index, name), {
|
|
index,
|
|
...merged,
|
|
literalTexts: merged.literalTexts ?? [],
|
|
name,
|
|
});
|
|
}
|
|
}
|
|
for (const [key, thenAssignment] of thenEffects.fsSafePropertyAssignments) {
|
|
const elseAssignment = elseEffects.fsSafePropertyAssignments.get(key);
|
|
if (!elseAssignment || thenAssignment.index >= legacyPathScopes.length) {
|
|
continue;
|
|
}
|
|
const mergedStoreValue =
|
|
thenAssignment.storeValue === true || elseAssignment.storeValue === true;
|
|
const mergedJsonStoreValue =
|
|
thenAssignment.jsonStoreValue === true || elseAssignment.jsonStoreValue === true;
|
|
const propertyKey = objectPropertyKey(thenAssignment.objectName, thenAssignment.propertyName);
|
|
if (applyToTargetScopes) {
|
|
fsSafeStoreScopes[thenAssignment.index].set(propertyKey, mergedStoreValue);
|
|
fsSafeJsonStoreScopes[thenAssignment.index].set(propertyKey, mergedJsonStoreValue);
|
|
}
|
|
lastScope(fsSafeStoreScopes).set(propertyKey, mergedStoreValue);
|
|
lastScope(fsSafeJsonStoreScopes).set(propertyKey, mergedJsonStoreValue);
|
|
if (parentEffect) {
|
|
recordBranchFsSafePropertyAssignment(
|
|
thenAssignment.index,
|
|
thenAssignment.objectName,
|
|
thenAssignment.propertyName,
|
|
mergedStoreValue,
|
|
mergedJsonStoreValue,
|
|
);
|
|
}
|
|
}
|
|
for (const [key, thenAssignment] of thenEffects.propertyAssignments) {
|
|
const elseAssignment = elseEffects.propertyAssignments.get(key);
|
|
if (!elseAssignment || thenAssignment.index >= legacyPathScopes.length) {
|
|
continue;
|
|
}
|
|
const identifierKey = branchIdentifierAssignmentKey(
|
|
thenAssignment.index,
|
|
thenAssignment.objectName,
|
|
);
|
|
if (mergedIdentifierNames.has(identifierKey)) {
|
|
continue;
|
|
}
|
|
const mergedValue = mergeLegacyObjectPropertyValues(
|
|
thenAssignment.value,
|
|
elseAssignment.value,
|
|
);
|
|
const mergedKnownObjectLiteral =
|
|
thenAssignment.knownObjectLiteral && elseAssignment.knownObjectLiteral;
|
|
const propertyKey = objectPropertyKey(thenAssignment.objectName, thenAssignment.propertyName);
|
|
if (applyToTargetScopes) {
|
|
legacyObjectPropertyScopes[thenAssignment.index].set(propertyKey, mergedValue);
|
|
legacyKnownObjectLiteralScopes[thenAssignment.index].set(
|
|
propertyKey,
|
|
mergedKnownObjectLiteral,
|
|
);
|
|
}
|
|
lastScope(legacyObjectPropertyScopes).set(propertyKey, mergedValue);
|
|
lastScope(legacyKnownObjectLiteralScopes).set(propertyKey, mergedKnownObjectLiteral);
|
|
if (parentEffect) {
|
|
recordBranchPropertyAssignment(
|
|
thenAssignment.index,
|
|
thenAssignment.objectName,
|
|
thenAssignment.propertyName,
|
|
mergedValue,
|
|
mergedKnownObjectLiteral,
|
|
);
|
|
}
|
|
}
|
|
for (const [key, thenAssignment] of thenEffects.wrapperAssignments) {
|
|
const elseAssignment = elseEffects.wrapperAssignments.get(key);
|
|
if (!elseAssignment || thenAssignment.index >= legacyPathScopes.length) {
|
|
continue;
|
|
}
|
|
const { index, name } = thenAssignment;
|
|
const mergedValue = mergeWrapperAssignmentValues(thenAssignment.value, elseAssignment.value);
|
|
if (applyToTargetScopes) {
|
|
wrapperFunctionScopes[index].set(name, cloneWrapperFunctionValue(mergedValue));
|
|
}
|
|
lastScope(wrapperFunctionScopes).set(name, cloneWrapperFunctionValue(mergedValue));
|
|
if (parentEffect) {
|
|
parentEffect.wrapperAssignments.set(branchWrapperAssignmentKey(index, name), {
|
|
index,
|
|
name,
|
|
value: cloneWrapperFunctionValue(mergedValue),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function markLegacyObjectProperties(
|
|
objectName,
|
|
initializer,
|
|
targetScope = lastScope(legacyObjectPropertyScopes),
|
|
knownObjectLiteralScope = lastScope(legacyKnownObjectLiteralScopes),
|
|
) {
|
|
const objectLiteral = unwrapExpression(initializer);
|
|
if (ts.isIdentifier(objectLiteral)) {
|
|
copyLegacyObjectProperties(objectName, objectLiteral.text, targetScope);
|
|
if (knownObjectLiteralScope) {
|
|
copyKnownLegacyObjectLiterals(objectName, objectLiteral.text, knownObjectLiteralScope);
|
|
}
|
|
return;
|
|
}
|
|
if (!ts.isObjectLiteralExpression(objectLiteral)) {
|
|
knownObjectLiteralScope?.set(objectName, false);
|
|
return;
|
|
}
|
|
knownObjectLiteralScope?.set(objectName, true);
|
|
for (const property of objectLiteral.properties) {
|
|
if (ts.isPropertyAssignment(property)) {
|
|
const resolvedNames = ts.isComputedPropertyName(property.name)
|
|
? elementAccessNames(property.name.expression)
|
|
: [propertyNameText(property.name)].filter(Boolean);
|
|
const names = resolvedNames.length > 0 ? resolvedNames : [unknownComputedPropertyName];
|
|
for (const name of names) {
|
|
const propertyKey = `${objectName}.${name}`;
|
|
const unknownComputed = name === unknownComputedPropertyName;
|
|
if (!unknownComputed) {
|
|
clearLegacyObjectProperties(targetScope, propertyKey);
|
|
if (knownObjectLiteralScope) {
|
|
clearKnownLegacyObjectLiterals(knownObjectLiteralScope, propertyKey);
|
|
}
|
|
}
|
|
const propertyValue = legacyObjectPropertyValueFromExpression(property.initializer);
|
|
targetScope.set(
|
|
propertyKey,
|
|
unknownComputed
|
|
? targetScope.get(propertyKey) === true || propertyValue
|
|
: propertyValue,
|
|
);
|
|
const propertyInitializer = unwrapExpression(property.initializer);
|
|
if (ts.isIdentifier(propertyInitializer)) {
|
|
copyLegacyObjectProperties(propertyKey, propertyInitializer.text, targetScope);
|
|
if (knownObjectLiteralScope) {
|
|
copyKnownLegacyObjectLiterals(
|
|
propertyKey,
|
|
propertyInitializer.text,
|
|
knownObjectLiteralScope,
|
|
);
|
|
}
|
|
} else {
|
|
knownObjectLiteralScope?.set(
|
|
propertyKey,
|
|
isKnownLegacyObjectLiteralExpression(property.initializer),
|
|
);
|
|
}
|
|
if (ts.isObjectLiteralExpression(propertyInitializer)) {
|
|
markLegacyObjectProperties(
|
|
propertyKey,
|
|
property.initializer,
|
|
targetScope,
|
|
knownObjectLiteralScope,
|
|
);
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
if (ts.isShorthandPropertyAssignment(property)) {
|
|
const propertyKey = `${objectName}.${property.name.text}`;
|
|
targetScope.set(propertyKey, legacyObjectPropertyValueFromExpression(property.name));
|
|
copyLegacyObjectProperties(propertyKey, property.name.text, targetScope);
|
|
if (knownObjectLiteralScope) {
|
|
copyKnownLegacyObjectLiterals(propertyKey, property.name.text, knownObjectLiteralScope);
|
|
}
|
|
continue;
|
|
}
|
|
if (ts.isSpreadAssignment(property)) {
|
|
const spreadExpression = unwrapExpression(property.expression);
|
|
if (ts.isIdentifier(spreadExpression)) {
|
|
copyLegacyObjectProperties(objectName, spreadExpression.text, targetScope);
|
|
if (knownObjectLiteralScope) {
|
|
copyKnownLegacyObjectLiterals(
|
|
objectName,
|
|
spreadExpression.text,
|
|
knownObjectLiteralScope,
|
|
);
|
|
}
|
|
} else if (ts.isObjectLiteralExpression(spreadExpression)) {
|
|
markLegacyObjectProperties(
|
|
objectName,
|
|
spreadExpression,
|
|
targetScope,
|
|
knownObjectLiteralScope,
|
|
);
|
|
} else {
|
|
knownObjectLiteralScope?.set(objectName, false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function copyLegacyObjectProperties(
|
|
targetName,
|
|
sourceName,
|
|
targetScope = lastScope(legacyObjectPropertyScopes),
|
|
) {
|
|
const sourcePrefix = `${sourceName}.`;
|
|
for (let index = legacyObjectPropertyScopes.length - 1; index >= 0; index--) {
|
|
const scope = legacyObjectPropertyScopes[index];
|
|
const copiedEntries = [];
|
|
for (const [key, value] of scope) {
|
|
if (key.startsWith(sourcePrefix)) {
|
|
copiedEntries.push([`${targetName}.${key.slice(sourcePrefix.length)}`, value]);
|
|
}
|
|
}
|
|
copiedEntries.sort((left, right) => left[0].length - right[0].length);
|
|
for (const [key, value] of copiedEntries) {
|
|
clearLegacyObjectProperties(targetScope, key);
|
|
targetScope.set(key, value);
|
|
}
|
|
const copied = copiedEntries.length > 0;
|
|
if (copied || legacyPathScopes[index].has(sourceName)) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
function copyKnownLegacyObjectLiterals(
|
|
targetName,
|
|
sourceName,
|
|
targetScope = lastScope(legacyKnownObjectLiteralScopes),
|
|
) {
|
|
targetScope.set(targetName, lookupKnownLegacyObjectLiteral(sourceName));
|
|
const sourcePrefix = `${sourceName}.`;
|
|
for (let index = legacyKnownObjectLiteralScopes.length - 1; index >= 0; index--) {
|
|
const scope = legacyKnownObjectLiteralScopes[index];
|
|
const copiedEntries = [];
|
|
for (const [key, value] of scope) {
|
|
if (key.startsWith(sourcePrefix)) {
|
|
copiedEntries.push([`${targetName}.${key.slice(sourcePrefix.length)}`, value]);
|
|
}
|
|
}
|
|
copiedEntries.sort((left, right) => left[0].length - right[0].length);
|
|
for (const [key, value] of copiedEntries) {
|
|
clearKnownLegacyObjectLiterals(targetScope, key);
|
|
targetScope.set(key, value);
|
|
}
|
|
const copied = copiedEntries.length > 0;
|
|
if (copied || scope.has(sourceName) || legacyPathScopes[index].has(sourceName)) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
function copyScopedLegacyObjectProperties(targetName, sourceName, sourceScope) {
|
|
const sourcePrefix = `${sourceName}.`;
|
|
const copiedEntries = [];
|
|
for (const [key, value] of sourceScope) {
|
|
if (key.startsWith(sourcePrefix)) {
|
|
copiedEntries.push([`${targetName}.${key.slice(sourcePrefix.length)}`, value]);
|
|
}
|
|
}
|
|
copiedEntries.sort((left, right) => left[0].length - right[0].length);
|
|
for (const [key, value] of copiedEntries) {
|
|
clearLegacyObjectProperties(lastScope(legacyObjectPropertyScopes), key);
|
|
lastScope(legacyObjectPropertyScopes).set(key, value);
|
|
}
|
|
}
|
|
|
|
function copyScopedKnownLegacyObjectLiterals(targetName, sourceName, sourceScope) {
|
|
lastScope(legacyKnownObjectLiteralScopes).set(targetName, sourceScope.get(sourceName) === true);
|
|
const sourcePrefix = `${sourceName}.`;
|
|
const copiedEntries = [];
|
|
for (const [key, value] of sourceScope) {
|
|
if (key.startsWith(sourcePrefix)) {
|
|
copiedEntries.push([`${targetName}.${key.slice(sourcePrefix.length)}`, value]);
|
|
}
|
|
}
|
|
copiedEntries.sort((left, right) => left[0].length - right[0].length);
|
|
for (const [key, value] of copiedEntries) {
|
|
clearKnownLegacyObjectLiterals(lastScope(legacyKnownObjectLiteralScopes), key);
|
|
lastScope(legacyKnownObjectLiteralScopes).set(key, value);
|
|
}
|
|
}
|
|
|
|
function bindingPatternNames(name) {
|
|
const names = [];
|
|
function visitName(current) {
|
|
if (ts.isIdentifier(current)) {
|
|
names.push(current.text);
|
|
return;
|
|
}
|
|
if (ts.isObjectBindingPattern(current) || ts.isArrayBindingPattern(current)) {
|
|
for (const element of current.elements) {
|
|
if (ts.isBindingElement(element)) {
|
|
visitName(element.name);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
visitName(name);
|
|
return names;
|
|
}
|
|
|
|
function markLegacyPathsFromObjectBinding(bindingPattern, sourceName, propertyPath = []) {
|
|
for (const element of bindingPattern.elements) {
|
|
const propertyName = element.propertyName
|
|
? propertyNameText(element.propertyName)
|
|
: ts.isIdentifier(element.name)
|
|
? element.name.text
|
|
: null;
|
|
if (!propertyName) {
|
|
continue;
|
|
}
|
|
const nextPath = [...propertyPath, propertyName];
|
|
if (ts.isIdentifier(element.name)) {
|
|
const trackedPropertyEntry = lookupLegacyObjectPropertyEntry(
|
|
sourceName,
|
|
nextPath.join("."),
|
|
);
|
|
const usesDefaultInitializer = trackedPropertyEntry.found
|
|
? trackedPropertyEntry.value === explicitUndefinedLegacyObjectPropertyValue
|
|
: trackedPropertyEntry.objectKnown;
|
|
const trackedPropertyValue = trackedPropertyEntry.found
|
|
? trackedPropertyEntry.value === explicitUndefinedLegacyObjectPropertyValue
|
|
? null
|
|
: trackedPropertyEntry.value === true
|
|
: null;
|
|
const propertyValue =
|
|
trackedPropertyValue === null
|
|
? element.initializer
|
|
? expressionContainsLegacyStore(element.initializer)
|
|
: false
|
|
: trackedPropertyValue;
|
|
lastScope(legacyPathScopes).set(element.name.text, propertyValue);
|
|
lastScope(knownUndefinedScopes).set(
|
|
element.name.text,
|
|
usesDefaultInitializer
|
|
? element.initializer
|
|
? isKnownUndefinedExpression(element.initializer)
|
|
: true
|
|
: false,
|
|
);
|
|
const sourcePropertyName = `${sourceName}.${nextPath.join(".")}`;
|
|
copyLegacyObjectProperties(element.name.text, sourcePropertyName);
|
|
copyKnownLegacyObjectLiterals(element.name.text, sourcePropertyName);
|
|
lastScope(wrapperFunctionScopes).set(
|
|
element.name.text,
|
|
cloneWrapperFunctionValue(resolveWrapperFunction(sourcePropertyName)),
|
|
);
|
|
continue;
|
|
}
|
|
if (ts.isObjectBindingPattern(element.name)) {
|
|
markLegacyPathsFromObjectBinding(element.name, sourceName, nextPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
function markLegacyPathsFromInlineObjectBinding(bindingPattern, initializer, propertyPath = []) {
|
|
const sourceName = "<inline-object-binding>";
|
|
const propertyScope = new Map();
|
|
const knownObjectLiteralScope = new Map();
|
|
markLegacyObjectProperties(sourceName, initializer, propertyScope, knownObjectLiteralScope);
|
|
function visitBinding(currentBindingPattern, currentPath) {
|
|
for (const element of currentBindingPattern.elements) {
|
|
const propertyName = element.propertyName
|
|
? propertyNameText(element.propertyName)
|
|
: ts.isIdentifier(element.name)
|
|
? element.name.text
|
|
: null;
|
|
if (!propertyName) {
|
|
continue;
|
|
}
|
|
const nextPath = [...currentPath, propertyName];
|
|
if (ts.isIdentifier(element.name)) {
|
|
const trackedPropertyEntry = lookupScopedLegacyObjectPropertyEntry(
|
|
sourceName,
|
|
nextPath,
|
|
propertyScope,
|
|
knownObjectLiteralScope,
|
|
);
|
|
const usesDefaultInitializer = trackedPropertyEntry.found
|
|
? trackedPropertyEntry.value === explicitUndefinedLegacyObjectPropertyValue
|
|
: trackedPropertyEntry.objectKnown;
|
|
const propertyValue = trackedPropertyEntry.found
|
|
? trackedPropertyEntry.value === explicitUndefinedLegacyObjectPropertyValue
|
|
? element.initializer
|
|
? expressionContainsLegacyStore(element.initializer)
|
|
: false
|
|
: trackedPropertyEntry.value === true
|
|
: trackedPropertyEntry.objectKnown && element.initializer
|
|
? expressionContainsLegacyStore(element.initializer)
|
|
: false;
|
|
lastScope(legacyPathScopes).set(element.name.text, propertyValue);
|
|
lastScope(knownUndefinedScopes).set(
|
|
element.name.text,
|
|
usesDefaultInitializer
|
|
? element.initializer
|
|
? isKnownUndefinedExpression(element.initializer)
|
|
: true
|
|
: false,
|
|
);
|
|
const sourcePropertyName = objectPropertyKey(sourceName, nextPath.join("."));
|
|
copyScopedLegacyObjectProperties(element.name.text, sourcePropertyName, propertyScope);
|
|
copyScopedKnownLegacyObjectLiterals(
|
|
element.name.text,
|
|
sourcePropertyName,
|
|
knownObjectLiteralScope,
|
|
);
|
|
continue;
|
|
}
|
|
if (ts.isObjectBindingPattern(element.name)) {
|
|
visitBinding(element.name, nextPath);
|
|
}
|
|
}
|
|
}
|
|
visitBinding(bindingPattern, propertyPath);
|
|
}
|
|
|
|
function markFsSafeStoresFromObjectBinding(bindingPattern, sourceName, propertyPath = []) {
|
|
for (const element of bindingPattern.elements) {
|
|
const propertyName = element.propertyName
|
|
? propertyNameText(element.propertyName)
|
|
: ts.isIdentifier(element.name)
|
|
? element.name.text
|
|
: null;
|
|
if (!propertyName) {
|
|
continue;
|
|
}
|
|
const nextPath = [...propertyPath, propertyName];
|
|
if (ts.isIdentifier(element.name)) {
|
|
const key = `${sourceName}.${nextPath.join(".")}`;
|
|
const trackedStore = lookupFsSafeStore(key);
|
|
const trackedJsonStore = lookupFsSafeJsonStore(key);
|
|
lastScope(fsSafeStoreScopes).set(
|
|
element.name.text,
|
|
trackedStore ??
|
|
(element.initializer ? isFsSafeStoreExpression(element.initializer) : false),
|
|
);
|
|
lastScope(fsSafeJsonStoreScopes).set(
|
|
element.name.text,
|
|
trackedJsonStore ??
|
|
(element.initializer
|
|
? expressionContainsFsSafeJsonStoreLegacyPath(element.initializer)
|
|
: false),
|
|
);
|
|
continue;
|
|
}
|
|
if (ts.isObjectBindingPattern(element.name)) {
|
|
markFsSafeStoresFromObjectBinding(element.name, sourceName, nextPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
function markFsSafeFactoryAliasesFromObjectBinding(bindingPattern, sourceName) {
|
|
for (const element of bindingPattern.elements) {
|
|
if (!ts.isIdentifier(element.name)) {
|
|
continue;
|
|
}
|
|
const propertyName = element.propertyName
|
|
? propertyNameText(element.propertyName)
|
|
: element.name.text;
|
|
const factoryAlias = propertyName
|
|
? resolveFsSafeStoreFactoryAlias(`${sourceName}.${propertyName}`)
|
|
: null;
|
|
if (factoryAlias) {
|
|
lastScope(fsSafeStoreFactoryAliasScopes).set(element.name.text, factoryAlias);
|
|
}
|
|
}
|
|
}
|
|
|
|
function registerTrackedObjectMethods({
|
|
objectName,
|
|
initializer,
|
|
directSourceName = () => null,
|
|
spreadSourceName = directSourceName,
|
|
onAlias = () => {},
|
|
onUnknownSpread = () => {},
|
|
onSpread = onAlias,
|
|
onPropertyName = () => {},
|
|
onMethod,
|
|
onIdentifier,
|
|
onNested,
|
|
onOther = () => {},
|
|
}) {
|
|
const objectLiteral = unwrapExpression(initializer);
|
|
const directSource = directSourceName(objectLiteral);
|
|
if (directSource) {
|
|
onAlias(objectName, directSource);
|
|
return;
|
|
}
|
|
if (!ts.isObjectLiteralExpression(objectLiteral)) {
|
|
return;
|
|
}
|
|
|
|
for (const property of objectLiteral.properties) {
|
|
if (ts.isSpreadAssignment(property)) {
|
|
const sourceName = spreadSourceName(unwrapExpression(property.expression));
|
|
if (sourceName) {
|
|
onSpread(objectName, sourceName);
|
|
} else {
|
|
onUnknownSpread(objectName);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
const propertyName =
|
|
ts.isMethodDeclaration(property) || ts.isPropertyAssignment(property)
|
|
? propertyNameText(property.name)
|
|
: ts.isShorthandPropertyAssignment(property)
|
|
? property.name.text
|
|
: null;
|
|
if (!propertyName) {
|
|
continue;
|
|
}
|
|
const key = `${objectName}.${propertyName}`;
|
|
onPropertyName(propertyName, key);
|
|
if (ts.isMethodDeclaration(property)) {
|
|
onMethod(key, property);
|
|
continue;
|
|
}
|
|
if (ts.isShorthandPropertyAssignment(property)) {
|
|
onIdentifier(key, property.name.text, property.name);
|
|
continue;
|
|
}
|
|
|
|
const propertyInitializer = unwrapExpression(property.initializer);
|
|
if (ts.isFunctionExpression(propertyInitializer) || ts.isArrowFunction(propertyInitializer)) {
|
|
onMethod(key, propertyInitializer);
|
|
continue;
|
|
}
|
|
if (ts.isIdentifier(propertyInitializer)) {
|
|
onIdentifier(key, propertyInitializer.text, property.initializer);
|
|
continue;
|
|
}
|
|
const hasNestedObject = ts.isObjectLiteralExpression(propertyInitializer);
|
|
if (hasNestedObject) {
|
|
onNested(key, propertyInitializer);
|
|
}
|
|
onOther(key, property.initializer, hasNestedObject);
|
|
}
|
|
}
|
|
|
|
function wrapperRecordForNode(node) {
|
|
return {
|
|
environment: lexicalScopeStacks.map(([scopes]) => [...scopes]),
|
|
node,
|
|
};
|
|
}
|
|
|
|
function registerWrapperFunction(name, node) {
|
|
lastScope(wrapperFunctionScopes).set(name, wrapperRecordForNode(node));
|
|
}
|
|
|
|
function setWrapperFunctionValue(scope, name, value, conditionalWrite) {
|
|
if (value) {
|
|
if (conditionalWrite && scope.has(name)) {
|
|
scope.set(name, [...wrapperRecords(scope.get(name)), ...wrapperRecords(value)]);
|
|
} else {
|
|
scope.set(name, value);
|
|
}
|
|
} else if (!conditionalWrite) {
|
|
scope.set(name, null);
|
|
}
|
|
}
|
|
|
|
function clearWrapperObjectMethods(scope, objectName) {
|
|
shadowObjectPropertyScopes([scope], objectName, null, scope);
|
|
}
|
|
|
|
function clearWrapperObjectMethod(scope, methodName) {
|
|
scope.set(methodName, null);
|
|
clearWrapperObjectMethods(scope, methodName);
|
|
}
|
|
|
|
function shadowVisibleWrapperObjectMethods(objectName) {
|
|
shadowObjectPropertyScopes(wrapperFunctionScopes, objectName, null);
|
|
}
|
|
|
|
function copyWrapperObjectMethods(
|
|
targetName,
|
|
sourceName,
|
|
scope = lastScope(wrapperFunctionScopes),
|
|
conditionalWrite = false,
|
|
) {
|
|
const sourcePrefix = `${sourceName}.`;
|
|
let copiedCount = 0;
|
|
const visibleScopeIndexes = [];
|
|
for (let index = wrapperFunctionScopes.length - 1; index >= 0; index--) {
|
|
visibleScopeIndexes.push(index);
|
|
if (wrapperFunctionScopes[index].has(sourceName) || legacyPathScopes[index].has(sourceName)) {
|
|
break;
|
|
}
|
|
}
|
|
for (const index of visibleScopeIndexes.toReversed()) {
|
|
const sourceScope = wrapperFunctionScopes[index];
|
|
for (const [name, value] of sourceScope) {
|
|
if (!name.startsWith(sourcePrefix)) {
|
|
continue;
|
|
}
|
|
setWrapperFunctionValue(
|
|
scope,
|
|
`${targetName}.${name.slice(sourcePrefix.length)}`,
|
|
cloneWrapperFunctionValue(value),
|
|
conditionalWrite,
|
|
);
|
|
copiedCount += 1;
|
|
}
|
|
}
|
|
return copiedCount;
|
|
}
|
|
|
|
function registerWrapperObjectMethods(
|
|
objectName,
|
|
initializer,
|
|
scope = lastScope(wrapperFunctionScopes),
|
|
conditionalWrite = false,
|
|
) {
|
|
const seenProperties = new Set();
|
|
const remember = (key, value) => setWrapperFunctionValue(scope, key, value, conditionalWrite);
|
|
const copy = (targetName, sourceName) =>
|
|
copyWrapperObjectMethods(targetName, sourceName, scope, conditionalWrite);
|
|
|
|
registerTrackedObjectMethods({
|
|
objectName,
|
|
initializer,
|
|
onUnknownSpread: () => clearWrapperObjectMethods(scope, objectName),
|
|
onSpread: (targetName, sourceName) => {
|
|
if (copy(targetName, sourceName) === 0 && !lookupKnownLegacyObjectLiteral(sourceName)) {
|
|
clearWrapperObjectMethods(scope, targetName);
|
|
}
|
|
},
|
|
spreadSourceName: (expression) =>
|
|
ts.isIdentifier(expression) ? expression.text : callExpressionName(expression),
|
|
onPropertyName: (propertyName, key) => {
|
|
if (seenProperties.has(propertyName)) {
|
|
clearWrapperObjectMethod(scope, key);
|
|
}
|
|
seenProperties.add(propertyName);
|
|
},
|
|
onMethod: (key, method) => remember(key, wrapperRecordForNode(method)),
|
|
onIdentifier: (key, identifier) => {
|
|
const wrapper = resolveWrapperFunction(identifier);
|
|
if (wrapper) {
|
|
remember(key, cloneWrapperFunctionValue(wrapper));
|
|
}
|
|
copy(key, identifier);
|
|
},
|
|
onNested: (key, nested) => registerWrapperObjectMethods(key, nested, scope, conditionalWrite),
|
|
onOther: (key, value) => {
|
|
const wrapper = resolveWrapperExpression(value);
|
|
if (wrapper) {
|
|
remember(key, cloneWrapperFunctionValue(wrapper));
|
|
}
|
|
},
|
|
});
|
|
}
|
|
function wrapperRecords(value) {
|
|
if (!value) {
|
|
return [];
|
|
}
|
|
return Array.isArray(value) ? value : [value];
|
|
}
|
|
|
|
function cloneWrapperRecord(record) {
|
|
return {
|
|
environment: record.environment.map((scopes) => [...scopes]),
|
|
node: record.node,
|
|
};
|
|
}
|
|
|
|
function cloneWrapperFunctionValue(value) {
|
|
if (!value) {
|
|
return null;
|
|
}
|
|
const records = wrapperRecords(value).map(cloneWrapperRecord);
|
|
return Array.isArray(value) ? records : records[0];
|
|
}
|
|
|
|
function registerHoistedWrapperFunctions(statements) {
|
|
for (const statement of statements) {
|
|
if (ts.isFunctionDeclaration(statement) && statement.name) {
|
|
bindIdentifierFact(statement.name.text, undefinedFact());
|
|
registerWrapperFunction(statement.name.text, statement);
|
|
continue;
|
|
}
|
|
if (
|
|
ts.isVariableStatement(statement) &&
|
|
(statement.declarationList.flags & ts.NodeFlags.BlockScoped) !== 0
|
|
) {
|
|
for (const declaration of statement.declarationList.declarations) {
|
|
for (const name of bindingPatternNames(declaration.name)) {
|
|
bindIdentifierFact(name, undefinedFact());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function resolveWrapperFunction(name) {
|
|
for (let index = wrapperFunctionScopes.length - 1; index >= 0; index--) {
|
|
const wrapperScope = wrapperFunctionScopes[index];
|
|
if (wrapperScope.has(name)) {
|
|
return wrapperScope.get(name);
|
|
}
|
|
if (legacyPathScopes[index].has(name)) {
|
|
return null;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function resolveWrapperExpression(expression) {
|
|
const unwrapped = unwrapExpression(expression);
|
|
if (ts.isIdentifier(unwrapped)) {
|
|
return resolveWrapperFunction(unwrapped.text);
|
|
}
|
|
const name = callExpressionName(unwrapped);
|
|
return name ? resolveWrapperFunction(name) : null;
|
|
}
|
|
|
|
function pathArgumentContainsLegacyStore(argument) {
|
|
return expressionContainsLegacyStore(argument);
|
|
}
|
|
|
|
function isUndefinedExpression(expression) {
|
|
const unwrapped = unwrapExpression(expression);
|
|
return (
|
|
(ts.isIdentifier(unwrapped) && unwrapped.text === "undefined") ||
|
|
ts.isVoidExpression(unwrapped)
|
|
);
|
|
}
|
|
|
|
function isKnownUndefinedExpression(expression) {
|
|
const unwrapped = unwrapExpression(expression);
|
|
return (
|
|
isUndefinedExpression(unwrapped) ||
|
|
(ts.isIdentifier(unwrapped) && resolveKnownUndefinedIdentifier(unwrapped.text))
|
|
);
|
|
}
|
|
|
|
function callExpressionName(expression) {
|
|
const callee = unwrapExpression(expression);
|
|
const pathParts = propertyAccessPath(callee);
|
|
return pathParts ? pathParts.join(".") : null;
|
|
}
|
|
|
|
function rootedPropertyAccessPath(expression) {
|
|
const access = rootedPropertyAccessCandidates(expression);
|
|
if (!access || access.propertyPaths.length !== 1 || access.propertyPaths[0].includes(null)) {
|
|
return null;
|
|
}
|
|
return { rootName: access.rootName, properties: access.propertyPaths[0] };
|
|
}
|
|
|
|
function rootedPropertyAccessCandidates(expression) {
|
|
const properties = [];
|
|
let current = unwrapExpression(expression);
|
|
while (true) {
|
|
if (ts.isPropertyAccessExpression(current)) {
|
|
properties.unshift([current.name.text]);
|
|
current = unwrapExpression(current.expression);
|
|
continue;
|
|
}
|
|
if (ts.isElementAccessExpression(current)) {
|
|
const propertyNames = elementAccessNames(current.argumentExpression);
|
|
properties.unshift(propertyNames.length > 0 ? propertyNames : [null]);
|
|
current = unwrapExpression(current.expression);
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
if (!ts.isIdentifier(current)) {
|
|
return null;
|
|
}
|
|
let propertyPaths = [[]];
|
|
let truncated = false;
|
|
for (const names of properties) {
|
|
const nextPaths = [];
|
|
outer: for (const propertyPath of propertyPaths) {
|
|
for (const name of names) {
|
|
if (nextPaths.length === 31) {
|
|
truncated = true;
|
|
break outer;
|
|
}
|
|
nextPaths.push([...propertyPath, name]);
|
|
}
|
|
}
|
|
propertyPaths = nextPaths;
|
|
}
|
|
if (truncated) {
|
|
propertyPaths.push(properties.map(() => null));
|
|
}
|
|
return { rootName: current.text, propertyPaths };
|
|
}
|
|
|
|
function lookupLegacyObjectPropertyCandidates({ rootName, propertyPaths }) {
|
|
let unresolved = false;
|
|
for (const propertyPath of propertyPaths) {
|
|
if (!propertyPath.includes(null)) {
|
|
let foundPrefix = false;
|
|
for (let length = propertyPath.length; length > 0; length--) {
|
|
const entry = lookupLegacyObjectPropertyEntry(
|
|
rootName,
|
|
propertyPath.slice(0, length).join("."),
|
|
);
|
|
if (entry.found) {
|
|
foundPrefix = true;
|
|
if (entry.value === true) {
|
|
return true;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
unresolved ||= !foundPrefix && resolveLegacyPathIdentifier(rootName);
|
|
continue;
|
|
}
|
|
const prefix = `${rootName}.`;
|
|
let rootIsLegacy = false;
|
|
for (let length = propertyPath.length; length > 0; length--) {
|
|
const candidate = propertyPath.slice(0, length);
|
|
const seen = new Set();
|
|
let matched = false;
|
|
for (let index = legacyObjectPropertyScopes.length - 1; index >= 0; index--) {
|
|
for (const [key, value] of legacyObjectPropertyScopes[index]) {
|
|
if (!key.startsWith(prefix) || seen.has(key)) {
|
|
continue;
|
|
}
|
|
const parts = key.slice(prefix.length).split(".");
|
|
if (
|
|
parts.length === candidate.length &&
|
|
candidate.every((part, partIndex) => part === null || part === parts[partIndex])
|
|
) {
|
|
seen.add(key);
|
|
matched = true;
|
|
if (value === true) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
if (legacyPathScopes[index].has(rootName)) {
|
|
rootIsLegacy ||= legacyPathScopes[index].get(rootName) === true;
|
|
break;
|
|
}
|
|
}
|
|
if (matched) {
|
|
break;
|
|
}
|
|
}
|
|
unresolved ||= rootIsLegacy;
|
|
}
|
|
return unresolved ? null : false;
|
|
}
|
|
|
|
function copyFactEntries(target, source, fromRoot, toRoot) {
|
|
for (const [key, value] of source) {
|
|
if (key === fromRoot || key.startsWith(`${fromRoot}.`)) {
|
|
target.set(`${toRoot}${key.slice(fromRoot.length)}`, value);
|
|
}
|
|
}
|
|
}
|
|
|
|
function expressionProducesLegacyPath(expression) {
|
|
const node = unwrapExpression(expression);
|
|
if (ts.isIdentifier(node)) {
|
|
return resolveLegacyPathIdentifier(node.text);
|
|
}
|
|
if (ts.isObjectLiteralExpression(node) || ts.isArrayLiteralExpression(node)) {
|
|
return false;
|
|
}
|
|
if (ts.isConditionalExpression(node)) {
|
|
return (
|
|
expressionProducesLegacyPath(node.whenTrue) || expressionProducesLegacyPath(node.whenFalse)
|
|
);
|
|
}
|
|
if (ts.isBinaryExpression(node)) {
|
|
const operator = node.operatorToken.kind;
|
|
if (
|
|
operator === ts.SyntaxKind.AmpersandAmpersandToken ||
|
|
operator === ts.SyntaxKind.CommaToken ||
|
|
(operator >= ts.SyntaxKind.FirstAssignment && operator <= ts.SyntaxKind.LastAssignment)
|
|
) {
|
|
return expressionProducesLegacyPath(node.right);
|
|
}
|
|
if (
|
|
operator !== ts.SyntaxKind.BarBarToken &&
|
|
operator !== ts.SyntaxKind.QuestionQuestionToken &&
|
|
operator !== ts.SyntaxKind.PlusToken
|
|
) {
|
|
return false;
|
|
}
|
|
return (
|
|
expressionTextContainsLegacyStore(node) ||
|
|
expressionProducesLegacyPath(node.left) ||
|
|
expressionProducesLegacyPath(node.right)
|
|
);
|
|
}
|
|
const access = rootedPropertyAccessCandidates(node);
|
|
if (access?.propertyPaths.some((propertyPath) => propertyPath.length > 0)) {
|
|
const value = lookupLegacyObjectPropertyCandidates(access);
|
|
return value ?? resolveLegacyPathIdentifier(access.rootName);
|
|
}
|
|
if (ts.isTemplateExpression(node)) {
|
|
return (
|
|
expressionTextContainsLegacyStore(node) ||
|
|
node.templateSpans.some((span) => expressionProducesLegacyPath(span.expression))
|
|
);
|
|
}
|
|
if (ts.isCallExpression(node)) {
|
|
const receiver =
|
|
ts.isPropertyAccessExpression(node.expression) ||
|
|
ts.isElementAccessExpression(node.expression)
|
|
? node.expression.expression
|
|
: null;
|
|
return (
|
|
expressionTextContainsLegacyStore(node) ||
|
|
(receiver ? expressionProducesLegacyPath(receiver) : false) ||
|
|
[...node.arguments].some(expressionProducesLegacyPath)
|
|
);
|
|
}
|
|
return expressionTextContainsLegacyStore(node);
|
|
}
|
|
|
|
function factExpressionAlternatives(expression) {
|
|
const unwrapped = unwrapExpression(expression);
|
|
if (ts.isSatisfiesExpression(unwrapped) || ts.isAwaitExpression(unwrapped)) {
|
|
return factExpressionAlternatives(unwrapped.expression);
|
|
}
|
|
if (ts.isConditionalExpression(unwrapped)) {
|
|
return [unwrapped.whenTrue, unwrapped.whenFalse].flatMap(factExpressionAlternatives);
|
|
}
|
|
if (ts.isBinaryExpression(unwrapped)) {
|
|
const operator = unwrapped.operatorToken.kind;
|
|
if (
|
|
operator === ts.SyntaxKind.CommaToken ||
|
|
(operator >= ts.SyntaxKind.FirstAssignment && operator <= ts.SyntaxKind.LastAssignment)
|
|
) {
|
|
return factExpressionAlternatives(unwrapped.right);
|
|
}
|
|
if (
|
|
[
|
|
ts.SyntaxKind.AmpersandAmpersandToken,
|
|
ts.SyntaxKind.BarBarToken,
|
|
ts.SyntaxKind.QuestionQuestionToken,
|
|
].includes(operator)
|
|
) {
|
|
return [unwrapped.left, unwrapped.right].flatMap(factExpressionAlternatives);
|
|
}
|
|
}
|
|
if (
|
|
ts.isCallExpression(unwrapped) &&
|
|
callExpressionName(unwrapped.expression) === "Promise.resolve" &&
|
|
unwrapped.arguments[0]
|
|
) {
|
|
return factExpressionAlternatives(unwrapped.arguments[0]);
|
|
}
|
|
if (ts.isNewExpression(unwrapped) && unwrapped.arguments?.length) {
|
|
return unwrapped.arguments.flatMap(factExpressionAlternatives);
|
|
}
|
|
return [unwrapped];
|
|
}
|
|
|
|
function mergeExpressionFacts(facts, expression) {
|
|
const mergeMap = (name, merge) => {
|
|
const result = new Map();
|
|
for (const fact of facts) {
|
|
for (const [key, value] of fact[name]) {
|
|
result.set(key, result.has(key) ? merge(result.get(key), value) : value);
|
|
}
|
|
}
|
|
return result;
|
|
};
|
|
const properties = new Map();
|
|
const propertyKeys = new Set(facts.flatMap((fact) => [...fact.properties.keys()]));
|
|
for (const key of propertyKeys) {
|
|
for (const fact of facts) {
|
|
const value = fact.properties.has(key)
|
|
? fact.properties.get(key)
|
|
: fact.knownObject
|
|
? explicitUndefinedLegacyObjectPropertyValue
|
|
: fact.legacyPath
|
|
? true
|
|
: undefined;
|
|
if (value !== undefined) {
|
|
properties.set(
|
|
key,
|
|
properties.has(key)
|
|
? mergeLegacyObjectPropertyValues(properties.get(key), value)
|
|
: value,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
return {
|
|
root: "<argument>",
|
|
expression,
|
|
fsModule: facts.some((fact) => fact.fsModule),
|
|
fsModules: mergeMap("fsModules", (left, right) => left === true || right === true),
|
|
fsSafeFactory: facts.find((fact) => fact.fsSafeFactory)?.fsSafeFactory ?? null,
|
|
fsWrite: facts.find((fact) => fact.fsWrite)?.fsWrite ?? null,
|
|
fsWrites: mergeMap("fsWrites", (left, right) => left ?? right),
|
|
jsonStore: facts.some((fact) => fact.jsonStore),
|
|
jsonStores: mergeMap("jsonStores", (left, right) => left === true || right === true),
|
|
knownObject: facts.every((fact) => fact.knownObject),
|
|
knownObjects: mergeMap("knownObjects", (left, right) => left === true && right === true),
|
|
knownUndefined: facts.every((fact) => fact.knownUndefined),
|
|
legacyPath: facts.some((fact) => fact.legacyPath),
|
|
literalTexts: [...new Set(facts.flatMap((fact) => fact.literalTexts))],
|
|
properties,
|
|
requireAlias: facts.some((fact) => fact.requireAlias),
|
|
store: facts.some((fact) => fact.store),
|
|
stores: mergeMap("stores", (left, right) => left === true || right === true),
|
|
wrapper: facts.map((fact) => fact.wrapper).reduce(mergeWrapperAssignmentValues, null),
|
|
wrappers: mergeMap("wrappers", mergeWrapperAssignmentValues),
|
|
possiblyUndefined: facts.some((fact) => fact.possiblyUndefined ?? fact.knownUndefined),
|
|
};
|
|
}
|
|
|
|
function factFromExpression(expression) {
|
|
const alternatives = factExpressionAlternatives(expression);
|
|
const bounded = alternatives.length > 32 ? alternatives.slice(0, 31) : alternatives;
|
|
const facts = bounded.map(atomicFactFromExpression);
|
|
if (bounded.length !== alternatives.length) {
|
|
const discarded = alternatives.slice(31);
|
|
facts.push(mergeExpressionFacts(discarded.map(atomicFactFromExpression), expression));
|
|
}
|
|
return facts.length === 1 && alternatives[0] === expression
|
|
? facts[0]
|
|
: mergeExpressionFacts(facts, expression);
|
|
}
|
|
|
|
function atomicFactFromExpression(expression) {
|
|
const root = "<argument>";
|
|
const properties = new Map();
|
|
const knownObjects = new Map();
|
|
const fsWrites = new Map();
|
|
const stores = new Map();
|
|
const jsonStores = new Map();
|
|
const fsModules = new Map();
|
|
const wrappers = new Map();
|
|
markLegacyObjectProperties(root, expression, properties, knownObjects);
|
|
registerFsWriteObjectAliases(root, expression, fsWrites);
|
|
registerFsSafeStoreObjectAliases(root, expression, stores, jsonStores);
|
|
registerFsModuleObjectProperties(root, expression, fsModules);
|
|
const unwrapped = unwrapExpression(expression);
|
|
const wrapperSource = callExpressionName(unwrapped);
|
|
registerWrapperObjectMethods(root, expression, wrappers);
|
|
if (wrapperSource) {
|
|
copyWrapperObjectMethods(root, wrapperSource, wrappers);
|
|
}
|
|
return {
|
|
root,
|
|
expression,
|
|
fsModule: isFsBindingExpression(expression),
|
|
fsModules,
|
|
fsSafeFactory: fsSafeStoreFactoryAliasName(expression),
|
|
fsWrite: legacyFsWriteName(expression),
|
|
fsWrites,
|
|
jsonStore: expressionContainsFsSafeJsonStoreLegacyPath(expression),
|
|
jsonStores,
|
|
knownObject: knownObjects.get(root) === true,
|
|
knownObjects,
|
|
knownUndefined: isKnownUndefinedExpression(expression),
|
|
legacyPath: expressionProducesLegacyPath(expression),
|
|
literalTexts: ts.isIdentifier(unwrapped)
|
|
? resolveLiteralTextIdentifier(unwrapped.text)
|
|
: literalTextsFromExpression(expression),
|
|
properties,
|
|
requireAlias: isRequireAliasExpression(expression),
|
|
store: isFsSafeStoreExpression(expression),
|
|
stores,
|
|
wrapper:
|
|
ts.isFunctionExpression(unwrapped) || ts.isArrowFunction(unwrapped)
|
|
? wrapperRecordForNode(unwrapped)
|
|
: cloneWrapperFunctionValue(resolveWrapperExpression(expression)),
|
|
wrappers,
|
|
};
|
|
}
|
|
|
|
function undefinedFact() {
|
|
return {
|
|
root: "<argument>",
|
|
expression: null,
|
|
fsModule: false,
|
|
fsModules: new Map(),
|
|
fsSafeFactory: null,
|
|
fsWrite: null,
|
|
fsWrites: new Map(),
|
|
jsonStore: false,
|
|
jsonStores: new Map(),
|
|
knownObject: false,
|
|
knownObjects: new Map(),
|
|
knownUndefined: true,
|
|
legacyPath: false,
|
|
literalTexts: [],
|
|
properties: new Map(),
|
|
requireAlias: false,
|
|
store: false,
|
|
stores: new Map(),
|
|
wrapper: null,
|
|
wrappers: new Map(),
|
|
};
|
|
}
|
|
|
|
function propertyFact(fact, propertyName) {
|
|
const root = `${fact.root}.${propertyName}`;
|
|
const value = fact.properties.get(root);
|
|
const missing =
|
|
value === undefined &&
|
|
!fact.wrappers.has(root) &&
|
|
!fact.fsWrites.has(root) &&
|
|
!fact.fsModules.has(root) &&
|
|
!fact.stores.has(root) &&
|
|
!fact.jsonStores.has(root) &&
|
|
!fact.knownObjects.has(root);
|
|
if (missing && fact.knownObject) {
|
|
return undefinedFact();
|
|
}
|
|
const result = undefinedFact();
|
|
result.root = root;
|
|
result.knownUndefined = value === explicitUndefinedLegacyObjectPropertyValue;
|
|
if (missing) {
|
|
result.knownUndefined = false;
|
|
}
|
|
result.legacyPath = value === true || (missing && !fact.knownObject && fact.legacyPath);
|
|
result.knownObject = fact.knownObjects.get(root) === true;
|
|
result.fsModule =
|
|
fact.fsModules.get(root) === true || (fact.fsModule && propertyName === "promises");
|
|
result.fsWrite =
|
|
fact.fsWrites.get(root) ??
|
|
(fact.fsModule && legacyWriteCallees.has(propertyName) ? propertyName : null);
|
|
result.store = fact.stores.get(root) === true;
|
|
result.jsonStore = fact.jsonStores.get(root) === true;
|
|
result.wrapper = cloneWrapperFunctionValue(fact.wrappers.get(root));
|
|
for (const name of [
|
|
"fsModules",
|
|
"fsWrites",
|
|
"stores",
|
|
"jsonStores",
|
|
"knownObjects",
|
|
"properties",
|
|
"wrappers",
|
|
]) {
|
|
result[name] = fact[name];
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function bindObjectPatternFact(pattern, sourceFact) {
|
|
for (const element of pattern.elements) {
|
|
const propertyName = element.propertyName
|
|
? propertyNameText(element.propertyName)
|
|
: ts.isIdentifier(element.name)
|
|
? element.name.text
|
|
: null;
|
|
if (!propertyName) {
|
|
continue;
|
|
}
|
|
let nextFact = propertyFact(sourceFact, propertyName);
|
|
if (nextFact.knownUndefined && element.initializer) {
|
|
visit(element.initializer);
|
|
nextFact = factFromExpression(element.initializer);
|
|
}
|
|
if (ts.isIdentifier(element.name)) {
|
|
bindIdentifierFact(element.name.text, nextFact);
|
|
} else if (ts.isObjectBindingPattern(element.name)) {
|
|
bindObjectPatternFact(element.name, nextFact);
|
|
}
|
|
}
|
|
}
|
|
|
|
function bindObjectPatternWrappers(pattern, sourceFact) {
|
|
for (const element of pattern.elements) {
|
|
const propertyName = element.propertyName
|
|
? propertyNameText(element.propertyName)
|
|
: ts.isIdentifier(element.name)
|
|
? element.name.text
|
|
: null;
|
|
if (!propertyName) {
|
|
continue;
|
|
}
|
|
let nextFact = propertyFact(sourceFact, propertyName);
|
|
if (nextFact.knownUndefined && element.initializer) {
|
|
nextFact = factFromExpression(element.initializer);
|
|
}
|
|
if (ts.isIdentifier(element.name)) {
|
|
lastScope(wrapperFunctionScopes).set(
|
|
element.name.text,
|
|
cloneWrapperFunctionValue(nextFact.wrapper),
|
|
);
|
|
} else if (ts.isObjectBindingPattern(element.name)) {
|
|
bindObjectPatternWrappers(element.name, nextFact);
|
|
}
|
|
}
|
|
}
|
|
|
|
function bindIdentifierFact(name, fact, type = null) {
|
|
shadowObjectPropertyScopes(fsModulePropertyScopes, name, false);
|
|
shadowObjectPropertyScopes(fsWriteAliasScopes, name, null);
|
|
shadowObjectPropertyScopes(fsSafeStoreScopes, name, false);
|
|
shadowObjectPropertyScopes(fsSafeJsonStoreScopes, name, false);
|
|
shadowObjectPropertyScopes(wrapperFunctionScopes, name, null);
|
|
if (createRequireBindings.has(name)) {
|
|
lastScope(createRequireShadowScopes).add(name);
|
|
}
|
|
lastScope(fsModuleBindingScopes).set(name, fact.fsModule);
|
|
lastScope(fsModulePropertyScopes).set(name, false);
|
|
copyFactEntries(lastScope(fsModulePropertyScopes), fact.fsModules, fact.root, name);
|
|
registerFsModuleTypeProperties(ts.factory.createIdentifier(name), type);
|
|
lastScope(fsWriteAliasScopes).set(name, fact.fsWrite);
|
|
copyFactEntries(lastScope(fsWriteAliasScopes), fact.fsWrites, fact.root, name);
|
|
lastScope(fsSafeStoreFactoryAliasScopes).set(name, fact.fsSafeFactory);
|
|
lastScope(fsSafeStoreScopes).set(name, fact.store);
|
|
lastScope(fsSafeJsonStoreScopes).set(name, fact.jsonStore);
|
|
copyFactEntries(lastScope(fsSafeStoreScopes), fact.stores, fact.root, name);
|
|
copyFactEntries(lastScope(fsSafeJsonStoreScopes), fact.jsonStores, fact.root, name);
|
|
lastScope(requireAliasScopes).set(name, fact.requireAlias);
|
|
lastScope(legacyPathScopes).set(name, fact.legacyPath);
|
|
lastScope(literalTextScopes).set(name, fact.literalTexts);
|
|
lastScope(staticExpressionScopes).set(name, fact.expression);
|
|
lastScope(knownUndefinedScopes).set(name, fact.knownUndefined);
|
|
lastScope(legacyKnownObjectLiteralScopes).set(name, fact.knownObject);
|
|
copyFactEntries(lastScope(legacyKnownObjectLiteralScopes), fact.knownObjects, fact.root, name);
|
|
copyFactEntries(lastScope(legacyObjectPropertyScopes), fact.properties, fact.root, name);
|
|
lastScope(wrapperFunctionScopes).set(name, cloneWrapperFunctionValue(fact.wrapper));
|
|
copyFactEntries(lastScope(wrapperFunctionScopes), fact.wrappers, fact.root, name);
|
|
}
|
|
|
|
function bindParameter(parameter, fact) {
|
|
if (ts.isIdentifier(parameter.name)) {
|
|
bindIdentifierFact(parameter.name.text, fact, parameter.type);
|
|
return;
|
|
}
|
|
if (ts.isObjectBindingPattern(parameter.name)) {
|
|
bindObjectPatternFact(parameter.name, fact);
|
|
return;
|
|
}
|
|
for (const name of bindingPatternNames(parameter.name)) {
|
|
bindIdentifierFact(name, undefinedFact());
|
|
}
|
|
if (ts.isArrayBindingPattern(parameter.name)) {
|
|
const array = fact.expression ? unwrapExpression(fact.expression) : null;
|
|
if (array && ts.isArrayLiteralExpression(array)) {
|
|
parameter.name.elements.forEach((element, elementIndex) => {
|
|
if (!ts.isBindingElement(element) || !ts.isIdentifier(element.name)) {
|
|
return;
|
|
}
|
|
const value = array.elements[elementIndex];
|
|
if (value && !ts.isOmittedExpression(value) && !ts.isSpreadElement(value)) {
|
|
bindIdentifierFact(element.name.text, factFromExpression(value));
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function withWrapperEnvironment(record, run) {
|
|
const savedScopes = lexicalScopeStacks.map(([scopes]) => [...scopes]);
|
|
for (let index = 0; index < lexicalScopeStacks.length; index++) {
|
|
const scopes = lexicalScopeStacks[index][0];
|
|
scopes.splice(0, scopes.length, ...record.environment[index]);
|
|
}
|
|
branchEffectScopes.push(null);
|
|
try {
|
|
return run();
|
|
} finally {
|
|
for (let index = 0; index < lexicalScopeStacks.length; index++) {
|
|
const scopes = lexicalScopeStacks[index][0];
|
|
scopes.splice(0, scopes.length, ...savedScopes[index]);
|
|
}
|
|
branchEffectScopes.pop();
|
|
}
|
|
}
|
|
|
|
function spreadArgumentFacts(expression, seen = new Set()) {
|
|
const unwrapped = unwrapExpression(expression);
|
|
if (ts.isSatisfiesExpression(unwrapped) || ts.isAwaitExpression(unwrapped)) {
|
|
return spreadArgumentFacts(unwrapped.expression, seen);
|
|
}
|
|
if (ts.isIdentifier(unwrapped)) {
|
|
const resolved = !seen.has(unwrapped.text) && resolveStaticExpression(unwrapped.text);
|
|
return resolved ? spreadArgumentFacts(resolved, new Set([...seen, unwrapped.text])) : null;
|
|
}
|
|
if (ts.isConditionalExpression(unwrapped)) {
|
|
const left = spreadArgumentFacts(unwrapped.whenTrue, seen);
|
|
const right = spreadArgumentFacts(unwrapped.whenFalse, seen);
|
|
return left && right
|
|
? Array.from({ length: Math.max(left.length, right.length) }, (_, index) =>
|
|
mergeExpressionFacts(
|
|
[left[index] ?? undefinedFact(), right[index] ?? undefinedFact()],
|
|
expression,
|
|
),
|
|
)
|
|
: null;
|
|
}
|
|
if (!ts.isArrayLiteralExpression(unwrapped)) {
|
|
return null;
|
|
}
|
|
const facts = [];
|
|
for (const element of unwrapped.elements) {
|
|
if (ts.isOmittedExpression(element)) {
|
|
facts.push(undefinedFact());
|
|
} else if (ts.isSpreadElement(element)) {
|
|
const nested = spreadArgumentFacts(element.expression, seen);
|
|
if (!nested) {
|
|
return null;
|
|
}
|
|
facts.push(...nested);
|
|
} else {
|
|
facts.push(factFromExpression(element));
|
|
}
|
|
}
|
|
return facts;
|
|
}
|
|
|
|
function wrapperArgumentFacts(call, parameterCount) {
|
|
const facts = [];
|
|
let ambiguous = false;
|
|
let ambiguousStart = 0;
|
|
for (const argument of call.arguments) {
|
|
const expanded = ts.isSpreadElement(argument)
|
|
? spreadArgumentFacts(argument.expression)
|
|
: [factFromExpression(argument)];
|
|
if (!expanded) {
|
|
ambiguousStart = ambiguous ? ambiguousStart : facts.length;
|
|
ambiguous = true;
|
|
}
|
|
const next = expanded ?? [factFromExpression(argument.expression)];
|
|
if (!ambiguous) {
|
|
facts.push(...next);
|
|
continue;
|
|
}
|
|
for (let index = ambiguousStart; index < parameterCount; index++) {
|
|
facts[index] = mergeExpressionFacts([facts[index] ?? undefinedFact(), ...next], argument);
|
|
}
|
|
}
|
|
return facts;
|
|
}
|
|
|
|
function executeWrapper(record, call) {
|
|
if (activeWrapperNodes.has(record.node)) {
|
|
return;
|
|
}
|
|
const argumentFacts = wrapperArgumentFacts(call, record.node.parameters.length);
|
|
activeWrapperNodes.add(record.node);
|
|
wrapperCallSites.push(call.expression);
|
|
try {
|
|
withWrapperEnvironment(record, () =>
|
|
withLexicalScope(false, () => {
|
|
wrapperExecutionScopeIndexes.push(wrapperFunctionScopes.length - 1);
|
|
try {
|
|
if (record.node.name && ts.isIdentifier(record.node.name)) {
|
|
bindIdentifierFact(record.node.name.text, {
|
|
...undefinedFact(),
|
|
knownUndefined: false,
|
|
wrapper: record,
|
|
});
|
|
}
|
|
record.node.parameters.forEach((parameter, index) => {
|
|
const supplied = argumentFacts[index] ?? undefinedFact();
|
|
const possiblyUndefined = supplied.possiblyUndefined ?? supplied.knownUndefined;
|
|
if (possiblyUndefined && parameter.initializer) {
|
|
visit(parameter.initializer);
|
|
}
|
|
const fact =
|
|
possiblyUndefined && parameter.initializer
|
|
? supplied.knownUndefined
|
|
? factFromExpression(parameter.initializer)
|
|
: mergeExpressionFacts(
|
|
[supplied, factFromExpression(parameter.initializer)],
|
|
supplied.expression,
|
|
)
|
|
: supplied;
|
|
bindParameter(parameter, fact);
|
|
});
|
|
if (record.node.body) {
|
|
visit(record.node.body);
|
|
}
|
|
} finally {
|
|
wrapperExecutionScopeIndexes.pop();
|
|
}
|
|
}),
|
|
);
|
|
} finally {
|
|
wrapperCallSites.pop();
|
|
activeWrapperNodes.delete(record.node);
|
|
}
|
|
}
|
|
|
|
function promoteVarBinding(name) {
|
|
const targetIndex = lastScope(wrapperExecutionScopeIndexes);
|
|
if (targetIndex === undefined || targetIndex === wrapperFunctionScopes.length - 1) {
|
|
return;
|
|
}
|
|
for (const [scopes] of lexicalScopeStacks) {
|
|
const source = lastScope(scopes);
|
|
const target = scopes[targetIndex];
|
|
if (!(source instanceof Map) || !(target instanceof Map)) {
|
|
continue;
|
|
}
|
|
for (const [key, value] of source) {
|
|
if (key !== name && !key.startsWith(`${name}.`)) {
|
|
continue;
|
|
}
|
|
if (scopes === wrapperFunctionScopes) {
|
|
target.set(key, mergeWrapperAssignmentValues(target.get(key), value));
|
|
} else if (scopes === fsWriteAliasScopes || scopes === fsSafeStoreFactoryAliasScopes) {
|
|
target.set(key, target.get(key) ?? value);
|
|
} else if (
|
|
scopes === fsModuleBindingScopes ||
|
|
scopes === fsSafeStoreScopes ||
|
|
scopes === fsSafeJsonStoreScopes ||
|
|
scopes === requireAliasScopes ||
|
|
scopes === legacyPathScopes
|
|
) {
|
|
target.set(key, target.get(key) === true || value === true);
|
|
} else {
|
|
target.set(key, value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function visitInConditionalExecution(node, branchEffects = null) {
|
|
withLexicalScope(true, () => {
|
|
branchEffectScopes.push(branchEffects);
|
|
try {
|
|
visit(node);
|
|
} finally {
|
|
branchEffectScopes.pop();
|
|
}
|
|
});
|
|
}
|
|
|
|
function visit(node) {
|
|
if (isTypeSyntaxNode(node)) {
|
|
return;
|
|
}
|
|
if (node === sourceFile) {
|
|
registerHoistedWrapperFunctions(sourceFile.statements);
|
|
}
|
|
|
|
if (ts.isIfStatement(node)) {
|
|
visit(node.expression);
|
|
const thenEffects = node.elseStatement ? createBranchEffects() : null;
|
|
const elseEffects = node.elseStatement ? createBranchEffects() : null;
|
|
visitInConditionalExecution(node.thenStatement, thenEffects);
|
|
if (node.elseStatement) {
|
|
visitInConditionalExecution(node.elseStatement, elseEffects);
|
|
mergeExhaustiveBranchEffects(thenEffects, elseEffects);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (ts.isWhileStatement(node)) {
|
|
visit(node.expression);
|
|
visitInConditionalExecution(node.statement);
|
|
return;
|
|
}
|
|
|
|
if (ts.isDoStatement(node)) {
|
|
visitInConditionalExecution(node.statement);
|
|
visit(node.expression);
|
|
return;
|
|
}
|
|
|
|
if (ts.isForStatement(node)) {
|
|
withLexicalScope(false, () => {
|
|
if (node.initializer) {
|
|
visit(node.initializer);
|
|
}
|
|
if (node.condition) {
|
|
visit(node.condition);
|
|
}
|
|
if (node.incrementor) {
|
|
visitInConditionalExecution(node.incrementor);
|
|
}
|
|
visitInConditionalExecution(node.statement);
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (ts.isForInStatement(node) || ts.isForOfStatement(node)) {
|
|
visit(node.expression);
|
|
withLexicalScope(true, () => {
|
|
visit(node.initializer);
|
|
if (ts.isForOfStatement(node)) {
|
|
markArrayBindingPatternFromForOf(node.initializer, node.expression);
|
|
}
|
|
visit(node.statement);
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (ts.isFunctionLike(node)) {
|
|
if (ts.isFunctionDeclaration(node) && node.name) {
|
|
registerWrapperFunction(node.name.text, node);
|
|
}
|
|
if (wrapperCallSites.length === 0) {
|
|
visitFunctionLike(node);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (ts.isCallExpression(node)) {
|
|
const callback = dynamicFsImportThenCallback(node);
|
|
if (callback) {
|
|
visitFunctionLike(callback, new Set([0]));
|
|
for (const argument of node.arguments.slice(1)) {
|
|
visit(argument);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (
|
|
ts.isBlock(node) ||
|
|
ts.isModuleBlock(node) ||
|
|
ts.isCaseBlock(node) ||
|
|
ts.isCatchClause(node)
|
|
) {
|
|
visitWithChildScope(node);
|
|
return;
|
|
}
|
|
|
|
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
|
|
const fact = node.initializer ? factFromExpression(node.initializer) : undefinedFact();
|
|
if (node.initializer) {
|
|
fact.legacyPath = expressionContainsLegacyStore(node.initializer);
|
|
} else {
|
|
fact.knownUndefined = !isAmbientVariableDeclaration(node);
|
|
}
|
|
bindIdentifierFact(node.name.text, fact, node.type);
|
|
}
|
|
if (ts.isVariableDeclaration(node) && !ts.isIdentifier(node.name)) {
|
|
const isFsAliasBinding =
|
|
node.initializer &&
|
|
ts.isObjectBindingPattern(node.name) &&
|
|
isFsBindingExpression(node.initializer);
|
|
collectFsModuleBindingsFromBinding(node);
|
|
collectFsWriteAliasesFromBinding(node);
|
|
markFsSafeStoreShadows(node.name);
|
|
if (!isFsAliasBinding) {
|
|
markFsWriteAliasShadows(node.name);
|
|
markFsModuleBindingShadows(node.name);
|
|
markFsModulePropertyShadows(node.name);
|
|
markCreateRequireShadows(node.name);
|
|
}
|
|
for (const name of bindingPatternNames(node.name)) {
|
|
lastScope(fsSafeStoreFactoryAliasScopes).set(name, null);
|
|
lastScope(fsSafeStoreScopes).set(name, false);
|
|
lastScope(fsSafeJsonStoreScopes).set(name, false);
|
|
lastScope(requireAliasScopes).set(name, false);
|
|
lastScope(legacyPathScopes).set(name, false);
|
|
lastScope(legacyKnownObjectLiteralScopes).set(name, false);
|
|
lastScope(knownUndefinedScopes).set(name, false);
|
|
lastScope(literalTextScopes).set(name, null);
|
|
lastScope(wrapperFunctionScopes).set(name, null);
|
|
}
|
|
if (ts.isObjectBindingPattern(node.name) && node.initializer) {
|
|
bindObjectPatternFact(node.name, factFromExpression(node.initializer));
|
|
}
|
|
if (
|
|
ts.isObjectBindingPattern(node.name) &&
|
|
node.initializer &&
|
|
ts.isIdentifier(node.initializer)
|
|
) {
|
|
markLegacyPathsFromObjectBinding(node.name, node.initializer.text);
|
|
markFsSafeStoresFromObjectBinding(node.name, node.initializer.text);
|
|
markFsSafeFactoryAliasesFromObjectBinding(node.name, node.initializer.text);
|
|
} else if (
|
|
ts.isObjectBindingPattern(node.name) &&
|
|
node.initializer &&
|
|
rootedPropertyAccessPath(node.initializer)?.properties.length > 0
|
|
) {
|
|
const propertyAccess = rootedPropertyAccessPath(node.initializer);
|
|
const sourceName = objectPropertyKey(
|
|
propertyAccess.rootName,
|
|
propertyAccess.properties.join("."),
|
|
);
|
|
markLegacyPathsFromObjectBinding(node.name, sourceName);
|
|
markFsSafeStoresFromObjectBinding(node.name, sourceName);
|
|
markFsSafeFactoryAliasesFromObjectBinding(node.name, sourceName);
|
|
} else if (
|
|
ts.isObjectBindingPattern(node.name) &&
|
|
node.initializer &&
|
|
ts.isObjectLiteralExpression(unwrapExpression(node.initializer))
|
|
) {
|
|
markLegacyPathsFromInlineObjectBinding(node.name, node.initializer);
|
|
}
|
|
if (ts.isObjectBindingPattern(node.name) && node.initializer) {
|
|
bindObjectPatternWrappers(node.name, factFromExpression(node.initializer));
|
|
}
|
|
}
|
|
if (ts.isVariableDeclaration(node) && isVarVariableDeclaration(node)) {
|
|
for (const name of bindingPatternNames(node.name)) {
|
|
promoteVarBinding(name);
|
|
}
|
|
}
|
|
if (
|
|
ts.isBinaryExpression(node) &&
|
|
node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
ts.isIdentifier(node.left)
|
|
) {
|
|
const { index, pathScope, propertyScope, wrapperScope } = legacyIdentifierWriteScopes(
|
|
node.left.text,
|
|
);
|
|
const nextPathValue = expressionContainsLegacyStore(node.right);
|
|
const nextLiteralTexts = literalTextsFromExpression(node.right);
|
|
const nextKnownUndefined = isKnownUndefinedExpression(node.right);
|
|
const nextFsModuleValue = isFsBindingExpression(node.right);
|
|
const nextFsWriteAlias = legacyFsWriteName(node.right);
|
|
const nextFsSafeFactoryAlias = fsSafeStoreFactoryAliasName(node.right);
|
|
const nextFsSafeStoreValue = isFsSafeStoreExpression(node.right);
|
|
const nextFsSafeJsonStoreValue = expressionContainsFsSafeJsonStoreLegacyPath(node.right);
|
|
const nextRequireAlias = isRequireAliasExpression(node.right);
|
|
const conditionalWrite =
|
|
lastScope(conditionalExecutionScopes) && !conditionalExecutionScopes[index];
|
|
pathScope.set(
|
|
node.left.text,
|
|
conditionalWrite ? pathScope.get(node.left.text) === true || nextPathValue : nextPathValue,
|
|
);
|
|
const literalScope = scopeForWrite(literalTextScopes, node.left.text);
|
|
literalScope.set(
|
|
node.left.text,
|
|
conditionalWrite
|
|
? mergeConditionalLiteralTexts(literalScope.get(node.left.text), nextLiteralTexts)
|
|
: nextLiteralTexts,
|
|
);
|
|
scopeForWrite(staticExpressionScopes, node.left.text).set(
|
|
node.left.text,
|
|
conditionalWrite ? null : node.right,
|
|
);
|
|
const knownUndefinedScope = scopeForWrite(knownUndefinedScopes, node.left.text);
|
|
knownUndefinedScope.set(
|
|
node.left.text,
|
|
conditionalWrite
|
|
? knownUndefinedScope.get(node.left.text) === true || nextKnownUndefined
|
|
: nextKnownUndefined,
|
|
);
|
|
if (conditionalWrite) {
|
|
const nextPropertyScope = legacyObjectPropertyRewriteValues(
|
|
node.left.text,
|
|
node.right,
|
|
propertyScope,
|
|
);
|
|
recordBranchIdentifierAssignment(
|
|
index,
|
|
node.left.text,
|
|
nextPathValue,
|
|
node.right,
|
|
nextLiteralTexts,
|
|
nextPropertyScope,
|
|
);
|
|
for (const [key, value] of nextPropertyScope) {
|
|
const mergedValue = mergeConditionalLegacyObjectPropertyValue(
|
|
propertyScope.get(key),
|
|
value,
|
|
);
|
|
if (mergedValue !== null) {
|
|
propertyScope.set(key, mergedValue);
|
|
}
|
|
}
|
|
legacyKnownObjectLiteralScopes[index].set(
|
|
node.left.text,
|
|
legacyKnownObjectLiteralScopes[index].get(node.left.text) === true &&
|
|
isKnownLegacyObjectLiteralExpression(node.right),
|
|
);
|
|
lastScope(legacyPathScopes).set(node.left.text, nextPathValue);
|
|
markKnownLegacyObjectLiteral(node.left.text, node.right);
|
|
clearLegacyObjectProperties(lastScope(legacyObjectPropertyScopes), node.left.text);
|
|
markLegacyObjectProperties(
|
|
node.left.text,
|
|
node.right,
|
|
lastScope(legacyObjectPropertyScopes),
|
|
);
|
|
} else {
|
|
scopeForWrite(fsModuleBindingScopes, node.left.text).set(node.left.text, nextFsModuleValue);
|
|
scopeForWrite(fsWriteAliasScopes, node.left.text).set(node.left.text, nextFsWriteAlias);
|
|
scopeForWrite(fsSafeStoreFactoryAliasScopes, node.left.text).set(
|
|
node.left.text,
|
|
nextFsSafeFactoryAlias,
|
|
);
|
|
scopeForWrite(fsSafeStoreScopes, node.left.text).set(node.left.text, nextFsSafeStoreValue);
|
|
scopeForWrite(fsSafeJsonStoreScopes, node.left.text).set(
|
|
node.left.text,
|
|
nextFsSafeJsonStoreValue,
|
|
);
|
|
const requireAliasTarget = requireAliasWriteTarget(node.left.text);
|
|
requireAliasTarget.scope.set(node.left.text, nextRequireAlias);
|
|
markFsModulePropertyShadows(node.left);
|
|
clearLegacyObjectProperties(propertyScope, node.left.text);
|
|
markKnownLegacyObjectLiteral(
|
|
node.left.text,
|
|
node.right,
|
|
legacyKnownObjectLiteralScopes[index],
|
|
);
|
|
markLegacyObjectProperties(
|
|
node.left.text,
|
|
node.right,
|
|
propertyScope,
|
|
legacyKnownObjectLiteralScopes[index],
|
|
);
|
|
clearFsWriteObjectAliases(fsWriteAliasScopes[index], node.left.text);
|
|
registerFsWriteObjectAliases(node.left.text, node.right, fsWriteAliasScopes[index]);
|
|
clearFsSafeStoreObjectAliases(
|
|
fsSafeStoreScopes[index],
|
|
fsSafeJsonStoreScopes[index],
|
|
node.left.text,
|
|
);
|
|
registerFsSafeStoreObjectAliases(
|
|
node.left.text,
|
|
node.right,
|
|
fsSafeStoreScopes[index],
|
|
fsSafeJsonStoreScopes[index],
|
|
);
|
|
registerFsModuleObjectProperties(node.left.text, node.right, fsModulePropertyScopes[index]);
|
|
clearWrapperObjectMethods(wrapperScope, node.left.text);
|
|
registerWrapperObjectMethods(node.left.text, node.right, wrapperScope);
|
|
}
|
|
if (conditionalWrite) {
|
|
const fsModuleScope = fsModuleBindingScopes[index];
|
|
const fsWriteScope = fsWriteAliasScopes[index];
|
|
const fsSafeFactoryAliasScope = fsSafeStoreFactoryAliasScopes[index];
|
|
const fsSafeStoreScope = fsSafeStoreScopes[index];
|
|
const fsSafeJsonStoreScope = fsSafeJsonStoreScopes[index];
|
|
fsModuleScope.set(
|
|
node.left.text,
|
|
fsModuleScope.get(node.left.text) === true || nextFsModuleValue,
|
|
);
|
|
fsWriteScope.set(node.left.text, fsWriteScope.get(node.left.text) ?? nextFsWriteAlias);
|
|
fsSafeFactoryAliasScope.set(
|
|
node.left.text,
|
|
fsSafeFactoryAliasScope.get(node.left.text) ?? nextFsSafeFactoryAlias,
|
|
);
|
|
fsSafeStoreScope.set(
|
|
node.left.text,
|
|
fsSafeStoreScope.get(node.left.text) === true || nextFsSafeStoreValue,
|
|
);
|
|
fsSafeJsonStoreScope.set(
|
|
node.left.text,
|
|
fsSafeJsonStoreScope.get(node.left.text) === true || nextFsSafeJsonStoreValue,
|
|
);
|
|
requireAliasScopes[index].set(
|
|
node.left.text,
|
|
requireAliasScopes[index].get(node.left.text) === true || nextRequireAlias,
|
|
);
|
|
lastScope(fsModuleBindingScopes).set(node.left.text, nextFsModuleValue);
|
|
lastScope(fsWriteAliasScopes).set(node.left.text, nextFsWriteAlias);
|
|
lastScope(fsSafeStoreFactoryAliasScopes).set(node.left.text, nextFsSafeFactoryAlias);
|
|
lastScope(fsSafeStoreScopes).set(node.left.text, nextFsSafeStoreValue);
|
|
lastScope(fsSafeJsonStoreScopes).set(node.left.text, nextFsSafeJsonStoreValue);
|
|
lastScope(requireAliasScopes).set(node.left.text, nextRequireAlias);
|
|
recordBranchFsIdentifierAssignment(
|
|
index,
|
|
node.left.text,
|
|
nextFsModuleValue,
|
|
nextFsWriteAlias,
|
|
nextFsSafeFactoryAlias,
|
|
nextFsSafeStoreValue,
|
|
nextFsSafeJsonStoreValue,
|
|
nextRequireAlias,
|
|
);
|
|
registerFsWriteObjectAliases(node.left.text, node.right, fsWriteAliasScopes[index], true);
|
|
registerFsSafeStoreObjectAliases(
|
|
node.left.text,
|
|
node.right,
|
|
fsSafeStoreScopes[index],
|
|
fsSafeJsonStoreScopes[index],
|
|
true,
|
|
);
|
|
registerFsModuleObjectProperties(
|
|
node.left.text,
|
|
node.right,
|
|
fsModulePropertyScopes[index],
|
|
true,
|
|
);
|
|
shadowVisibleFsWriteObjectAliases(node.left.text);
|
|
registerFsWriteObjectAliases(node.left.text, node.right);
|
|
registerFsSafeStoreObjectAliases(node.left.text, node.right);
|
|
registerFsModuleObjectProperties(node.left.text, node.right);
|
|
registerWrapperObjectMethods(node.left.text, node.right, wrapperScope, true);
|
|
recordBranchWrapperObjectRewrite(index, node.left.text, node.right);
|
|
shadowVisibleWrapperObjectMethods(node.left.text);
|
|
registerWrapperObjectMethods(node.left.text, node.right);
|
|
}
|
|
const assignedWrapper =
|
|
ts.isFunctionExpression(node.right) || ts.isArrowFunction(node.right)
|
|
? wrapperRecordForNode(node.right)
|
|
: cloneWrapperFunctionValue(resolveWrapperExpression(node.right));
|
|
if (conditionalWrite) {
|
|
recordBranchWrapperAssignment(index, node.left.text, assignedWrapper);
|
|
}
|
|
setWrapperFunctionValue(wrapperScope, node.left.text, assignedWrapper, conditionalWrite);
|
|
const wrapperObjectSource = callExpressionName(node.right);
|
|
if (wrapperObjectSource) {
|
|
copyWrapperObjectMethods(
|
|
node.left.text,
|
|
wrapperObjectSource,
|
|
wrapperScope,
|
|
conditionalWrite,
|
|
);
|
|
}
|
|
}
|
|
if (
|
|
ts.isBinaryExpression(node) &&
|
|
node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
rootedPropertyAccessPath(node.left)?.properties.length > 0
|
|
) {
|
|
const propertyAccess = rootedPropertyAccessPath(node.left);
|
|
const propertyName = propertyAccess.properties.join(".");
|
|
const target = legacyObjectPropertyWriteTarget(propertyAccess.rootName, propertyName);
|
|
const key = objectPropertyKey(propertyAccess.rootName, propertyName);
|
|
const nextValue = legacyObjectPropertyValueFromExpression(node.right);
|
|
const nextKnownObjectLiteral = isKnownLegacyObjectLiteralExpression(node.right);
|
|
const rewriteValues = legacyObjectPropertyRewriteValues(key, node.right, target.scope);
|
|
const conditionalPropertyWrite =
|
|
lastScope(conditionalExecutionScopes) && !conditionalExecutionScopes[target.index];
|
|
if (conditionalPropertyWrite) {
|
|
const previousKnownObjectLiteral = lookupKnownLegacyObjectLiteral(key);
|
|
clearKnownLegacyObjectLiterals(legacyKnownObjectLiteralScopes[target.index], key);
|
|
legacyKnownObjectLiteralScopes[target.index].set(
|
|
key,
|
|
previousKnownObjectLiteral && nextKnownObjectLiteral,
|
|
);
|
|
const previousValue = target.scope.has(key)
|
|
? target.scope.get(key)
|
|
: lookupKnownLegacyObjectLiteral(propertyAccess.rootName)
|
|
? explicitUndefinedLegacyObjectPropertyValue
|
|
: undefined;
|
|
const mergedValue = mergeConditionalLegacyObjectPropertyValue(previousValue, nextValue);
|
|
if (mergedValue !== null) {
|
|
target.scope.set(key, mergedValue);
|
|
}
|
|
} else {
|
|
target.scope.set(key, nextValue);
|
|
clearKnownLegacyObjectLiterals(legacyKnownObjectLiteralScopes[target.index], key);
|
|
legacyKnownObjectLiteralScopes[target.index].set(key, nextKnownObjectLiteral);
|
|
}
|
|
if (!conditionalPropertyWrite) {
|
|
clearLegacyObjectProperties(target.scope, key);
|
|
for (const [propertyKey, value] of rewriteValues) {
|
|
target.scope.set(propertyKey, value);
|
|
}
|
|
}
|
|
if (conditionalPropertyWrite) {
|
|
for (const [propertyKey, value] of rewriteValues) {
|
|
const mergedValue = mergeConditionalLegacyObjectPropertyValue(
|
|
target.scope.get(propertyKey),
|
|
value,
|
|
);
|
|
if (mergedValue !== null) {
|
|
target.scope.set(propertyKey, mergedValue);
|
|
recordBranchPropertyAssignment(
|
|
target.index,
|
|
propertyAccess.rootName,
|
|
propertyKey.slice(`${propertyAccess.rootName}.`.length),
|
|
value,
|
|
);
|
|
}
|
|
}
|
|
lastScope(legacyObjectPropertyScopes).set(key, nextValue);
|
|
clearKnownLegacyObjectLiterals(lastScope(legacyKnownObjectLiteralScopes), key);
|
|
lastScope(legacyKnownObjectLiteralScopes).set(key, nextKnownObjectLiteral);
|
|
clearLegacyObjectProperties(lastScope(legacyObjectPropertyScopes), key);
|
|
for (const [propertyKey, value] of rewriteValues) {
|
|
lastScope(legacyObjectPropertyScopes).set(propertyKey, value);
|
|
}
|
|
recordBranchPropertyAssignment(
|
|
target.index,
|
|
propertyAccess.rootName,
|
|
propertyName,
|
|
nextValue,
|
|
nextKnownObjectLiteral,
|
|
);
|
|
}
|
|
const wrapperTarget = legacyIdentifierWriteScopes(propertyAccess.rootName);
|
|
const conditionalWrapperWrite =
|
|
lastScope(conditionalExecutionScopes) && !conditionalExecutionScopes[wrapperTarget.index];
|
|
setFsWriteObjectAlias(
|
|
fsWriteAliasScopes[wrapperTarget.index],
|
|
key,
|
|
legacyFsWriteName(node.right),
|
|
conditionalWrapperWrite,
|
|
);
|
|
setFsModuleObjectProperty(
|
|
fsModulePropertyScopes[wrapperTarget.index],
|
|
key,
|
|
isFsModuleExpression(node.right),
|
|
conditionalWrapperWrite,
|
|
);
|
|
if (!conditionalWrapperWrite) {
|
|
clearFsSafeStoreObjectAliases(
|
|
fsSafeStoreScopes[wrapperTarget.index],
|
|
fsSafeJsonStoreScopes[wrapperTarget.index],
|
|
key,
|
|
);
|
|
}
|
|
setFsSafeStoreObjectAlias(
|
|
fsSafeStoreScopes[wrapperTarget.index],
|
|
fsSafeJsonStoreScopes[wrapperTarget.index],
|
|
key,
|
|
isFsSafeStoreExpression(node.right),
|
|
expressionContainsFsSafeJsonStoreLegacyPath(node.right),
|
|
conditionalWrapperWrite,
|
|
);
|
|
if (!conditionalWrapperWrite) {
|
|
registerFsSafeStoreObjectAliases(
|
|
key,
|
|
node.right,
|
|
fsSafeStoreScopes[wrapperTarget.index],
|
|
fsSafeJsonStoreScopes[wrapperTarget.index],
|
|
);
|
|
}
|
|
if (conditionalWrapperWrite) {
|
|
lastScope(fsWriteAliasScopes).set(key, legacyFsWriteName(node.right));
|
|
lastScope(fsModulePropertyScopes).set(key, isFsModuleExpression(node.right));
|
|
shadowVisibleFsSafeStoreObjectAliases(key);
|
|
lastScope(fsSafeStoreScopes).set(key, isFsSafeStoreExpression(node.right));
|
|
lastScope(fsSafeJsonStoreScopes).set(
|
|
key,
|
|
expressionContainsFsSafeJsonStoreLegacyPath(node.right),
|
|
);
|
|
registerFsSafeStoreObjectAliases(key, node.right);
|
|
recordBranchFsSafeObjectPropertyAssignment(
|
|
wrapperTarget.index,
|
|
propertyAccess.rootName,
|
|
propertyName,
|
|
node.right,
|
|
isFsSafeStoreExpression(node.right),
|
|
expressionContainsFsSafeJsonStoreLegacyPath(node.right),
|
|
);
|
|
}
|
|
const assignedWrapper =
|
|
ts.isFunctionExpression(node.right) || ts.isArrowFunction(node.right)
|
|
? wrapperRecordForNode(node.right)
|
|
: cloneWrapperFunctionValue(resolveWrapperExpression(node.right));
|
|
if (conditionalWrapperWrite) {
|
|
lastScope(wrapperFunctionScopes).set(key, assignedWrapper);
|
|
recordBranchWrapperAssignment(wrapperTarget.index, key, assignedWrapper);
|
|
recordBranchWrapperObjectRewrite(wrapperTarget.index, key, node.right);
|
|
} else {
|
|
clearWrapperObjectMethods(wrapperTarget.wrapperScope, key);
|
|
}
|
|
setWrapperFunctionValue(
|
|
wrapperTarget.wrapperScope,
|
|
key,
|
|
assignedWrapper,
|
|
conditionalWrapperWrite,
|
|
);
|
|
registerWrapperObjectMethods(
|
|
key,
|
|
node.right,
|
|
wrapperTarget.wrapperScope,
|
|
conditionalWrapperWrite,
|
|
);
|
|
}
|
|
|
|
if (ts.isCallExpression(node)) {
|
|
const fsWriteName = legacyFsWriteName(node.expression);
|
|
const filePathOptionsWrite =
|
|
fsWriteName === "appendRegularFile" ||
|
|
fsWriteName === "appendRegularFileSync" ||
|
|
fsWriteName === "replaceFileAtomic" ||
|
|
fsWriteName === "replaceFileAtomicSync";
|
|
if (
|
|
fsWriteName &&
|
|
fsWriteCallMayWrite(fsWriteName, [...node.arguments]) &&
|
|
(filePathOptionsWrite
|
|
? node.arguments[0] && objectFilePathContainsLegacyStore(node.arguments[0])
|
|
: pathArgumentsForFsWrite(fsWriteName, [...node.arguments]).some((argument) =>
|
|
pathArgumentContainsLegacyStore(argument),
|
|
))
|
|
) {
|
|
addViolation(node.expression, "legacy store filesystem write", node);
|
|
}
|
|
if (
|
|
fsSafeStoreWritePathArguments(node).some((argument) =>
|
|
pathArgumentContainsLegacyStore(argument),
|
|
)
|
|
) {
|
|
addViolation(node.expression, "legacy store filesystem write", node);
|
|
}
|
|
if (fsSafeJsonStoreWriteContainsLegacyStore(node)) {
|
|
addViolation(node.expression, "legacy store filesystem write", node);
|
|
}
|
|
const wrapperName = callExpressionName(node.expression);
|
|
const wrapperRecord = wrapperName ? resolveWrapperFunction(wrapperName) : null;
|
|
if (!intrinsicWrapperCalls.has(node) || wrapperCallSites.length === 0) {
|
|
const violationCount = violations.length;
|
|
for (const record of wrapperRecords(wrapperRecord)) {
|
|
executeWrapper(record, node);
|
|
}
|
|
if (definitionScanDepth > 0 && violations.length > violationCount) {
|
|
intrinsicWrapperCalls.add(node);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (
|
|
(ts.isStringLiteralLike(node) || ts.isIdentifier(node) || ts.isTemplateExpression(node)) &&
|
|
bridgeMarkerPattern.test(node.getText(sourceFile))
|
|
) {
|
|
addViolation(node, "legacy transcript bridge marker");
|
|
}
|
|
|
|
ts.forEachChild(node, visit);
|
|
}
|
|
|
|
visit(sourceFile);
|
|
if (
|
|
scanOptions.enforceCurrentLegacyAllowlist &&
|
|
!scanOptions.currentLegacyWriteAllowances &&
|
|
currentLegacyWriteAllowances.size > 0
|
|
) {
|
|
violations.push({ kind: "stale current legacy write allowlist", line: 1 });
|
|
}
|
|
return violations;
|
|
}
|
|
|
|
/**
|
|
* Runs the database-first legacy-store guard.
|
|
*/
|
|
export async function main() {
|
|
const repoRoot = resolveRepoRoot(import.meta.url);
|
|
const sourceRoots = databaseFirstLegacyStoreSourceRoots.map((root) => path.join(repoRoot, root));
|
|
const files = await collectDatabaseFirstLegacyStoreSourceFiles(sourceRoots);
|
|
const nativeSourceRoots = databaseFirstNativeSourceRoots.map((root) => path.join(repoRoot, root));
|
|
const nativeFiles = (await Promise.all(nativeSourceRoots.map(collectNativeSourceFiles))).flat();
|
|
const violations = [];
|
|
const currentLegacyWriteAllowances = currentLegacyWriteViolationAllowances();
|
|
|
|
for (const filePath of files) {
|
|
const relativePath = path.relative(repoRoot, filePath).replaceAll(path.sep, "/");
|
|
const content = await fs.readFile(filePath, "utf8");
|
|
for (const violation of collectDatabaseFirstLegacyStoreViolations(content, relativePath, {
|
|
currentLegacyWriteAllowances,
|
|
})) {
|
|
violations.push(`${relativePath}:${violation.line} ${violation.kind}`);
|
|
}
|
|
}
|
|
for (const fingerprint of currentLegacyWriteAllowances.keys()) {
|
|
const relativePath = currentLegacyWriteViolationPath(fingerprint) ?? "<unknown>";
|
|
violations.push(`${relativePath}:1 stale current legacy write allowlist`);
|
|
}
|
|
for (const filePath of nativeFiles) {
|
|
const relativePath = path.relative(repoRoot, filePath).replaceAll(path.sep, "/");
|
|
const content = await fs.readFile(filePath, "utf8");
|
|
for (const violation of collectDatabaseFirstNativeLegacyStoreViolations(
|
|
content,
|
|
relativePath,
|
|
)) {
|
|
violations.push(`${relativePath}:${violation.line} ${violation.kind}`);
|
|
}
|
|
}
|
|
|
|
if (violations.length === 0) {
|
|
console.log("Database-first legacy-store guard passed.");
|
|
return;
|
|
}
|
|
|
|
console.error("Found database-first legacy-store guard violations:");
|
|
for (const violation of violations.toSorted()) {
|
|
console.error(`- ${violation}`);
|
|
}
|
|
console.error(
|
|
"Runtime state/cache writes must use the shared or per-agent SQLite stores. Keep legacy file import/removal under doctor or migration owners.",
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
runAsScript(import.meta.url, main);
|