Files
openclaw/scripts/check-database-first-legacy-stores.mjs
2026-08-01 05:07:21 -07:00

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);