mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-03 07:54:40 +08:00
With SCRIPTC_INFER_JS=1, shape inference writes its overlay for the program's own JavaScript files, with or without npm-static packages, and leaves package files as they are. The analysis and overlay keep a value untyped where an inferred type would be narrower than what the program holds: awaited values of for await, iterable object literals with computed keys, matchAll and other builtin results, fields an instance may lack, overridden methods and getters, class instances reached by reflection, constructor parameters stored into wider fields, and results the analysis cannot type. A computed member name no longer folds a read that runs before its binding is initialized. In every build, a JavaScript binding written with a value that may be a string, or with a union, keeps the dynamic representation.
8027 lines
338 KiB
TypeScript
8027 lines
338 KiB
TypeScript
/* THE program lifecycle: load → preflight — tsconfig adoption, the tsc
|
|
* gate with its project-world second chance, the import/export form
|
|
* fences, CommonJS require discipline, and the module evaluation order —
|
|
* on the native TypeScript compiler (one spawned tsgo server behind
|
|
* Ts7Host, through the ts7/ adapter) with scriptc's OWN resolver
|
|
* (resolve.ts) standing in for ts.resolveModuleName /
|
|
* ts.resolveTypeReferenceDirective, which typescript@7.0.2 no longer
|
|
* ships.
|
|
*
|
|
* HISTORY. This began as a deliberately MECHANICAL PORT of the
|
|
* typescript@5.9.3 checkPreflight (the adapter's design: swap the ts
|
|
* import, keep every spelling), shipped alongside it for phases 2-3 with
|
|
* the order-parity suite holding both lanes to identical diagnostics and
|
|
* module order over the whole corpus. The phase-4 flip deleted the 5.9.3
|
|
* pipeline; its recorded verdicts live on as the canary baselines
|
|
* (test/ts7/baselines/order-parity/), and the rationale prose the old
|
|
* program.ts carried was folded in here where behavior demands it.
|
|
*
|
|
* Differences from the retired 5.9.3 lane, each deliberate and each
|
|
* pinned by the canary baselines:
|
|
* - Module resolution is resolve.ts (answer-parity proven by its suite).
|
|
* - Diagnostics carry pos/end and flat text + messageChain; toPassthrough
|
|
* renders through the adapter's flattenDiagnosticMessageText, whose
|
|
* output is identical to 5.9.3's flattener output.
|
|
* - Finding 5 (tsgo models CommonJS strictly): TS2309 joins the JS-file
|
|
* relaxation here — 5.9.3 never fires it on JS CJS files, tsgo does on
|
|
* the table-then-member pattern. The EXPORT-IDENTITY fences do not live
|
|
* in tsc codes at all: cjsExportDiscardReason (lowering) names every
|
|
* genuinely discarded export, so relaxing the checker's stricter
|
|
* modeling loses nothing.
|
|
* - isNodeEsmFile: 7's client SourceFile has no impliedNodeFormat, so this
|
|
* lane spells Node's format decision directly: fixed extensions first,
|
|
* then an explicit nearest-package `type`, then syntax detection for
|
|
* ambiguous .js/.ts files.
|
|
* - Malformed tsconfig.json: tsgo's parseConfigFile swallows JSONC syntax
|
|
* errors (recovers to empty options) where 5.9.3 reported a passthrough
|
|
* diagnostic, so this lane validates the config's JSONC syntax itself
|
|
* (jsoncSyntaxError) and fails preflight like 5.9.3 does — with
|
|
* JSON.parse's wording rather than tsc's, the one message-text delta on
|
|
* that path (no snapshot pins it). */
|
|
|
|
import { isFrontendIsland } from "../node-islands/boundary.js";
|
|
import { builtinModules } from "node:module";
|
|
import { dirname, isAbsolute, resolve } from "node:path";
|
|
import * as ts from "./ts7/adapter.js";
|
|
import type { ScrDiagnostic } from "../diagnostics/diagnostic.js";
|
|
import {
|
|
commonJsModuleSyntaxDiag,
|
|
invalidPackageConfigDiag,
|
|
strictNullChecksFloorDiag,
|
|
tscPassthroughDiag,
|
|
unsupportedDiag,
|
|
} from "../diagnostics/diagnostic.js";
|
|
import {
|
|
isNodeModulesPath,
|
|
nearestInvalidPackageJsonPath,
|
|
nearestPackageType,
|
|
nearestPkgJsonPath,
|
|
packageImportTarget,
|
|
projectDtsRuntimeSibling,
|
|
resolveBareModule,
|
|
resolveProjectModule,
|
|
resolveTypeDirective,
|
|
setProjectPathMappings,
|
|
setProjectRealm,
|
|
} from "./resolve.js";
|
|
import { probeNodeImportRefusal, probeNodeRequireRefusal } from "./npm.js";
|
|
import {
|
|
isNpmStaticPackage,
|
|
npmStaticActive,
|
|
npmStaticFsShadow,
|
|
programBuiltinAliasSource,
|
|
clearProgramBuiltinAliasCache,
|
|
npmStaticLexerVisibleNames,
|
|
npmStaticLiveExportTable,
|
|
npmStaticPackageOfPath,
|
|
reportNpmStaticOffender,
|
|
setNpmStaticDeclarationOverloads,
|
|
setNpmStaticPackages,
|
|
setNpmStaticTypeExports,
|
|
} from "./npm-static.js";
|
|
import { isPrunedNpmReexport, planNpmStaticReexports } from "./npm-static-prune.js";
|
|
import { npmStaticTypeExportPlan } from "./npm-static-type-exports-host.js";
|
|
import { isNpmStaticSubclassArgument } from "./npm-static-subtyping.js";
|
|
import {
|
|
npmStaticDeclarationReexports,
|
|
npmStaticRuntimeClassTargets,
|
|
parseNpmStaticDeclarationOverloads,
|
|
parseNpmStaticDeclarationProperties,
|
|
} from "./npm-static-declaration-syntax.js";
|
|
import type { FrontendServices } from "./services.js";
|
|
import type {
|
|
NpmStaticDeclarationOverloads,
|
|
NpmStaticDeclarationProperties,
|
|
NpmStaticOverloadSignature,
|
|
} from "./npm-static-declaration-syntax.js";
|
|
import {
|
|
configurePackageShims,
|
|
isPackageShimFile,
|
|
isShimmedImport,
|
|
isUnloadedPackage,
|
|
npmStaticOmitsPackage,
|
|
} from "./package-shims.js";
|
|
import { provenanceEntryFor, provenancePaths } from "./provenance-registry.js";
|
|
import { cjsLexedExportsOfFile, cjsVisibleNames } from "./cjs-syntax.js";
|
|
import {
|
|
ambientDtsPath,
|
|
fallbackDtsPath,
|
|
fallbackModulesNamedIn,
|
|
isFallbackDtsPath,
|
|
isNodeTypesPath,
|
|
overridesDtsPath,
|
|
tsgoPath,
|
|
} from "./dts-paths.js";
|
|
import {
|
|
builtinDefaultImportModule,
|
|
canonicalBuiltinModule,
|
|
npmBuiltinAliasOf,
|
|
SUPPORTED_NODE_MODULES,
|
|
unsupportedModuleFeatureOf,
|
|
TEMPORAL_GLOBAL,
|
|
} from "./builtin-modules.js";
|
|
import {
|
|
clearWorkspacePackages,
|
|
isRelativeSpecifier,
|
|
isWorkspacePackageName,
|
|
npmPackageNameOf,
|
|
packageNameOfSpecifier,
|
|
registerWorkspacePackage,
|
|
workspacePackageOfPath,
|
|
} from "./workspace-registry.js";
|
|
import {
|
|
ADOPTED_OPTIONS,
|
|
isJsSourceFileName,
|
|
JS_ANY_OPERATOR_CODES,
|
|
JS_RELAXED_TSC_CODES,
|
|
} from "./tsc-codes.js";
|
|
import { trackedFileExists, trackedReadFile, trackedRealpath } from "./input-tracker.js";
|
|
import { earlyErrorDiagnostics7 } from "./early-errors.js";
|
|
import { forkTargetPaths } from "./fork-target.js";
|
|
import { workerTargetPaths } from "./worker-target.js";
|
|
import {
|
|
erasedTypeOnlyImport,
|
|
typeOnlyNamedImport,
|
|
typeOnlyReachedFiles,
|
|
} from "./type-only-imports.js";
|
|
import { moduleSourceCandidates } from "./module-source-candidates.js";
|
|
import {
|
|
planDeclProjection,
|
|
resetDeclBoundaryOrigins,
|
|
type DeclProjection,
|
|
} from "./decl-projection.js";
|
|
import { configureTypePatches, packageSourceText } from "./type-patches/layer.js";
|
|
import { resetDeclOriginVersions } from "./lowering/decl-origin.js";
|
|
import {
|
|
reportShapes,
|
|
resetShapeOverlay,
|
|
shapeOverlayDone,
|
|
shapeOverlayEnabled,
|
|
shapeOverlayer,
|
|
captureShapeOverlayState,
|
|
type ShapeOverlayLoadState,
|
|
} from "./shape-inference/index.js";
|
|
import { jsInferenceEnabled } from "./js-inference.js";
|
|
import { inferredJsDiagnosticSuppressed } from "./inferred-js-diagnostics.js";
|
|
import {
|
|
containsJsx,
|
|
firstJsx,
|
|
jsxEmitName,
|
|
jsxEmitPlan,
|
|
jsxPragmas,
|
|
lowerJsx,
|
|
type LoweredJsx,
|
|
} from "./jsx-source.js";
|
|
|
|
const BASE_OPTIONS: ts.Ts7CompilerOptions = {
|
|
strict: true,
|
|
};
|
|
|
|
const FORCED_OPTIONS: ts.Ts7CompilerOptions = {
|
|
target: ts.ScriptTarget.ESNext as number,
|
|
module: ts.ModuleKind.ESNext as number,
|
|
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
// Every file Node executes has its own lexical module wrapper: ESM has a
|
|
// module environment and CommonJS has the per-file function wrapper. In
|
|
// particular, an ambiguous file whose ONLY ESM marker is top-level await
|
|
// must not remain a TypeScript global script (otherwise declarations in
|
|
// sibling files merge and reads cross module boundaries). Runtime format
|
|
// classification is intentionally independent below; this option is only
|
|
// the binder's scope decision.
|
|
moduleDetection: ts.ModuleDetectionKind.Force,
|
|
// Explicit resource management, Array.fromAsync and Error.isError are
|
|
// ESNext library slices in TS 7 even though Node 24 ships them. Include
|
|
// those focused libs without widening the rest of the standard-library
|
|
// target past ES2025.
|
|
lib: [
|
|
"lib.es2025.d.ts",
|
|
"lib.esnext.disposable.d.ts",
|
|
"lib.esnext.array.d.ts",
|
|
"lib.esnext.error.d.ts",
|
|
],
|
|
types: [],
|
|
allowImportingTsExtensions: true,
|
|
allowJs: true,
|
|
checkJs: true,
|
|
resolveJsonModule: true,
|
|
noEmit: true,
|
|
};
|
|
|
|
/** JSONC syntax validation for a tsconfig text: tsgo's parseConfigFile
|
|
* RECOVERS over syntax errors without reporting them (probed: a hard-broken
|
|
* file answers empty options, no diagnostic), where 5.9.3's readConfigFile
|
|
* reported the first parse error — a preflight-visible difference (a broken
|
|
* config fails the build instead of being adopted). tsconfig's grammar is
|
|
* JSON plus comments and trailing commas, so stripping exactly those and
|
|
* handing the rest to JSON.parse decides validity without either
|
|
* TypeScript's parser. The
|
|
* MESSAGE is JSON.parse's, not 5.9.3's ("'}' expected.") — the one
|
|
* remaining wording delta on this path, unpinned by any snapshot. */
|
|
function jsoncSyntaxError(text: string): string | null {
|
|
let out = "";
|
|
for (let i = 0; i < text.length; i++) {
|
|
const ch = text[i]!;
|
|
if (ch === '"') {
|
|
const from = i;
|
|
i++;
|
|
while (i < text.length && text[i] !== '"') i += text[i] === "\\" ? 2 : 1;
|
|
out += text.slice(from, i + 1);
|
|
continue;
|
|
}
|
|
if (ch === "/" && text[i + 1] === "/") {
|
|
while (i < text.length && text[i] !== "\n") i++;
|
|
out += "\n";
|
|
continue;
|
|
}
|
|
if (ch === "/" && text[i + 1] === "*") {
|
|
const close = text.indexOf("*/", i + 2);
|
|
i = close < 0 ? text.length : close + 1;
|
|
continue;
|
|
}
|
|
out += ch;
|
|
}
|
|
// Trailing commas: `,` followed only by whitespace before } or ].
|
|
out = out.replace(/,(\s*[}\]])/g, "$1");
|
|
if (out.trim() === "") return null; // an empty config file is legal
|
|
try {
|
|
JSON.parse(out);
|
|
return null;
|
|
} catch (e) {
|
|
return e instanceof Error ? e.message : String(e);
|
|
}
|
|
}
|
|
|
|
/** Preserve TypeScript 5.9's computed synthetic-default permission from the
|
|
* project's own module settings. Preflight later forces ESNext/Bundler for
|
|
* scriptc's checker, so those forced options must not decide which source
|
|
* import forms were legal in the original project. */
|
|
function projectAllowsSyntheticDefaultImports(options: Record<string, unknown>): boolean {
|
|
const explicitSyntheticDefaultImports = options["allowSyntheticDefaultImports"];
|
|
if (explicitSyntheticDefaultImports !== undefined)
|
|
return explicitSyntheticDefaultImports === true;
|
|
const moduleKind = options["module"];
|
|
const explicitEsModuleInterop = options["esModuleInterop"];
|
|
const esModuleInterop =
|
|
explicitEsModuleInterop !== undefined
|
|
? explicitEsModuleInterop === true
|
|
: moduleKind === ts.ModuleKind.Node16 ||
|
|
moduleKind === ts.ModuleKind.Node18 ||
|
|
moduleKind === ts.ModuleKind.Node20 ||
|
|
moduleKind === ts.ModuleKind.NodeNext ||
|
|
moduleKind === ts.ModuleKind.Preserve;
|
|
return (
|
|
esModuleInterop ||
|
|
moduleKind === ts.ModuleKind.System ||
|
|
options["moduleResolution"] === ts.ModuleResolutionKind.Bundler
|
|
);
|
|
}
|
|
|
|
/** The project's tsconfig adoption in the 7 world: tsgo's own config parser
|
|
* (extends chains resolved server-side), the ADOPTED_OPTIONS subset taken,
|
|
* the rest forced, the strictNullChecks floor enforced. */
|
|
function adoptProjectConfig7(
|
|
host: ts.Ts7Host,
|
|
entryPath: string,
|
|
): {
|
|
configFile: string | null;
|
|
options: ts.Ts7CompilerOptions;
|
|
diags: ScrDiagnostic[];
|
|
projectLib?: readonly string[];
|
|
forcedStrictNullChecks?: boolean;
|
|
checkJs?: boolean;
|
|
} {
|
|
const configFile = ts.findConfigFile(dirname(entryPath), ts.sys.fileExists) ?? null;
|
|
if (!configFile) {
|
|
return { configFile, options: { ...BASE_OPTIONS, ...FORCED_OPTIONS }, diags: [] };
|
|
}
|
|
const diags: ScrDiagnostic[] = [];
|
|
const syntaxError = jsoncSyntaxError(ts.sys.readFile(configFile) ?? "");
|
|
if (syntaxError !== null) {
|
|
diags.push(tscPassthroughDiag(syntaxError, { file: configFile, start: 0, end: 0 }));
|
|
return { configFile, options: { ...BASE_OPTIONS, ...FORCED_OPTIONS }, diags };
|
|
}
|
|
const parsed = host.parseConfigFile(configFile);
|
|
const adopted: Record<string, unknown> = {};
|
|
for (const key of ADOPTED_OPTIONS) {
|
|
const value = parsed.options[key];
|
|
if (value !== undefined) adopted[key] = value;
|
|
}
|
|
if (projectAllowsSyntheticDefaultImports(parsed.options)) {
|
|
adopted["allowSyntheticDefaultImports"] = true;
|
|
}
|
|
// TS 7 accepts `paths`, but no longer accepts `baseUrl` as a compiler
|
|
// option. Translate the established baseUrl+paths spelling into absolute
|
|
// targets: the synthesized config lives beside the entry rather than the
|
|
// real config, so preserving relative targets would change their meaning
|
|
// without a diagnostic. The absolute map is also the own resolver's
|
|
// configuration.
|
|
const rawPaths = parsed.options["paths"];
|
|
if (rawPaths !== undefined && typeof rawPaths === "object" && rawPaths !== null) {
|
|
const configDir = dirname(configFile);
|
|
const rawBaseUrl = parsed.options["baseUrl"];
|
|
const base =
|
|
typeof rawBaseUrl === "string"
|
|
? isAbsolute(rawBaseUrl)
|
|
? rawBaseUrl
|
|
: resolve(configDir, rawBaseUrl)
|
|
: configDir;
|
|
const paths: Record<string, string[]> = {};
|
|
for (const [key, targets] of Object.entries(rawPaths as Record<string, unknown>)) {
|
|
if (!Array.isArray(targets)) continue;
|
|
paths[key] = targets
|
|
.filter((target): target is string => typeof target === "string")
|
|
.map((target) => (isAbsolute(target) ? target : resolve(base, target)));
|
|
}
|
|
adopted["paths"] = paths;
|
|
}
|
|
const nullChecks = adopted["strictNullChecks"] ?? adopted["strict"] ?? false;
|
|
const forcedStrictNullChecks = nullChecks !== true;
|
|
if (forcedStrictNullChecks) {
|
|
diags.push(strictNullChecksFloorDiag(configFile));
|
|
adopted["strictNullChecks"] = true;
|
|
}
|
|
// The project's own library selection, kept only to explain errors: the
|
|
// checker always uses scriptc's fixed library set (FORCED_OPTIONS).
|
|
const rawLib = parsed.options["lib"];
|
|
const projectLib = Array.isArray(rawLib)
|
|
? rawLib.filter((entry): entry is string => typeof entry === "string")
|
|
: undefined;
|
|
return {
|
|
configFile,
|
|
options: { ...BASE_OPTIONS, ...adopted, ...FORCED_OPTIONS },
|
|
diags,
|
|
...(projectLib === undefined ? {} : { projectLib }),
|
|
forcedStrictNullChecks,
|
|
checkJs: parsed.options["checkJs"] === true,
|
|
};
|
|
}
|
|
|
|
/** The target project's @types/node, resolved with the OWN resolver's
|
|
* type-directive lookup anchored at the ENTRY (typeRoots-free — exactly the
|
|
* secondary lookup 5.9.3 ran with typeRoots emptied). */
|
|
function resolveNodeTypes7(entryPath: string): string | null {
|
|
const file = resolveTypeDirective("node", entryPath);
|
|
return file !== null && isNodeTypesPath(file) ? file : null;
|
|
}
|
|
|
|
export interface TypeWorldDifferences {
|
|
/** No @types/node: the bundled fallback declarations type Node. */
|
|
nodeFallback: boolean;
|
|
/** The project tsconfig's "lib" entries (scriptc checks with its own). */
|
|
projectLib: readonly string[] | null;
|
|
/** The project disables strictNullChecks; scriptc forces it on. */
|
|
forcedStrictNullChecks: boolean;
|
|
/** The project's tsconfig sets `checkJs` (scriptc's own checker always
|
|
* reads JavaScript files; this records whether the project's does). */
|
|
projectCheckJs: boolean;
|
|
}
|
|
|
|
export interface LoadResult {
|
|
program: ts.Program;
|
|
entry: ts.SourceFile;
|
|
/** Shape inference's overlay state for this load (restored before its
|
|
* lowering: a later load resets the per-process registry). */
|
|
shapeOverlayState?: ShapeOverlayLoadState;
|
|
/** User source files in Node's evaluation order: depth-first postorder
|
|
* of the import graph from the entry, each module once. The entry is
|
|
* last. Empty until checkPreflight runs (and empty when preflight fails
|
|
* structurally). */
|
|
moduleOrder: ts.SourceFile[];
|
|
/** Inline require() calls whose edge closes a require cycle. Node answers
|
|
* them with the still-evaluating module's partial exports, which the
|
|
* static export globals do not model; their lowering refuses them by name
|
|
* at run time instead of refusing the whole program. Filled by
|
|
* checkPreflight. */
|
|
inlineRequireCycleCalls?: ReadonlySet<ts.Node>;
|
|
/** Every program file's module edges as preflight resolved them,
|
|
* require() edges included: the lowering orders the modules only a
|
|
* literal import(), fork() or Worker reaches with them. Filled by
|
|
* checkPreflight. */
|
|
moduleEdges?: ReadonlyMap<ts.SourceFile, readonly CycleEdge[]> | undefined;
|
|
/** Program modules only a literal import() loads whose static graph
|
|
* Node's resolution refuses: each such import() rejects with the refusal
|
|
* instead of failing the build, and the module never joins the compiled
|
|
* graph. Filled by checkPreflight. */
|
|
dynamicImportRejections?: ReadonlyMap<ts.SourceFile, StartupCrash> | undefined;
|
|
/** Node's startup refusal, when the graph carries an import edge Node
|
|
* rejects before ANY module evaluates — a RESOLUTION refusal (a bare
|
|
* specifier nothing installed resolves, an exports/imports map that
|
|
* refuses the subpath or names a missing file, an invalid "#"
|
|
* specifier: fetch/resolve phase, ERR_MODULE_NOT_FOUND and friends) or
|
|
* the module-LINK SyntaxError (a named import of a CommonJS export
|
|
* Node's lexer cannot detect: instantiate phase). Either way Node
|
|
* refuses the whole program before evaluation, so the lowering compiles
|
|
* the program to exactly that startup crash (buildMain); resolution
|
|
* refusals win over the link SyntaxError because Node's fetch phase
|
|
* runs first. Null when the graph resolves and links clean. Set by
|
|
* checkPreflight. */
|
|
startupCrash?: StartupCrash | null;
|
|
configDiags: ScrDiagnostic[];
|
|
/** SC6102 performance notes of the declaration-typed boundary
|
|
* (decl-projection.ts): files whose projection was dropped. */
|
|
declBoundaryNotes?: ScrDiagnostic[];
|
|
/** The files whose JSX the program lowers (jsx-source.ts), by file name.
|
|
* The program holds their rewritten text; positions in diagnostics refer
|
|
* to it. */
|
|
jsxSources?: ReadonlyMap<string, LoweredJsx>;
|
|
/** With JSX sources: the program over the original text, whose TypeScript
|
|
* errors are the program's, and its project world when it has errors. */
|
|
tscWorld?: {
|
|
program: ts.Program;
|
|
/** The program's pre-emit diagnostics, computed once. */
|
|
diagnostics: readonly ts.Diagnostic[];
|
|
projectWorld: ts.Program | null;
|
|
/** The declaration-typed boundary rewrote user files: errors fall
|
|
* back to the declaration world (load.projectWorld()), as without it. */
|
|
declWorldFallback?: true;
|
|
};
|
|
/** How scriptc's type world differs from the project's own tsc setup —
|
|
* used only to label the TypeScript errors those differences cause. */
|
|
typeWorld?: TypeWorldDifferences;
|
|
/** Coverage-only external host modules: exact bare specifier → local
|
|
* declaration file. These participate in checker resolution but never
|
|
* become runtime module edges. */
|
|
externalTypes: ReadonlyMap<string, string>;
|
|
/** The mapped declarations plus their relative declaration-file closure,
|
|
* keyed by normalized file name and attributed to every owning external
|
|
* specifier. Multiple exact module names may deliberately share one
|
|
* declaration surface. */
|
|
externalTypeSpecifiersByFile: ReadonlyMap<string, readonly string[]>;
|
|
projectWorld: () => ts.Program;
|
|
}
|
|
|
|
function declarationReexportTarget(fromFile: string, specifier: string): string | null {
|
|
const base = resolve(dirname(fromFile), specifier);
|
|
const candidates: string[] = [];
|
|
const extension = /\.(mjs|cjs|js)$/.exec(base)?.[1];
|
|
if (extension !== undefined) {
|
|
const stem = base.slice(0, -(extension.length + 1));
|
|
if (extension === "mjs") candidates.push(`${stem}.d.mts`, `${stem}.d.ts`);
|
|
else if (extension === "cjs") candidates.push(`${stem}.d.cts`, `${stem}.d.ts`);
|
|
else candidates.push(`${stem}.d.ts`, `${stem}.d.mts`, `${stem}.d.cts`);
|
|
} else {
|
|
candidates.push(base, `${base}.d.ts`, `${base}.d.mts`, `${base}.d.cts`);
|
|
}
|
|
candidates.push(
|
|
resolve(base, "index.d.ts"),
|
|
resolve(base, "index.d.mts"),
|
|
resolve(base, "index.d.cts"),
|
|
);
|
|
return candidates.find((candidate) => trackedFileExists(candidate)) ?? null;
|
|
}
|
|
|
|
function runtimeReexportTarget(fromFile: string, specifier: string): string | null {
|
|
const base = resolve(dirname(fromFile), specifier);
|
|
const candidates = [
|
|
base,
|
|
`${base}.js`,
|
|
`${base}.mjs`,
|
|
`${base}.cjs`,
|
|
resolve(base, "index.js"),
|
|
resolve(base, "index.mjs"),
|
|
resolve(base, "index.cjs"),
|
|
];
|
|
const target = candidates.find(
|
|
(candidate) => trackedFileExists(candidate) && isJsSourceFileName(candidate),
|
|
);
|
|
return target === undefined ? null : (trackedRealpath(target) ?? target);
|
|
}
|
|
|
|
/** Visits an opted package's own declaration entry and relative barrel
|
|
* closure. This is intentionally narrower than TypeScript module
|
|
* resolution: bare edges name other packages and never inherit trust. */
|
|
function visitNpmStaticDeclarationClosure(
|
|
services: FrontendServices,
|
|
entryPath: string,
|
|
onSource: (path: string, source: string) => void,
|
|
): void {
|
|
const seen = new Set<string>();
|
|
const packageJson = nearestPkgJsonPath(entryPath);
|
|
const packageRoot = (packageJson === null ? dirname(entryPath) : dirname(packageJson))
|
|
.split("\\")
|
|
.join("/");
|
|
const visit = (path: string): void => {
|
|
path = resolve(path);
|
|
const normalized = path.split("\\").join("/");
|
|
if (normalized !== packageRoot && !normalized.startsWith(packageRoot + "/")) return;
|
|
if (seen.has(path)) return;
|
|
seen.add(path);
|
|
const source = packageSourceText(path);
|
|
if (source === null) return;
|
|
onSource(path, source);
|
|
for (const specifier of npmStaticDeclarationReexports(services.parse(path, source, "ts"))) {
|
|
const target = declarationReexportTarget(path, specifier);
|
|
if (target !== null) visit(target);
|
|
}
|
|
};
|
|
visit(entryPath);
|
|
}
|
|
|
|
/** The overloads and properties of the classes a package's declaration
|
|
* closure declares, read from one parse of each declaration file. */
|
|
function npmStaticDeclarationMetadataOf(
|
|
services: FrontendServices,
|
|
entryPath: string,
|
|
): { overloads: NpmStaticDeclarationOverloads; properties: NpmStaticDeclarationProperties } {
|
|
const overloads = new Map<string, Map<string, readonly NpmStaticOverloadSignature[]>>();
|
|
const properties = new Map<string, Map<string, string>>();
|
|
visitNpmStaticDeclarationClosure(services, entryPath, (path, source) => {
|
|
const sourceFile = services.parse(path, source, "ts");
|
|
for (const [className, methods] of parseNpmStaticDeclarationOverloads(sourceFile)) {
|
|
const target =
|
|
overloads.get(className) ?? new Map<string, readonly NpmStaticOverloadSignature[]>();
|
|
overloads.set(className, target);
|
|
for (const [methodName, signatures] of methods) {
|
|
if (!target.has(methodName)) target.set(methodName, signatures);
|
|
}
|
|
}
|
|
for (const [className, fields] of parseNpmStaticDeclarationProperties(sourceFile)) {
|
|
const target = properties.get(className) ?? new Map<string, string>();
|
|
properties.set(className, target);
|
|
for (const [propertyName, type] of fields) {
|
|
if (!target.has(propertyName)) target.set(propertyName, type);
|
|
}
|
|
}
|
|
});
|
|
return { overloads, properties };
|
|
}
|
|
|
|
/** See LoadResult.startupCrash: Node's exact error message, the IR error
|
|
* class that carries it (a RUNTIME_ERROR_CLASSES name — %Error for the
|
|
* resolver's ERR_MODULE_NOT_FOUND family, %TypeError for invalid-specifier
|
|
* refusals, %SyntaxError for either named-import link check), and the source
|
|
* position of the refused edge. */
|
|
export interface StartupCrash {
|
|
message: string;
|
|
className: "%Error" | "%TypeError" | "%SyntaxError";
|
|
loc: { file: string; start: number; end: number };
|
|
/** Node's error code, for the refusals that carry one. */
|
|
code?: string;
|
|
}
|
|
|
|
/** True when a coverage mapping names one exact bare host module. TypeScript
|
|
* gives `paths` keys containing `*` pattern semantics, so accepting one here
|
|
* would broaden the mapping beyond the specifier the caller named. */
|
|
export function isExactExternalTypeSpecifier(specifier: string): boolean {
|
|
return (
|
|
specifier !== "" &&
|
|
!specifier.startsWith(".") &&
|
|
!specifier.startsWith("/") &&
|
|
!specifier.startsWith("#") &&
|
|
!specifier.startsWith("node:") &&
|
|
!specifier.includes("*")
|
|
);
|
|
}
|
|
|
|
function externalHostModuleDiag7(specifier: string, node: ts.Node): ScrDiagnostic {
|
|
return unsupportedDiag(
|
|
"SC1010",
|
|
locOf7(node),
|
|
`the '${specifier}' external host module (types supplied by --external-types, but no runtime implementation or scriptc lowering was provided)`,
|
|
"the declaration mapping is analysis-only: coverage continues through project code, while executing this module requires an embedder integration with explicit runtime semantics",
|
|
);
|
|
}
|
|
|
|
/** Expand each mapped declaration entry through RELATIVE declaration
|
|
* dependencies. Declaration packages commonly expose a barrel index.d.ts;
|
|
* the structural types declared in its sibling files are part of the same
|
|
* checker-only host surface. Bare imports deliberately do not inherit this
|
|
* trust: they name a separate package/surface and need their own mapping. */
|
|
function externalTypeFileClosure7(
|
|
program: ts.Program,
|
|
externalTypes: ReadonlyMap<string, string>,
|
|
): ReadonlyMap<string, readonly string[]> {
|
|
const ownersByFile = new Map<string, Set<string>>();
|
|
const addOwner = (file: string, specifier: string): boolean => {
|
|
let owners = ownersByFile.get(file);
|
|
if (owners === undefined) {
|
|
owners = new Set();
|
|
ownersByFile.set(file, owners);
|
|
}
|
|
if (owners.has(specifier)) return false;
|
|
owners.add(specifier);
|
|
return true;
|
|
};
|
|
for (const [specifier, file] of externalTypes) {
|
|
addOwner(tsgoPath(resolve(file)), specifier);
|
|
}
|
|
|
|
const queue: { sf: ts.SourceFile; owner: string }[] = [];
|
|
for (const [owner, entry] of externalTypes) {
|
|
const file = tsgoPath(resolve(entry));
|
|
const sf = program.getSourceFile(file);
|
|
if (sf?.isDeclarationFile) queue.push({ sf, owner });
|
|
}
|
|
const visited = new Set<string>();
|
|
while (queue.length > 0) {
|
|
const { sf, owner } = queue.shift()!;
|
|
const file = tsgoPath(resolve(sf.fileName));
|
|
const visitKey = `${file}\0${owner}`;
|
|
if (visited.has(visitKey)) continue;
|
|
visited.add(visitKey);
|
|
|
|
const admit = (dep: ts.SourceFile | null): void => {
|
|
if (dep === null || !dep.isDeclarationFile) return;
|
|
const depFile = tsgoPath(resolve(dep.fileName));
|
|
if (!addOwner(depFile, owner)) return;
|
|
queue.push({ sf: dep, owner });
|
|
};
|
|
// SourceFile.imports includes import/export declarations, import =
|
|
// require(), and import("…") type nodes. Only relative edges inherit
|
|
// the mapped entry's ownership.
|
|
for (const node of sf.imports) {
|
|
if (ts.isStringLiteralLike(node) && isRelativeSpecifier(node.text)) {
|
|
admit(resolveImport7(program, sf, node.text));
|
|
}
|
|
}
|
|
// Triple-slash path references are the other declaration-local edge.
|
|
for (const ref of sf.referencedFiles) {
|
|
const depPath = tsgoPath(resolve(dirname(sf.fileName), ref.fileName));
|
|
admit(program.getSourceFile(depPath) ?? null);
|
|
}
|
|
}
|
|
return new Map([...ownersByFile].map(([file, owners]) => [file, [...owners]] as const));
|
|
}
|
|
|
|
/** The fallback declaration files of the Node modules the program's files
|
|
* name (fallbackModulesNamedIn) that the program does not hold yet. Each
|
|
* file is read as tsgo reads it; the TypeScript libraries and scriptc's own
|
|
* declarations name no module that needs a file. */
|
|
function fallbackModuleRoots7(program: ts.Program, host: ts.Ts7Host): string[] {
|
|
const names = program.getSourceFileNames();
|
|
const held = new Set(names.filter(isFallbackDtsPath).map((name) => tsgoPath(name)));
|
|
const ambient = ambientDtsPath();
|
|
const overrides = overridesDtsPath();
|
|
const roots = new Set<string>();
|
|
for (const name of names) {
|
|
const path = tsgoPath(name);
|
|
if (held.has(path) || path === ambient || path === overrides) continue;
|
|
if (/\/lib\.[^/]*\.d\.ts$/.test(path) && program.getSourceFileMetadata(name)?.isDefaultLibrary)
|
|
continue;
|
|
for (const file of fallbackModulesNamedIn(host.sourceText(name) ?? ""))
|
|
if (!held.has(file)) roots.add(file);
|
|
}
|
|
return [...roots];
|
|
}
|
|
|
|
/** Program roots hidden behind canonical createRequire bindings. tsgo
|
|
* discovers ordinary import/require edges itself, but a source binding
|
|
* returned by createRequire is intentionally just a function to the type
|
|
* checker. Rebuild the native program with each literal project target as
|
|
* an explicit root, to a fixpoint, so the same TS 7 AST/checker world owns
|
|
* their exports and transitive graph. */
|
|
function createRequireProgramRoots7(program: ts.Program): string[] {
|
|
const known = new Set(program.getSourceFileNames().map((name) => tsgoPath(resolve(name))));
|
|
const roots = new Set<string>();
|
|
for (const sf of program.getImplementationSourceFiles()) {
|
|
if (sf.isDeclarationFile || sf.fileName.endsWith(".json")) continue;
|
|
const calls: { callee: ts.Identifier; spec: string }[] = [];
|
|
for (const node of moduleSourceCandidates(program, sf).calls) {
|
|
if (
|
|
node.questionDotToken !== undefined ||
|
|
node.arguments.nodes.length !== 1 ||
|
|
!ts.isStringLiteralLike(node.arguments.nodes[0]!) ||
|
|
!ts.isIdentifier(node.expression)
|
|
) {
|
|
continue;
|
|
}
|
|
calls.push({ callee: node.expression, spec: node.arguments.nodes[0]!.text });
|
|
}
|
|
// Root discovery only needs the callee's binding. A first-miss lookup
|
|
// otherwise fetches symbols and types for every identifier in the file,
|
|
// including all unreachable bodies in large published packages.
|
|
program.getTypeChecker().prefetchSymbolNodesExact(calls.map((call) => call.callee));
|
|
for (const { callee, spec } of calls) {
|
|
if (!isCreateRequireBinding7(program, callee)) continue;
|
|
if (canonicalBuiltinModule(spec) !== null) continue;
|
|
let target = resolveProjectModule(sf.fileName, spec);
|
|
if (target === null && !spec.startsWith("#")) {
|
|
const npm = resolveNpmImport7(sf.fileName, spec, "require");
|
|
if (
|
|
npm !== null &&
|
|
isNpmStaticPackage(npm.packageName) &&
|
|
isJsSourceFileName(npm.typesFile)
|
|
) {
|
|
target = npm.typesFile;
|
|
}
|
|
}
|
|
// A directory's package.json may point at a native addon. Keep it
|
|
// out of TypeScript's source roots so the require call can report
|
|
// the native-addon boundary instead of an unsupported-file error.
|
|
if (target === null || target.endsWith(".json") || target.endsWith(".node")) continue;
|
|
const normalized = tsgoPath(resolve(target));
|
|
if (!known.has(normalized)) roots.add(normalized);
|
|
}
|
|
}
|
|
return [...roots].sort();
|
|
}
|
|
|
|
/** Whether the tsc gate can keep a file's diagnostics when the program
|
|
* loads: it drops all of an opted-in package's files and of the package
|
|
* shims (keptErrors in preflight also drops unselected package JavaScript,
|
|
* which is not known yet here, so those files are still checked). */
|
|
function tscGateFileKept(fileName: string): boolean {
|
|
return npmStaticPackageOfPath(fileName) === null && !isPackageShimFile(fileName);
|
|
}
|
|
|
|
/** Classify the explicitly selected installed package's own sources. */
|
|
export function entryPackageFilePredicate(entryPath: string): (file: string) => boolean {
|
|
const entryFile = tsgoPath(entryPath);
|
|
const installedAt = entryFile.lastIndexOf("/node_modules/");
|
|
const prefix =
|
|
installedAt < 0
|
|
? null
|
|
: entryFile.slice(0, installedAt) + "/node_modules/" + npmPackageNameOf(entryFile) + "/";
|
|
return (file) => {
|
|
const normalized = tsgoPath(file);
|
|
return (
|
|
normalized === entryFile ||
|
|
(prefix !== null &&
|
|
normalized.startsWith(prefix) &&
|
|
!normalized.slice(prefix.length).split("/").includes("node_modules"))
|
|
);
|
|
};
|
|
}
|
|
|
|
/** maxNodeModuleJsDepth can omit relative imports within an admitted
|
|
* package, and opted-in packages deep in the dependency graph. Discover
|
|
* their runtime sources to a fixpoint so the checker and module resolver
|
|
* see the same graph regardless of import traversal order or depth. */
|
|
function entryPackageProgramRoots7(
|
|
program: ts.Program,
|
|
entryPath: string,
|
|
rootNpmStaticEdges: boolean,
|
|
): string[] {
|
|
const belongsToEntry = entryPackageFilePredicate(entryPath);
|
|
const roots = new Set<string>();
|
|
for (const sf of program.getImplementationSourceFiles()) {
|
|
if (
|
|
sf.isDeclarationFile ||
|
|
(!belongsToEntry(sf.fileName) && npmStaticPackageOfPath(sf.fileName) === null)
|
|
)
|
|
continue;
|
|
for (const specifier of sf.imports) {
|
|
if (!ts.isStringLiteralLike(specifier)) continue;
|
|
const target = resolveProjectModule(sf.fileName, specifier.text);
|
|
if (
|
|
target !== null &&
|
|
(belongsToEntry(target) || npmStaticPackageOfPath(target) !== null) &&
|
|
isJsSourceFileName(target) &&
|
|
program.getSourceFile(target) === undefined
|
|
)
|
|
roots.add(target);
|
|
// An opted-in package's bare edge counts against the same depth
|
|
// budget: tsgo charges every node_modules JS hop, so express's
|
|
// body-parser → qs → side-channel → get-intrinsic chain runs out of
|
|
// depth before its leaves load. Root the runtime file this edge
|
|
// lowers to (preflight's own resolution and miss rule) so the leaf
|
|
// joins like a shallow one; complete programs add nothing. Only
|
|
// the shadowed lowering world does this (the authored world checks
|
|
// user code against declarations), and only from package files: a
|
|
// program file's own edges are never deeper than one hop.
|
|
// (resolveBareModule directly: resolveNpmImport7's workspace
|
|
// registration stays with preflight's own walk.)
|
|
if (
|
|
target === null &&
|
|
rootNpmStaticEdges &&
|
|
npmStaticPackageOfPath(sf.fileName) !== null &&
|
|
!isRelativeSpecifier(specifier.text)
|
|
) {
|
|
const spec = specifier.text;
|
|
if (spec.startsWith("#") || canonicalBuiltinModule(spec) !== null) continue;
|
|
const npm = resolveBareModule(
|
|
sf.fileName,
|
|
spec,
|
|
undefined,
|
|
moduleSpecifierKind7(specifier),
|
|
);
|
|
if (
|
|
npm !== null &&
|
|
isNpmStaticPackage(npm.packageName) &&
|
|
(isNodeModulesPath(npm.typesFile) || npm.workspaceDir !== undefined) &&
|
|
isJsSourceFileName(npm.typesFile) &&
|
|
npmStaticProgramFile(program, npm.typesFile) === null
|
|
)
|
|
roots.add(tsgoPath(resolve(npm.typesFile)));
|
|
}
|
|
}
|
|
}
|
|
return [...roots].sort();
|
|
}
|
|
|
|
/** TS 7's ESNext temporal declarations: a lib slice scriptc adds only for
|
|
* programs whose sources name the global (programNamesTemporal7). */
|
|
const TEMPORAL_LIB = "lib.esnext.temporal.d.ts";
|
|
|
|
/** True when a program source (user code or compiled npm JavaScript)
|
|
* contains the temporal global's name as a whole word. A cheap syntactic
|
|
* scan of the implementation files root discovery already materialized; a
|
|
* mention in a comment or string also counts, which only adds the lib
|
|
* slice. */
|
|
function programNamesTemporal7(program: ts.Program): boolean {
|
|
for (const sf of program.getImplementationSourceFiles()) {
|
|
const text = sf.text;
|
|
for (
|
|
let at = text.indexOf(TEMPORAL_GLOBAL);
|
|
at >= 0;
|
|
at = text.indexOf(TEMPORAL_GLOBAL, at + 8)
|
|
) {
|
|
if (!isIdentifierCharAt(text, at - 1) && !isIdentifierCharAt(text, at + 8)) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function isIdentifierCharAt(text: string, index: number): boolean {
|
|
if (index < 0 || index >= text.length) return false;
|
|
const c = text.charCodeAt(index);
|
|
return (
|
|
(c >= 48 && c <= 57) ||
|
|
(c >= 65 && c <= 90) ||
|
|
(c >= 97 && c <= 122) ||
|
|
c === 36 ||
|
|
c === 95 ||
|
|
c > 127
|
|
);
|
|
}
|
|
|
|
/** "require" for a CommonJS edge (`require("x")`, `import x = require("x")`),
|
|
* "import" for every ESM form — the resolution kind preflight uses. */
|
|
function moduleSpecifierKind7(specifier: ts.StringLiteralLike): "import" | "require" {
|
|
const parent = specifier.parent;
|
|
if (ts.isExternalModuleReference(parent)) return "require";
|
|
return ts.isCallExpression(parent) &&
|
|
ts.isIdentifier(parent.expression) &&
|
|
parent.expression.text === "require"
|
|
? "require"
|
|
: "import";
|
|
}
|
|
|
|
/** The program's files with JSX, lowered with the project's jsx settings
|
|
* and each file's pragmas, or the refusal of a setting that emits no calls.
|
|
* TypeScript files (.ts, .mts, .cts) cannot hold JSX; without a jsx setting
|
|
* the checker already rejects any JSX (TS17004). */
|
|
function lowerProgramJsx7(
|
|
program: ts.Program,
|
|
options: ts.Ts7CompilerOptions,
|
|
): { sources: Map<string, LoweredJsx>; refusal: ScrDiagnostic | null } {
|
|
const sources = new Map<string, LoweredJsx>();
|
|
const mode = jsxEmitName(options.jsx as string | number | undefined);
|
|
if (mode === undefined) return { sources, refusal: null };
|
|
for (const sf of program.getImplementationSourceFiles()) {
|
|
if (sf.isDeclarationFile || /\.[mc]?ts$/.test(sf.fileName) || !containsJsx(sf)) continue;
|
|
const at = firstJsx(sf)!;
|
|
const loc = { file: sf.fileName, start: ts.tokenStartOfNode(at), end: at.end };
|
|
if (mode === "preserve" || mode === "react-native") {
|
|
return {
|
|
sources,
|
|
refusal: {
|
|
code: "SC1090",
|
|
message: `JSX with "jsx": "${mode}" is not supported: that setting keeps JSX in the output instead of lowering it to calls`,
|
|
loc,
|
|
hint: 'set "jsx" to "react", "react-jsx" or "react-jsxdev" in the project tsconfig',
|
|
},
|
|
};
|
|
}
|
|
const plan = jsxEmitPlan(
|
|
{
|
|
jsx: mode,
|
|
jsxFactory: options.jsxFactory as string | undefined,
|
|
jsxFragmentFactory: options.jsxFragmentFactory as string | undefined,
|
|
jsxImportSource: options.jsxImportSource as string | undefined,
|
|
reactNamespace: options.reactNamespace as string | undefined,
|
|
},
|
|
jsxPragmas(sf.text),
|
|
);
|
|
sources.set(sf.fileName, lowerJsx(sf, plan));
|
|
}
|
|
return { sources, refusal: null };
|
|
}
|
|
|
|
function loadProgram7(
|
|
host: ts.Ts7Host,
|
|
entryPath: string,
|
|
externalTypes: ReadonlyMap<string, string> = new Map(),
|
|
/** The --npm-static lowering world: root opted-in packages' deep edges. */
|
|
rootNpmStaticEdges = false,
|
|
/** The declaration-typed boundary's projection (decl-projection.ts):
|
|
* answers rewritten user files for the lowering world, or null. */
|
|
projector?: (program: ts.Program) => DeclProjection | null,
|
|
/** Shape inference's typed overlay (shape-inference/overlay.ts):
|
|
* rewritten package JavaScript for the lowering world, or null. */
|
|
overlayer?: (program: ts.Program, round: number) => ReadonlyMap<string, string> | null,
|
|
): LoadResult & { disposeAll: () => void } {
|
|
const config = adoptProjectConfig7(host, entryPath);
|
|
const configuredPaths = config.options["paths"];
|
|
setProjectPathMappings(
|
|
configuredPaths !== undefined && typeof configuredPaths === "object" && configuredPaths !== null
|
|
? (configuredPaths as Record<string, string[]>)
|
|
: null,
|
|
);
|
|
const nodeTypes = config.configFile ? resolveNodeTypes7(entryPath) : null;
|
|
// skipLibCheck is FORCED with @types/node in the program: checking a
|
|
// third-party lib's internals against OUR lib choice (es2025, no dyn) is
|
|
// not scriptc's fence and drowns real diagnostics in hundreds of
|
|
// .d.ts-internal errors. Fence discipline never depended on it: the
|
|
// lowerer checks provenance and forms at every use site.
|
|
let options: ts.Ts7CompilerOptions = nodeTypes
|
|
? { ...config.options, skipLibCheck: true }
|
|
: { ...config.options };
|
|
// --npm-static: opted-in packages' shipped JS must be TYPE-INCLUDED (not
|
|
// just resolved) — without maxNodeModuleJsDepth, node_modules JS types as
|
|
// an implicit-any module (TS7016) and nothing infers. Only flagged
|
|
// compiles pay this; flagless builds keep the exact historical options.
|
|
// Eight levels retain package-internal re-export chains such as Effect's;
|
|
// the former four-level cap left valid bindings outside the checker graph.
|
|
if (npmStaticActive()) options.maxNodeModuleJsDepth = 8;
|
|
// --provenance-sources: the registered entries become tsconfig "paths"
|
|
// so tsgo's OWN resolution of the bare specifiers lands on the same
|
|
// source files the preflight resolver answers — the checker types the
|
|
// driver against the package's real TypeScript, not its shipped .d.ts.
|
|
const provenance = provenancePaths();
|
|
if (provenance !== null || externalTypes.size > 0) {
|
|
const paths: Record<string, string[]> = {
|
|
...(configuredPaths as Record<string, string[]> | undefined),
|
|
// Keep an index-signature source for native lowering when provenance is absent.
|
|
// oxlint-disable-next-line unicorn/no-useless-fallback-in-spread
|
|
...(provenance ?? {}),
|
|
};
|
|
for (const [specifier, declarationPath] of externalTypes) {
|
|
paths[specifier] = [declarationPath];
|
|
}
|
|
options = { ...options, paths };
|
|
}
|
|
// Without @types/node the fallback declarations stand in: the core file,
|
|
// and a Node module's file once a program file names the module
|
|
// (fallbackModuleRoots7). The modules the entry names join the first
|
|
// program.
|
|
const coreRoots = [
|
|
entryPath,
|
|
ambientDtsPath(),
|
|
...(nodeTypes === null
|
|
? [fallbackDtsPath(), ...fallbackModulesNamedIn(host.sourceText(entryPath) ?? "")]
|
|
: [nodeTypes]),
|
|
];
|
|
const programRoots = [...coreRoots];
|
|
let program = host.createProgram([...programRoots, overridesDtsPath()], options);
|
|
try {
|
|
for (let pass = 0; pass < 32; pass++) {
|
|
const candidates = [
|
|
...(nodeTypes === null ? fallbackModuleRoots7(program, host) : []),
|
|
...entryPackageProgramRoots7(program, entryPath, rootNpmStaticEdges),
|
|
...createRequireProgramRoots7(program),
|
|
...forkTargetPaths(program, program.getImplementationSourceFiles()),
|
|
...workerTargetPaths(program, program.getImplementationSourceFiles()),
|
|
];
|
|
const extraRoots = candidates.filter(
|
|
(root, index) => !programRoots.includes(root) && candidates.indexOf(root) === index,
|
|
);
|
|
if (extraRoots.length === 0) break;
|
|
program.dispose();
|
|
for (const root of extraRoots) programRoots.push(root);
|
|
program = host.createProgram([...programRoots, overridesDtsPath()], options);
|
|
if (pass === 31) throw new Error("static program-root discovery did not converge");
|
|
}
|
|
// Node 24 ships no temporal global, but TS 7 types it in an ESNext
|
|
// lib slice. Programs that name it (feature probes such as smol-toml's
|
|
// typeof check) get that slice so they type-check, and lower to Node's
|
|
// answers (typeof "undefined", ReferenceError on use). Every other
|
|
// program keeps the identical lib set: the slice would add globals to
|
|
// the checker's world for everyone.
|
|
if (programNamesTemporal7(program)) {
|
|
program.dispose();
|
|
options = {
|
|
...options,
|
|
lib: [...((options.lib as string[] | undefined) ?? []), TEMPORAL_LIB],
|
|
};
|
|
program = host.createProgram([...programRoots, overridesDtsPath()], options);
|
|
}
|
|
// JSX: the program above checks the files as written; the program the
|
|
// lowering compiles holds them with their JSX lowered.
|
|
const jsx = lowerProgramJsx7(program, options);
|
|
let tscWorld: LoadResult["tscWorld"];
|
|
const declBoundaryNotes: ScrDiagnostic[] = [];
|
|
if (jsx.refusal !== null) {
|
|
config.diags.push(jsx.refusal);
|
|
} else if (jsx.sources.size > 0) {
|
|
const original = program;
|
|
// The project world must read the original text too, so it is built
|
|
// before the rewritten files are served, and only when it is needed.
|
|
const diagnostics = ts.getPreEmitDiagnostics(original, tscGateFileKept);
|
|
const errors = diagnostics.some((d) => d.category === ts.DiagnosticCategory.Error);
|
|
tscWorld = {
|
|
program: original,
|
|
diagnostics,
|
|
projectWorld: errors ? host.createProgram(programRoots, options) : null,
|
|
};
|
|
for (const [file, lowered] of jsx.sources) host.addVirtualFile(file, lowered.text);
|
|
program = host.createProgram([...programRoots, overridesDtsPath()], options);
|
|
} else if (projector !== undefined) {
|
|
// The declaration-typed boundary: user files typed from package
|
|
// declarations (decl-projection.ts). The tsc gate reads the original
|
|
// text; the lowering compiles the rewritten files.
|
|
const projection = projector(program);
|
|
if (projection !== null) {
|
|
const original = program;
|
|
const baseline = new Map<string, number>();
|
|
for (const file of projection.files.keys()) {
|
|
const sf = original.getSourceFile(file);
|
|
if (sf !== undefined) baseline.set(file, original.getSemanticDiagnostics(sf).length);
|
|
}
|
|
for (const [file, text] of projection.files) host.addVirtualFile(file, text);
|
|
host.addVirtualFile(projection.declFile, projection.declText);
|
|
programRoots.push(projection.declFile);
|
|
program = host.createProgram([...programRoots, overridesDtsPath()], options);
|
|
// A rewrite the lowering world disagrees with (new errors) is
|
|
// dropped for that file: the file lowers as it would without it.
|
|
const dropped: string[] = [];
|
|
for (const file of projection.files.keys()) {
|
|
const sf = program.getSourceFile(file);
|
|
if (sf === undefined) continue;
|
|
if (program.getSemanticDiagnostics(sf).length > (baseline.get(file) ?? 0)) {
|
|
dropped.push(file);
|
|
host.addVirtualFile(file, original.getSourceFile(file)!.text);
|
|
}
|
|
}
|
|
if (dropped.length > 0) {
|
|
program.dispose();
|
|
program = host.createProgram([...programRoots, overridesDtsPath()], options);
|
|
for (const file of dropped) declBoundaryNotes.push(declBoundaryDroppedDiag(file));
|
|
}
|
|
if (dropped.length === projection.files.size) {
|
|
program.dispose();
|
|
programRoots.pop();
|
|
program = original;
|
|
} else {
|
|
tscWorld = {
|
|
program: original,
|
|
diagnostics: ts.getPreEmitDiagnostics(original, tscGateFileKept),
|
|
projectWorld: null,
|
|
declWorldFallback: true,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
// Shape inference's overlay: package files typed from proven facts. The
|
|
// tsc gate keeps reading the files as written. loadProgram resets the
|
|
// overlay registry before the lowering world's load.
|
|
const overlayOriginals = new Map<string, string | null>();
|
|
if (overlayer !== undefined && jsx.refusal === null) {
|
|
// Rounds: structural rewrites, then the typed overlay of the rebuilt
|
|
// program (shape-inference/index.ts).
|
|
const timeShapes = process.env["SCRIPTC_DEBUG_SHAPE_TIME"] !== undefined;
|
|
for (let round = 0; round < 3; round++) {
|
|
const t0 = timeShapes ? performance.now() : 0;
|
|
const overlay = overlayer(program, round);
|
|
if (timeShapes)
|
|
process.stderr.write(
|
|
`[shape-time] round ${round} overlayer ${(performance.now() - t0).toFixed(0)} ms\n`,
|
|
);
|
|
if (overlay === null || overlay.size === 0) break;
|
|
const t1 = timeShapes ? performance.now() : 0;
|
|
const before = program;
|
|
// The tsc gate reads the files as written: its diagnostics are
|
|
// taken before the overlay's files replace them on the host, and
|
|
// its project world (load.projectWorld()) is built over the files
|
|
// as written.
|
|
if (tscWorld === undefined)
|
|
tscWorld = {
|
|
program: before,
|
|
diagnostics: ts.getPreEmitDiagnostics(before),
|
|
projectWorld: null,
|
|
declWorldFallback: true,
|
|
};
|
|
for (const file of overlay.keys())
|
|
if (!overlayOriginals.has(file))
|
|
overlayOriginals.set(file, before.getSourceFile(file)?.text ?? null);
|
|
for (const [file, text] of overlay) host.addVirtualFile(file, text);
|
|
program = host.createProgram([...programRoots, overridesDtsPath()], options);
|
|
if (tscWorld.program !== before) before.dispose();
|
|
if (timeShapes)
|
|
process.stderr.write(
|
|
`[shape-time] round ${round} gate+program ${(performance.now() - t1).toFixed(0)} ms\n`,
|
|
);
|
|
if (shapeOverlayDone()) break;
|
|
}
|
|
}
|
|
const entry = program.getSourceFile(entryPath);
|
|
if (!entry) throw new Error(`could not load ${entryPath}`);
|
|
const externalTypeSpecifiersByFile = externalTypeFileClosure7(program, externalTypes);
|
|
let projectWorld: ts.Program | null = null;
|
|
return {
|
|
program,
|
|
entry,
|
|
shapeOverlayState: captureShapeOverlayState(),
|
|
moduleOrder: [],
|
|
configDiags: config.diags,
|
|
...(declBoundaryNotes.length > 0 ? { declBoundaryNotes } : {}),
|
|
typeWorld: {
|
|
nodeFallback: nodeTypes === null,
|
|
projectLib: config.projectLib ?? null,
|
|
forcedStrictNullChecks: config.forcedStrictNullChecks ?? false,
|
|
projectCheckJs: config.checkJs ?? false,
|
|
},
|
|
externalTypes,
|
|
externalTypeSpecifiersByFile,
|
|
...(tscWorld === undefined ? {} : { jsxSources: jsx.sources }),
|
|
...(tscWorld === undefined ? {} : { tscWorld }),
|
|
projectWorld: () =>
|
|
(projectWorld ??= withFilesAsWritten(host, overlayOriginals, () =>
|
|
host.createProgram(programRoots, options),
|
|
)),
|
|
disposeAll: () => {
|
|
try {
|
|
projectWorld?.dispose();
|
|
tscWorld?.projectWorld?.dispose();
|
|
tscWorld?.program.dispose();
|
|
} finally {
|
|
program.dispose();
|
|
}
|
|
},
|
|
};
|
|
} catch (error) {
|
|
program.dispose();
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/** The pipeline surface: ONE tsgo program serves preflight AND the
|
|
* lowering. The caller runs checkPreflight, hands program/entry/
|
|
* moduleOrder straight to lowerToIr, and MUST dispose() when done — the
|
|
* spawned tsgo server must not outlive the compile. */
|
|
export interface ProgramLoadOptions {
|
|
/** Package names whose shipped JavaScript is compiled as program code. */
|
|
npmStatic?: readonly string[];
|
|
/** Exact host module specifiers paired with their declaration paths. */
|
|
externalTypes?: readonly (readonly [string, string])[];
|
|
}
|
|
|
|
/** Runs `build` with the overlay's files served as written (their original
|
|
* texts), then serves the overlay's texts again. */
|
|
function withFilesAsWritten<T>(
|
|
host: ts.Ts7Host,
|
|
originals: ReadonlyMap<string, string | null>,
|
|
build: () => T,
|
|
): T {
|
|
if (originals.size === 0) return build();
|
|
const overlaid = new Map<string, string>();
|
|
for (const [file, text] of originals) {
|
|
const current = host.virtualFileText(file);
|
|
if (current !== undefined) overlaid.set(file, current);
|
|
if (text !== null) host.addVirtualFile(file, text);
|
|
}
|
|
try {
|
|
return build();
|
|
} finally {
|
|
for (const [file, text] of overlaid) host.addVirtualFile(file, text);
|
|
}
|
|
}
|
|
|
|
export function loadProgram(
|
|
entryPath: string,
|
|
services: FrontendServices,
|
|
opts?: ProgramLoadOptions,
|
|
): LoadResult & { services: FrontendServices; dispose: () => void } {
|
|
entryPath = resolve(entryPath);
|
|
const externalTypeEntries = opts?.externalTypes ?? [];
|
|
const externalTypes = new Map<string, string>();
|
|
for (const [specifier, file] of externalTypeEntries) {
|
|
if (!isExactExternalTypeSpecifier(specifier)) {
|
|
throw new TypeError(
|
|
`invalid external type specifier ${JSON.stringify(specifier)} (expected an exact bare package specifier)`,
|
|
);
|
|
}
|
|
if (!/\.d\.(?:ts|mts|cts)$/.test(file)) {
|
|
throw new TypeError(
|
|
`invalid external type declaration ${JSON.stringify(file)} for ${JSON.stringify(specifier)} (expected a .d.ts, .d.mts, or .d.cts file)`,
|
|
);
|
|
}
|
|
const declarationPath = resolve(file);
|
|
if (!trackedFileExists(declarationPath)) {
|
|
throw new TypeError(
|
|
`external type declaration for ${JSON.stringify(specifier)} does not name a readable file: ${declarationPath}`,
|
|
);
|
|
}
|
|
externalTypes.set(specifier, declarationPath);
|
|
}
|
|
// Packages scriptc implements itself, and packages the project marks
|
|
// never loaded (package-shims.ts), never compile their published
|
|
// JavaScript, even when named. A registry entry's `sourceVersions`
|
|
// (chalk 6) compile from their own source.
|
|
configurePackageShims(entryPath);
|
|
resetDeclBoundaryOrigins();
|
|
resetDeclOriginVersions();
|
|
// The project's type patches (type-patches/layer.ts), read by every host
|
|
// this load creates; nothing at all without `scriptc.typePatches`.
|
|
configureTypePatches(entryPath, (path, text, kind) => services.parse(path, text, kind));
|
|
const npmStaticPackages = [...new Set(opts?.npmStatic ?? [])].filter(
|
|
(name) => !npmStaticOmitsPackage(entryPath, name),
|
|
);
|
|
// Resolve authored declaration entries before enabling the JS-only
|
|
// package resolver. Safe overload metadata binds through the runtime
|
|
// entry to one exact implementation file; it never contributes values
|
|
// or executable module edges.
|
|
const declarationOverloads = new Map<string, NpmStaticDeclarationOverloads>();
|
|
const declarationProperties = new Map<string, NpmStaticDeclarationProperties>();
|
|
const projectionOwners = new Map<string, string | null>();
|
|
for (const pkg of npmStaticPackages) {
|
|
const resolved = resolveBareModule(entryPath, pkg, "types-only");
|
|
if (
|
|
resolved === null ||
|
|
resolved.packageName !== pkg ||
|
|
!/\.d\.(?:ts|mts|cts)$/.test(resolved.typesFile)
|
|
) {
|
|
continue;
|
|
}
|
|
const { overloads, properties } = npmStaticDeclarationMetadataOf(services, resolved.typesFile);
|
|
const classNames = new Set([...overloads.keys(), ...properties.keys()]);
|
|
if (classNames.size === 0) continue;
|
|
const runtime = resolveBareModule(entryPath, pkg, "js-only");
|
|
if (runtime === null || !isJsSourceFileName(runtime.typesFile)) continue;
|
|
const runtimeSource = trackedReadFile(runtime.typesFile);
|
|
if (runtimeSource === null) continue;
|
|
const targets = npmStaticRuntimeClassTargets(
|
|
services.parse(runtime.typesFile, runtimeSource, "js"),
|
|
runtimeSource,
|
|
classNames,
|
|
);
|
|
for (const [className, { specifier, localName }] of targets) {
|
|
const methods = overloads.get(className);
|
|
const fields = properties.get(className);
|
|
if (methods === undefined && fields === undefined) continue;
|
|
const target =
|
|
specifier === null
|
|
? runtime.typesFile
|
|
: runtimeReexportTarget(runtime.typesFile, specifier);
|
|
if (target === null) continue;
|
|
const targetPackage = npmPackageNameOf(target);
|
|
const targetNorm = target.split("\\").join("/");
|
|
const insideWorkspace =
|
|
runtime.workspaceDir !== undefined &&
|
|
targetNorm.startsWith(runtime.workspaceDir.split("\\").join("/") + "/");
|
|
if (targetPackage !== pkg && !insideWorkspace) continue;
|
|
const projectionKey = JSON.stringify([targetNorm, localName]);
|
|
const previousOwner = projectionOwners.get(projectionKey);
|
|
if (previousOwner !== undefined && previousOwner !== className) {
|
|
// Two public class declarations naming one runtime class are
|
|
// ambiguous. Keep inference instead of choosing one alias's ABI.
|
|
projectionOwners.set(projectionKey, null);
|
|
const priorMethods = new Map(declarationOverloads.get(targetNorm) ?? []);
|
|
priorMethods.delete(localName);
|
|
declarationOverloads.set(targetNorm, priorMethods);
|
|
const priorFields = new Map(declarationProperties.get(targetNorm) ?? []);
|
|
priorFields.delete(localName);
|
|
declarationProperties.set(targetNorm, priorFields);
|
|
continue;
|
|
}
|
|
projectionOwners.set(projectionKey, className);
|
|
// The safe declaration grammar can name only its own class. Bind
|
|
// that type to the actual runtime class after a named ESM alias.
|
|
const renameSelf = (type: string): string =>
|
|
type
|
|
.split(/([\s[\]<>()|,]+)/)
|
|
.map((name) => (name === className ? localName : name))
|
|
.join("");
|
|
if (methods !== undefined) {
|
|
const byClass = new Map(declarationOverloads.get(targetNorm) ?? []);
|
|
byClass.set(
|
|
localName,
|
|
new Map(
|
|
[...methods].map(([name, signatures]) => [
|
|
name,
|
|
signatures.map((signature) => ({
|
|
parameters: signature.parameters.map((parameter) => ({
|
|
name: parameter.name,
|
|
type: renameSelf(parameter.type),
|
|
optional: parameter.optional,
|
|
})),
|
|
returnType: renameSelf(signature.returnType),
|
|
})),
|
|
]),
|
|
),
|
|
);
|
|
declarationOverloads.set(targetNorm, byClass);
|
|
}
|
|
if (fields !== undefined) {
|
|
const byClass = new Map(declarationProperties.get(targetNorm) ?? []);
|
|
byClass.set(
|
|
localName,
|
|
new Map([...fields].map(([name, type]) => [name, renameSelf(type)])),
|
|
);
|
|
declarationProperties.set(targetNorm, byClass);
|
|
}
|
|
}
|
|
}
|
|
setNpmStaticPackages(npmStaticPackages);
|
|
setNpmStaticDeclarationOverloads(declarationOverloads, declarationProperties);
|
|
// Types only the packages' .d.ts declares keep their declarations
|
|
// (npm-static-type-exports.ts).
|
|
const typeExports = npmStaticTypeExportPlan(services, entryPath, npmStaticPackages);
|
|
setNpmStaticTypeExports(typeExports.copies, typeExports.typedefs);
|
|
// Workspace-package registrations reset per load (same discipline as the
|
|
// npm-static set), then the opted-in names are probed UP FRONT: a
|
|
// workspace-linked opt-in resolves to files whose realpaths carry no
|
|
// node_modules segment, and the tsgo host's fs shadow must already
|
|
// recognize those directories while the PROGRAM is created (hiding the
|
|
// package's declaration twins along its internal relative edges).
|
|
// Non-opted workspace packages register lazily as preflight's own
|
|
// resolver discovers their import edges.
|
|
clearWorkspacePackages();
|
|
for (const pkg of opts?.npmStatic ?? []) {
|
|
const probe = resolveBareModule(entryPath, pkg);
|
|
if (probe?.workspaceDir !== undefined)
|
|
registerWorkspacePackage(probe.packageName, probe.workspaceDir);
|
|
}
|
|
// Declaration-twin hiding is scoped to the entry's own package realm
|
|
// (see projectDtsRuntimeSibling) — set before the host exists so the
|
|
// program never sees an out-of-realm twin hidden.
|
|
setProjectRealm(entryPath);
|
|
// Two fs shadows compose: --npm-static's per-package hiding, and the
|
|
// always-on project declaration-TWIN hiding (a .d.ts beside runtime JS
|
|
// outside node_modules — the classic typed-JS-library entry — must not exist for
|
|
// the checker, so its resolution lands on the JS Node actually loads;
|
|
// resolve.ts answers the same sibling for scriptc's own edges).
|
|
const npmShadow = npmStaticFsShadow(services);
|
|
clearProgramBuiltinAliasCache();
|
|
const fsShadow = {
|
|
readFile: (path: string) =>
|
|
npmShadow?.readFile(path) ?? programBuiltinAliasSource(services, path),
|
|
hideFile: (path: string) =>
|
|
(npmShadow?.hideFile(path) ?? false) || projectDtsRuntimeSibling(path) !== null,
|
|
};
|
|
const host = services.createProgramHost({ cwd: dirname(entryPath), fsShadow });
|
|
let authoredHost: ts.Ts7Host | null = null;
|
|
let authoredLoad: ReturnType<typeof loadProgram7> | null = null;
|
|
const authored = (): ReturnType<typeof loadProgram7> => {
|
|
if (!authoredLoad) {
|
|
authoredHost = services.createProgramHost({ cwd: dirname(entryPath) });
|
|
authoredLoad = loadProgram7(authoredHost, entryPath, externalTypes);
|
|
}
|
|
return authoredLoad;
|
|
};
|
|
const declPackages = npmStaticPackages.filter((pkg) => {
|
|
const resolved = resolveBareModule(entryPath, pkg, "types-only");
|
|
return resolved !== null && /\.d\.(?:ts|mts|cts)$/.test(resolved.typesFile);
|
|
});
|
|
const projector =
|
|
declPackages.length === 0
|
|
? undefined
|
|
: (program: ts.Program): DeclProjection | null => {
|
|
const userFiles = declBoundaryUserFiles(program, declPackages);
|
|
if (userFiles.length === 0) return null;
|
|
let projection: DeclProjection | null;
|
|
try {
|
|
projection = planDeclProjection({
|
|
jsProgram: program,
|
|
declProgram: authored().program,
|
|
userFiles,
|
|
outDir: dirname(entryPath),
|
|
});
|
|
} catch (error) {
|
|
if (process.env["SCRIPTC_DEBUG_DECL_BOUNDARY"] === "1") throw error;
|
|
return null;
|
|
}
|
|
if (process.env["SCRIPTC_DEBUG_DECL_BOUNDARY"] === "1") {
|
|
process.stderr.write(
|
|
`[decl-boundary] ${projection === null ? "no projection" : projection.plan.join("\n[decl-boundary] ")}\n`,
|
|
);
|
|
if (projection !== null) process.stderr.write(projection.declText);
|
|
}
|
|
return projection;
|
|
};
|
|
try {
|
|
// The overlay registry is per process and describes the lowering
|
|
// world's load: a load without the overlay must not see an earlier
|
|
// load's overlay files. The authored world (authored()) is a second load
|
|
// of the same build, which preflight can create after the overlay
|
|
// rounds, so it leaves the registry as this load set it.
|
|
resetShapeOverlay();
|
|
const load = loadProgram7(
|
|
host,
|
|
entryPath,
|
|
externalTypes,
|
|
npmStaticPackages.length > 0,
|
|
projector,
|
|
(npmStaticPackages.length > 0 && shapeOverlayEnabled()) ||
|
|
(jsInferenceEnabled() && process.env["SCRIPTC_SHAPE_OVERLAY"] !== "0")
|
|
? shapeOverlayer
|
|
: undefined,
|
|
);
|
|
// Shape inference, stage S1: a report only (SCRIPTC_DEBUG_SHAPES).
|
|
const shapes = process.env["SCRIPTC_DEBUG_SHAPES"];
|
|
if (shapes !== undefined && shapes !== "" && shapes !== "0" && npmStaticPackages.length > 0)
|
|
reportShapes(load.program, shapes);
|
|
return {
|
|
...load,
|
|
services,
|
|
projectWorld:
|
|
npmStaticPackages.length === 0
|
|
? load.projectWorld
|
|
: () =>
|
|
// Check authored code against the package's published declarations.
|
|
// Runtime bodies still lower from JavaScript in the original world;
|
|
// inference there must not manufacture errors in valid user code.
|
|
authored().projectWorld(),
|
|
dispose: () => {
|
|
try {
|
|
(authoredLoad as ReturnType<typeof loadProgram7> | null)?.disposeAll();
|
|
} finally {
|
|
(authoredHost as ts.Ts7Host | null)?.close();
|
|
try {
|
|
load.disposeAll();
|
|
} finally {
|
|
host.close();
|
|
}
|
|
}
|
|
},
|
|
};
|
|
} catch (error) {
|
|
(authoredLoad as ReturnType<typeof loadProgram7> | null)?.disposeAll();
|
|
(authoredHost as ts.Ts7Host | null)?.close();
|
|
host.close();
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/** User TypeScript files (outside node_modules) that import one of the
|
|
* packages whose declarations the boundary projects from. */
|
|
function declBoundaryUserFiles(program: ts.Program, packages: readonly string[]): string[] {
|
|
const out: string[] = [];
|
|
for (const sf of program.getImplementationSourceFiles()) {
|
|
const name = sf.fileName.split("\\").join("/");
|
|
if (name.includes("/node_modules/") || !/\.[mc]?tsx?$/.test(name)) continue;
|
|
if (name.endsWith(".tsx")) continue; // JSX files: not projected yet
|
|
const text = sf.text;
|
|
if (
|
|
packages.some(
|
|
(pkg) =>
|
|
text.includes(`"${pkg}"`) ||
|
|
text.includes(`'${pkg}'`) ||
|
|
text.includes(`"${pkg}/`) ||
|
|
text.includes(`'${pkg}/`),
|
|
)
|
|
)
|
|
out.push(sf.fileName);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** SC6102 — a projected file the lowering world disagreed with. */
|
|
function declBoundaryDroppedDiag(file: string): ScrDiagnostic {
|
|
return {
|
|
code: "SC6102",
|
|
message:
|
|
"values from npm packages stay checked-dynamic in this file: the types projected from the packages' declarations do not type-check against their JavaScript",
|
|
loc: { file, start: 0, end: 0 },
|
|
};
|
|
}
|
|
|
|
/** tsc diagnostics (syntax + types), the supported-import fence, and the
|
|
* module evaluation order: fills load.moduleOrder (the SourceFiles
|
|
* themselves — the lowering consumes them directly) and returns the
|
|
* preflight diagnostics. Preserve the caller's complete load shape when
|
|
* mutating it, including its owned services and disposal callback. */
|
|
export function checkPreflight<T extends LoadResult>(load: T): ScrDiagnostic[] {
|
|
const {
|
|
diags,
|
|
moduleOrder,
|
|
startupCrash,
|
|
inlineRequireCycleCalls,
|
|
moduleEdges,
|
|
dynamicImportRejections,
|
|
} = preflight7(load);
|
|
load.moduleOrder = moduleOrder;
|
|
load.startupCrash = startupCrash;
|
|
load.inlineRequireCycleCalls = inlineRequireCycleCalls;
|
|
load.moduleEdges = moduleEdges;
|
|
load.dynamicImportRejections = dynamicImportRejections;
|
|
return diags;
|
|
}
|
|
|
|
/* Finding 5: tsgo's strict CommonJS modeling fires checker codes on JS
|
|
* shapes 5.9.3 accepted, and the real hazards those shapes carry are fenced
|
|
* by scriptc's OWN analysis, never by these codes — so for JS FILES ONLY
|
|
* they join the relaxation (TS sources keep the full gate; the order-parity
|
|
* harness holds both lanes to identical verdicts, so a case where 5.9.3
|
|
* itself raises one of these on a JS file would fail there, not slip by):
|
|
* - TS2309 ("export assignment with other exported elements") on the
|
|
* table-then-member pattern — cjsExportDiscardReason names every export
|
|
* Node actually discards.
|
|
* - TS2323 ("cannot redeclare exported variable") on symbol-keyed expando
|
|
* assignments in CJS classes (`this[kSym] = v` under `module.exports =
|
|
* Class`) — 5.9.3 synthesized expando symbols, tsgo trips its
|
|
* redeclaration check instead (the corpus's countdown fixture). */
|
|
// Ordinary JavaScript functions are constructable even when the checker
|
|
// retains only their call signature after a factory return. Heritage
|
|
// lowering separately verifies the actual constructor implementation.
|
|
const TS7_JS_RELAXED_EXTRA: ReadonlySet<number> = new Set([2309, 2323, 2507]);
|
|
|
|
/** Relaxed JavaScript codes that checked JavaScript files keep: values of
|
|
* another type in a typed slot (2322 and its elaborations 2740/2741/2559,
|
|
* 2345, 2769), call arity (2554-2556) and incompatible subclass members
|
|
* (2415). */
|
|
const CHECKED_JS_ENFORCED_CODES: ReadonlySet<number> = new Set([
|
|
2322, 2345, 2415, 2554, 2555, 2556, 2559, 2740, 2741, 2769,
|
|
]);
|
|
|
|
function suppressedJsStrictness7(d: ts.Diagnostic): boolean {
|
|
const file = d.fileName;
|
|
if (!file || !isJsSourceFileName(file)) return false;
|
|
if (JS_RELAXED_TSC_CODES.has(d.code) || TS7_JS_RELAXED_EXTRA.has(d.code)) return true;
|
|
// The 8xxx range is TypeScript's JSDoc-specific diagnostic family
|
|
// (malformed tags, signature/tag mismatches): comment semantics with no
|
|
// runtime meaning in a JS file.
|
|
if (d.code >= 8000 && d.code <= 8999) return true;
|
|
if (JS_ANY_OPERATOR_CODES.has(d.code)) {
|
|
const text = ts.flattenDiagnosticMessageText(d, "\n");
|
|
// A primitive wrapper operand (`new Number(1) * 2`): the operator
|
|
// converts it with ToPrimitive, as for any object.
|
|
return text.includes("'any'") || /'(Number|String|Boolean)'/.test(text);
|
|
}
|
|
// An index write on a String wrapper (`new String("ab")[5] = "q"`): Node
|
|
// adds the property past the end and ignores writes to the read-only
|
|
// index properties (sloppy) or throws (strict), as the object does.
|
|
if (d.code === 2542 && ts.flattenDiagnosticMessageText(d, "\n").includes("type 'String'"))
|
|
return true;
|
|
// The deprecated Buffer(...) call without `new`, which Node's Buffer
|
|
// function accepts (containers/bytes.ts lowers it), and fs.ReadStream(...)
|
|
// and fs.WriteStream(...), which construct their stream
|
|
// (builtins/fs-streams.ts).
|
|
if (d.code === 2348) {
|
|
const text = ts.flattenDiagnosticMessageText(d, "\n");
|
|
return (
|
|
text.includes("'BufferConstructor'") ||
|
|
text.includes("'typeof ReadStream'") ||
|
|
text.includes("'typeof WriteStream'")
|
|
);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** The specifier of a `require("...")` call: the callee is the bare
|
|
* identifier `require` and the single argument a string literal. The one
|
|
* recognizer preflight edges, global collection, and statement lowering
|
|
* share — anything require-shaped it does NOT match (computed specifiers,
|
|
* extra arguments) is not a module edge and fences at its use site. */
|
|
function requireSpecOf7(node: ts.Node): string | null {
|
|
let value = node;
|
|
while (
|
|
ts.isParenthesizedExpression(value) ||
|
|
ts.isAsExpression(value) ||
|
|
ts.isTypeAssertion(value) ||
|
|
ts.isNonNullExpression(value)
|
|
) {
|
|
value = value.expression;
|
|
}
|
|
if (!ts.isCallExpression(value)) return null;
|
|
if (!ts.isIdentifier(value.expression) || value.expression.text !== "require") return null;
|
|
if (value.arguments.nodes.length !== 1) return null;
|
|
const arg = value.arguments.nodes[0]!;
|
|
return ts.isStringLiteral(arg) ? arg.text : null;
|
|
}
|
|
|
|
/** True for statements that are CommonJS require IMPORTS at a module's top
|
|
* level: `const x = require("s")` / `const { a, b } = require("s")`
|
|
* (every declarator a require), and the bare side-effect `require("s");`.
|
|
* These lower to NOTHING — the bindings are alias plumbing (tsc models
|
|
* them as import aliases) and the module edge lives in the order walk. */
|
|
function isRequireStatement7(stmt: ts.Statement): boolean {
|
|
if (ts.isExpressionStatement(stmt)) return requireSpecOf7(stmt.expression) !== null;
|
|
if (!ts.isVariableStatement(stmt)) return false;
|
|
const decls = stmt.declarationList.declarations.nodes;
|
|
return (
|
|
decls.length > 0 &&
|
|
decls.every((d) => d.initializer !== undefined && requireSpecOf7(d.initializer) !== null)
|
|
);
|
|
}
|
|
|
|
/** A require() occurrence of a top-level statement. `split` marks the
|
|
* assignment of a split require binding (splitRequireBindings7): `node`
|
|
* is its require call and `decl` the binding's declaration. */
|
|
interface RequireOccurrence7 {
|
|
spec: string;
|
|
node: ts.Node;
|
|
decl: ts.VariableDeclaration | null;
|
|
split?: true;
|
|
}
|
|
|
|
/** The require() occurrences of one top-level statement: the declaration
|
|
* forms, the bare side-effect call, and the assignment of a split require
|
|
* binding. Empty for everything else. */
|
|
function requiresOf7(stmt: ts.Statement, program: ts.Program): RequireOccurrence7[] {
|
|
if (ts.isExpressionStatement(stmt)) {
|
|
const split = splitRequireOfStatement7(program, stmt);
|
|
if (split !== null)
|
|
return [
|
|
{ spec: requireSpecOf7(split.call)!, node: split.call, decl: split.decl, split: true },
|
|
];
|
|
const spec = requireSpecOf7(stmt.expression);
|
|
return spec !== null ? [{ spec, node: stmt.expression, decl: null }] : [];
|
|
}
|
|
if (!ts.isVariableStatement(stmt)) return [];
|
|
const out: RequireOccurrence7[] = [];
|
|
for (const decl of stmt.declarationList.declarations.nodes) {
|
|
const spec = decl.initializer !== undefined ? requireSpecOf7(decl.initializer) : null;
|
|
if (spec !== null) out.push({ spec, node: decl, decl });
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** CommonJS require bindings split into a declaration and one later
|
|
* assignment (`var m; m = require("./x")`): a top-level `var` or `let`
|
|
* declaration without initializer in a CommonJS file, assigned by exactly one
|
|
* top-level `m = require("<literal>")` statement of the require global over
|
|
* a compiled program module, and written nowhere else. The binding holds the
|
|
* module's exports from the assignment on, as `const m = require("./x")`
|
|
* does, so preflight treats the assignment as that require: its module edge,
|
|
* its cycle admission and its early-read fence. The lowering's recognizer
|
|
* (splitRequireBindingOf in cjs-inline-require.ts) accepts only these
|
|
* bindings. Keyed by declaration, with the assignment's require call. */
|
|
function splitRequireBindings7(
|
|
program: ts.Program,
|
|
sf: ts.SourceFile,
|
|
): ReadonlyMap<ts.VariableDeclaration, ts.CallExpression> {
|
|
const memo = program.analysis.splitRequireBindings;
|
|
const cached = memo.get(sf);
|
|
if (cached !== undefined) return cached;
|
|
const out = new Map<ts.VariableDeclaration, ts.CallExpression>();
|
|
memo.set(sf, out);
|
|
if (sf.isDeclarationFile || !isCjsJsFile7(sf, program) || !sf.text.includes("require"))
|
|
return out;
|
|
const checker = program.getTypeChecker();
|
|
const stmts = sf.statements.nodes;
|
|
const candidates = new Map<
|
|
ts.Symbol,
|
|
{ decl: ts.VariableDeclaration; call: ts.CallExpression | null; assigns: number }
|
|
>();
|
|
for (const stmt of stmts) {
|
|
if (!ts.isVariableStatement(stmt)) continue;
|
|
if ((stmt.declarationList.flags & (ts.NodeFlags.Const | ts.NodeFlags.Using)) !== 0) continue;
|
|
for (const decl of stmt.declarationList.declarations.nodes) {
|
|
if (!ts.isIdentifier(decl.name) || decl.initializer !== undefined) continue;
|
|
const symbol = checker.getSymbolAtLocation(decl.name);
|
|
if (symbol === undefined || symbol.declarations.length !== 1) continue;
|
|
candidates.set(symbol, { decl, call: null, assigns: 0 });
|
|
}
|
|
}
|
|
if (candidates.size === 0) return out;
|
|
// The one top-level `m = ...` statement, whose right side is the require.
|
|
for (const stmt of stmts) {
|
|
if (!ts.isExpressionStatement(stmt) || !ts.isBinaryExpression(stmt.expression)) continue;
|
|
const assign = stmt.expression;
|
|
if (assign.operatorToken.kind !== ts.SyntaxKind.EqualsToken || !ts.isIdentifier(assign.left))
|
|
continue;
|
|
const symbol = checker.getSymbolAtLocation(assign.left);
|
|
const candidate = symbol !== undefined ? candidates.get(symbol) : undefined;
|
|
if (candidate === undefined) continue;
|
|
candidate.assigns++;
|
|
let right = assign.right;
|
|
while (ts.isParenthesizedExpression(right)) right = right.expression;
|
|
if (ts.isCallExpression(right) && splitRequireTarget7(program, sf, right))
|
|
candidate.call = right;
|
|
}
|
|
const names = new Set<string>();
|
|
for (const candidate of candidates.values()) {
|
|
if (candidate.assigns === 1 && candidate.call !== null)
|
|
names.add((candidate.decl.name as ts.Identifier).text);
|
|
}
|
|
if (names.size === 0) return out;
|
|
// Exactly one write in the whole file: the assignment itself.
|
|
const writes = new Map<ts.Symbol, number>();
|
|
ts.walkPreorder(sf, (node) => {
|
|
if (!ts.isIdentifier(node) || !names.has(node.text)) return undefined;
|
|
const parent = node.parent;
|
|
const symbol =
|
|
parent !== undefined && ts.isShorthandPropertyAssignment(parent) && parent.name === node
|
|
? (checker.getShorthandAssignmentValueSymbol(parent) ?? checker.getSymbolAtLocation(node))
|
|
: checker.getSymbolAtLocation(node);
|
|
if (symbol === undefined || !candidates.has(symbol) || !isWriteReference7(node))
|
|
return undefined;
|
|
writes.set(symbol, (writes.get(symbol) ?? 0) + 1);
|
|
return undefined;
|
|
});
|
|
for (const [symbol, candidate] of candidates) {
|
|
if (candidate.assigns === 1 && candidate.call !== null && writes.get(symbol) === 1)
|
|
out.set(candidate.decl, candidate.call);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** True when `call` is a require of the CommonJS global over a compiled
|
|
* program module: not a builtin, a JSON document or a declaration file. */
|
|
function splitRequireTarget7(
|
|
program: ts.Program,
|
|
sf: ts.SourceFile,
|
|
call: ts.CallExpression,
|
|
): boolean {
|
|
const spec = requireSpecOf7(call);
|
|
if (spec === null || canonicalBuiltinModule(spec) !== null) return false;
|
|
if (requireCallBindingKind7(program, call) !== "ambient") return false;
|
|
const dep =
|
|
resolveImport7(program, sf, spec, "require") ?? npmStaticDepSf7(program, sf, spec, "require");
|
|
return dep !== null && !dep.isDeclarationFile && !dep.fileName.endsWith(".json");
|
|
}
|
|
|
|
/** The split require binding a top-level statement assigns, or null. */
|
|
function splitRequireOfStatement7(
|
|
program: ts.Program,
|
|
stmt: ts.Statement,
|
|
): { decl: ts.VariableDeclaration; call: ts.CallExpression } | null {
|
|
if (
|
|
!ts.isExpressionStatement(stmt) ||
|
|
!ts.isBinaryExpression(stmt.expression) ||
|
|
!ts.isSourceFile(stmt.parent)
|
|
)
|
|
return null;
|
|
let right = stmt.expression.right;
|
|
while (ts.isParenthesizedExpression(right)) right = right.expression;
|
|
for (const [decl, call] of splitRequireBindings7(program, stmt.parent))
|
|
if (call === right) return { decl, call };
|
|
return null;
|
|
}
|
|
|
|
/** The top-level statement of a split require binding's assignment, from
|
|
* its require call (`m = require("./x")` or `m = (require("./x"))`). */
|
|
function splitRequireStatementOf7(call: ts.CallExpression): ts.Node | undefined {
|
|
let node: ts.Node = call;
|
|
while (node.parent !== undefined && ts.isParenthesizedExpression(node.parent)) node = node.parent;
|
|
return node.parent?.parent;
|
|
}
|
|
|
|
/** The require call of a split require binding's declaration, or null. */
|
|
function splitRequireCallOf7(
|
|
program: ts.Program,
|
|
decl: ts.VariableDeclaration,
|
|
): ts.CallExpression | null {
|
|
return splitRequireBindings7(program, ts.getSourceFileOfNode(decl)).get(decl) ?? null;
|
|
}
|
|
|
|
/** True when `id` is written: an assignment target (compound and
|
|
* destructuring included), an update operand, or a for-in/of target. */
|
|
function isWriteReference7(id: ts.Identifier): boolean {
|
|
let n: ts.Node = id;
|
|
for (;;) {
|
|
const p: ts.Node | undefined = n.parent;
|
|
if (p === undefined) return false;
|
|
if (ts.isBinaryExpression(p)) {
|
|
const k = p.operatorToken.kind;
|
|
return (
|
|
p.left === n && k >= ts.SyntaxKind.FirstAssignment && k <= ts.SyntaxKind.LastAssignment
|
|
);
|
|
}
|
|
if (ts.isPostfixUnaryExpression(p) || ts.isPrefixUnaryExpression(p)) {
|
|
return (
|
|
(p.operator === ts.SyntaxKind.PlusPlusToken ||
|
|
p.operator === ts.SyntaxKind.MinusMinusToken) &&
|
|
p.operand === n
|
|
);
|
|
}
|
|
if (ts.isForOfStatement(p) || ts.isForInStatement(p)) return p.initializer === n;
|
|
if (
|
|
ts.isParenthesizedExpression(p) ||
|
|
ts.isArrayLiteralExpression(p) ||
|
|
ts.isSpreadElement(p) ||
|
|
ts.isSpreadAssignment(p) ||
|
|
ts.isShorthandPropertyAssignment(p) ||
|
|
ts.isObjectLiteralExpression(p) ||
|
|
(ts.isPropertyAssignment(p) && p.initializer === n)
|
|
) {
|
|
n = p;
|
|
continue;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/** `true`, or `!0`, its minified spelling. */
|
|
function isTrueLiteral(expr: ts.Expression): boolean {
|
|
return (
|
|
expr.kind === ts.SyntaxKind.TrueKeyword ||
|
|
(ts.isPrefixUnaryExpression(expr) &&
|
|
expr.operator === ts.SyntaxKind.ExclamationToken &&
|
|
ts.isNumericLiteral(expr.operand) &&
|
|
expr.operand.text === "0")
|
|
);
|
|
}
|
|
|
|
/** The exact `Object.defineProperty(exports|module.exports, "__esModule",
|
|
* { value: true })` interop stamp (shared by preflight and the no-op lowering);
|
|
* `{ value: !0 }` is the same stamp. */
|
|
export function isEsModuleStamp(expr: ts.Expression): boolean {
|
|
if (!ts.isCallExpression(expr) || expr.questionDotToken !== undefined) return false;
|
|
const callee = expr.expression;
|
|
if (
|
|
!ts.isPropertyAccessExpression(callee) ||
|
|
!ts.isIdentifier(callee.expression) ||
|
|
callee.expression.text !== "Object" ||
|
|
!ts.isIdentifier(callee.name) ||
|
|
callee.name.text !== "defineProperty"
|
|
) {
|
|
return false;
|
|
}
|
|
if (expr.arguments.nodes.length !== 3) return false;
|
|
const [recv, nameArg, desc] = expr.arguments.nodes as unknown as [
|
|
ts.Expression,
|
|
ts.Expression,
|
|
ts.Expression,
|
|
];
|
|
const isExports =
|
|
(ts.isIdentifier(recv) && recv.text === "exports") ||
|
|
(ts.isPropertyAccessExpression(recv) &&
|
|
ts.isIdentifier(recv.expression) &&
|
|
recv.expression.text === "module" &&
|
|
ts.isIdentifier(recv.name) &&
|
|
recv.name.text === "exports");
|
|
if (!isExports) return false;
|
|
if (!ts.isStringLiteral(nameArg) || nameArg.text !== "__esModule") return false;
|
|
if (!ts.isObjectLiteralExpression(desc) || desc.properties.nodes.length !== 1) return false;
|
|
const p = desc.properties.nodes[0]!;
|
|
return (
|
|
ts.isPropertyAssignment(p) &&
|
|
ts.isIdentifier(p.name) &&
|
|
p.name.text === "value" &&
|
|
isTrueLiteral(p.initializer)
|
|
);
|
|
}
|
|
|
|
/** Plain CommonJS prologue writes only publish values; in particular,
|
|
* publishing a hoisted function does not invoke its body. Later runnable
|
|
* statements may call the export or mutate its receiver, and still need
|
|
* the conservative TDZ scan over the whole prefix. */
|
|
function pureCjsExport7(program: ts.Program, stmt: ts.Statement): boolean {
|
|
const cjs = cjsExportAssignmentOf(stmt);
|
|
if (cjs === null) return false;
|
|
const sf = ts.getSourceFileOfNode(stmt);
|
|
const plainMember = (left: ts.Expression): boolean => {
|
|
if (!ts.isPropertyAccessExpression(left) || left.questionDotToken) return false;
|
|
// A replacement export can have setters or non-writable function
|
|
// properties. Only the wrapper's original object gets this exception.
|
|
if (sf.statements.nodes.some((s) => cjsExportAssignmentOf(s)?.kind === "table")) return false;
|
|
if (left.name.text === "__proto__" || left.name.text === "__esModule") return false;
|
|
let receiver = left.expression;
|
|
if (isModuleExportsAccess(receiver)) {
|
|
receiver = receiver.expression;
|
|
return ts.isIdentifier(receiver) && !sourceBinding7(program, receiver);
|
|
}
|
|
return (
|
|
ts.isIdentifier(receiver) && receiver.text === "exports" && !sourceBinding7(program, receiver)
|
|
);
|
|
};
|
|
if (cjs.kind === "member") {
|
|
if (!plainMember(cjs.expr.left)) return false;
|
|
} else {
|
|
const receiver = (cjs.expr.left as ts.PropertyAccessExpression).expression as ts.Identifier;
|
|
if (sourceBinding7(program, receiver)) return false;
|
|
}
|
|
let value = cjs.expr.right;
|
|
// tsc emits chains such as exports.A = exports.B = void 0.
|
|
while (ts.isBinaryExpression(value) && value.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
|
if (!plainMember(value.left)) return false;
|
|
value = value.right;
|
|
}
|
|
if (ts.isVoidExpression(value) && ts.isNumericLiteral(value.expression)) return true;
|
|
if (
|
|
ts.isStringLiteralLike(value) ||
|
|
ts.isNumericLiteral(value) ||
|
|
value.kind === ts.SyntaxKind.TrueKeyword ||
|
|
value.kind === ts.SyntaxKind.FalseKeyword ||
|
|
value.kind === ts.SyntaxKind.NullKeyword
|
|
)
|
|
return true;
|
|
if (!ts.isIdentifier(value)) return false;
|
|
if (value.text === "undefined" && !sourceBinding7(program, value)) return true;
|
|
const checker = program.getTypeChecker();
|
|
const symbol = checker.getSymbolAtLocation(value);
|
|
return (
|
|
symbol !== undefined &&
|
|
symbol.declarations.some(
|
|
(decl) => ts.isFunctionDeclaration(decl) && decl.parent === stmt.parent,
|
|
)
|
|
);
|
|
}
|
|
|
|
function sourceBinding7(program: ts.Program, ident: ts.Identifier): boolean {
|
|
const checker = program.getTypeChecker();
|
|
const symbol = checker.getSymbolAtLocation(ident);
|
|
return (
|
|
symbol !== undefined &&
|
|
symbol.declarations.some(
|
|
(decl) => !ts.getSourceFileOfNode(decl).isDeclarationFile && !ts.isSourceFile(decl),
|
|
)
|
|
);
|
|
}
|
|
|
|
function purePrefixDecl7(decl: ts.VariableDeclaration): boolean {
|
|
const init = decl.initializer;
|
|
return (
|
|
init === undefined ||
|
|
requireSpecOf7(init) !== null ||
|
|
ts.isStringLiteralLike(init) ||
|
|
ts.isNumericLiteral(init) ||
|
|
init.kind === ts.SyntaxKind.TrueKeyword ||
|
|
init.kind === ts.SyntaxKind.FalseKeyword ||
|
|
init.kind === ts.SyntaxKind.NullKeyword
|
|
);
|
|
}
|
|
|
|
/** True for top-level statements that cannot run user code: directives,
|
|
* empty statements, hoisted declarations, require statements themselves,
|
|
* literal-initialized variables, and CommonJS function-export prologues.
|
|
* A require preceded ONLY by these cannot observe its own bindings early;
|
|
* require cycles are checked separately by the module graph fences. */
|
|
function purePrefixStmt7(program: ts.Program, s: ts.Statement): boolean {
|
|
if (ts.isEmptyStatement(s) || ts.isFunctionDeclaration(s)) return true;
|
|
if (ts.isExpressionStatement(s) && ts.isStringLiteral(s.expression)) return true; // directive
|
|
if (isRequireStatement7(s) || splitRequireOfStatement7(program, s) !== null) return true;
|
|
if (isCjsJsFile7(ts.getSourceFileOfNode(s), program)) {
|
|
if (ts.isExpressionStatement(s) && isEsModuleStamp(s.expression)) {
|
|
const call = s.expression as ts.CallExpression;
|
|
const object = (call.expression as ts.PropertyAccessExpression).expression as ts.Identifier;
|
|
let receiver = call.arguments.nodes[0]!;
|
|
if (ts.isPropertyAccessExpression(receiver)) receiver = receiver.expression;
|
|
if (
|
|
!sourceBinding7(program, object) &&
|
|
ts.isIdentifier(receiver) &&
|
|
!sourceBinding7(program, receiver)
|
|
)
|
|
return true;
|
|
}
|
|
if (pureCjsExport7(program, s)) return true;
|
|
}
|
|
if (ts.isVariableStatement(s)) {
|
|
return s.declarationList.declarations.nodes.every(purePrefixDecl7);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** Can code that runs BEFORE the declaration at index `k` reach one of
|
|
* its bindings? Static require aliases and symbol field keys bypass
|
|
* storage reads, so an early use could erase a TDZ error or a var's
|
|
* initial undefined value. Conservative reachability: earlier
|
|
* statements' whole subtrees (arrows and function expressions included)
|
|
* read the binding directly, or reference a hoisted function/class whose
|
|
* body (transitively through other hoisted declarations) reads it — a
|
|
* referenced function value may be invoked immediately by whatever takes
|
|
* it. Asynchronously-scheduled callbacks run after the whole module body,
|
|
* hence after the require: never an early read. Returns the first
|
|
* reachable binding's name, or null when position is provably free.
|
|
* (The CheckerFacade memoizes symbol queries, and 7's client dedupes
|
|
* symbol identity by server handle, so the Map-keyed-by-Symbol discipline
|
|
* carries over from the 5.9.3 original unchanged.) */
|
|
export function bindingEarlyUse7(
|
|
program: ts.Program,
|
|
sf: ts.SourceFile,
|
|
k: number,
|
|
decl: ts.VariableDeclaration,
|
|
precedingDecls: readonly ts.VariableDeclaration[],
|
|
): string | null {
|
|
const checker = program.getTypeChecker();
|
|
const bound: ts.Identifier[] = [];
|
|
if (ts.isIdentifier(decl.name)) bound.push(decl.name);
|
|
else if (ts.isObjectBindingPattern(decl.name)) {
|
|
for (const el of decl.name.elements.nodes)
|
|
if (el.name !== undefined && ts.isIdentifier(el.name)) bound.push(el.name);
|
|
}
|
|
const bindings = new Map<ts.Symbol, string>();
|
|
for (const id of bound) {
|
|
const sym = checker.getSymbolAtLocation(id);
|
|
if (sym) bindings.set(sym, id.text);
|
|
}
|
|
if (bindings.size === 0) return null;
|
|
const stmts = sf.statements.nodes;
|
|
const hoisted = new Map<ts.Symbol, ts.Statement>();
|
|
for (const s of stmts) {
|
|
if ((ts.isFunctionDeclaration(s) || ts.isClassDeclaration(s)) && s.name) {
|
|
const sym = checker.getSymbolAtLocation(s.name);
|
|
if (sym) hoisted.set(sym, s);
|
|
}
|
|
}
|
|
let hit: string | null = null;
|
|
const scanned = new Set<ts.Statement>();
|
|
const work: ts.Statement[] = [];
|
|
const scan = (root: ts.Node): void => {
|
|
// Iterative walk (walkPreorder): a statement's subtree can be
|
|
// pathologically deep (the binderBinaryExpressionStress chains), and
|
|
// this scan must survive it so the nesting fence can answer later.
|
|
ts.walkPreorder(root, (n) => {
|
|
if (ts.isIdentifier(n)) {
|
|
const sym = ts.isShorthandPropertyAssignment(n.parent)
|
|
? checker.getShorthandAssignmentValueSymbol(n.parent)
|
|
: checker.getSymbolAtLocation(n);
|
|
if (sym) {
|
|
const name = bindings.get(sym);
|
|
if (name !== undefined) {
|
|
hit = name;
|
|
return "stop";
|
|
}
|
|
const target = hoisted.get(sym);
|
|
if (target && !scanned.has(target)) {
|
|
scanned.add(target);
|
|
work.push(target);
|
|
}
|
|
}
|
|
}
|
|
return undefined;
|
|
});
|
|
};
|
|
// These are the exact roots the TDZ analysis scans eagerly. Their files
|
|
// are phase-managed already, so warm their deferred identifiers as one
|
|
// symbol-only batch instead of paying one IPC query per occurrence.
|
|
const roots = [
|
|
...stmts.slice(0, k).filter((stmt) => !ts.isFunctionDeclaration(stmt)),
|
|
...precedingDecls,
|
|
];
|
|
checker.prefetchSymbolRoots(roots);
|
|
for (const root of roots) {
|
|
if (hit !== null) break;
|
|
scan(root);
|
|
}
|
|
while (hit === null && work.length > 0) {
|
|
// A scanned reference can make a hoisted declaration's body reachable
|
|
// to this analysis. Batch every currently discovered root before
|
|
// retaining the historical LIFO traversal order.
|
|
checker.prefetchSymbolRoots(work);
|
|
scan(work.pop()!);
|
|
}
|
|
return hit;
|
|
}
|
|
|
|
/** The first reference to a top-level require declaration's bindings that
|
|
* code running before the require completes can reach, or null when no
|
|
* earlier code can run or reach one. Memoized per program: preflight
|
|
* decides the fence and the lowering places the initialization checks
|
|
* from the same answer. */
|
|
function requireEarlyReference7(
|
|
program: ts.Program,
|
|
decl: ts.VariableDeclaration,
|
|
): ts.Identifier | null {
|
|
const memo = program.analysis.requireEarlyReferences;
|
|
const cached = memo.get(decl);
|
|
if (cached !== undefined) return cached;
|
|
let found: ts.Identifier | null = null;
|
|
const sf = ts.getSourceFileOfNode(decl);
|
|
// A split require binding is bound at its assignment statement.
|
|
const splitCall = splitRequireCallOf7(program, decl);
|
|
const stmt = splitCall !== null ? splitRequireStatementOf7(splitCall) : decl.parent?.parent;
|
|
if (
|
|
stmt !== undefined &&
|
|
(ts.isVariableStatement(stmt) || splitCall !== null) &&
|
|
ts.isSourceFile(stmt.parent)
|
|
) {
|
|
const stmts = sf.statements.nodes;
|
|
const k = stmts.indexOf(stmt as ts.Statement);
|
|
const declarations = ts.isVariableStatement(stmt)
|
|
? stmt.declarationList.declarations.nodes
|
|
: [];
|
|
// Earlier declarators run too: `const x = read(), dep = require('./dep')`.
|
|
const precedingDecls = declarations.slice(0, declarations.indexOf(decl));
|
|
let firstRunnable = program.analysis.requireFirstRunnable.get(sf);
|
|
if (firstRunnable === undefined) {
|
|
firstRunnable = stmts.findIndex((s) => !purePrefixStmt7(program, s));
|
|
program.analysis.requireFirstRunnable.set(sf, firstRunnable);
|
|
}
|
|
const prefixCanRun =
|
|
(firstRunnable >= 0 && firstRunnable < k) || precedingDecls.some((d) => !purePrefixDecl7(d));
|
|
if (prefixCanRun) found = requireBindingEarlyUse7(program, sf, k, decl, precedingDecls);
|
|
}
|
|
memo.set(decl, found);
|
|
return found;
|
|
}
|
|
|
|
/** The CommonJS twin of bindingEarlyUse7 for a top-level require binding:
|
|
* the first reference to one of `decl`'s bindings that code running before
|
|
* the require completes can reach, or null. It follows Node's evaluation
|
|
* order instead of the conservative "every earlier statement" scan:
|
|
* - hoisted functions and classes, and the function and class values a
|
|
* var stored before the require, run only when reachable code names
|
|
* them; a class statement runs only its heritage, computed keys,
|
|
* decorators and static initializers;
|
|
* - callbacks handed to timers, microtasks and promise reactions run after
|
|
* the module body, hence after the require;
|
|
* - publishing a value (`module.exports = f`, `exports.n = f`,
|
|
* `Object.defineProperty(exports, "n", { get })`, `F.prototype.m = f`)
|
|
* runs nothing. The published value becomes reachable only when
|
|
* reachable code reads it back: an `exports.n` read reaches the values
|
|
* published as `n`, any other read of the export object reaches all of
|
|
* them, and naming `F` reaches what was stored on `F`. No other module
|
|
* can read this module's exports before its body completes: require
|
|
* cycles keep their SC1016 fence and a compiled module graph has no
|
|
* reflective exports object.
|
|
* Every other function-like met in reachable code counts as reachable
|
|
* because it may be handed to a callee. */
|
|
export function requireBindingEarlyUse7(
|
|
program: ts.Program,
|
|
sf: ts.SourceFile,
|
|
k: number,
|
|
decl: ts.VariableDeclaration,
|
|
precedingDecls: readonly ts.VariableDeclaration[],
|
|
/** A reference of the binding whose early evaluation is known exact. */
|
|
exactEarlyRead?: (reference: ts.Identifier) => boolean,
|
|
): ts.Identifier | null {
|
|
const checker = program.getTypeChecker();
|
|
const bindings = new Set<ts.Symbol>();
|
|
for (const id of boundNames7(decl.name)) {
|
|
const sym = checker.getSymbolAtLocation(id);
|
|
if (sym) bindings.add(sym);
|
|
}
|
|
if (bindings.size === 0) return null;
|
|
// A split require binding's assignment writes the binding; it reads nothing.
|
|
let splitWrite: ts.Node | null = splitRequireCallOf7(program, decl);
|
|
while (splitWrite !== null && ts.isParenthesizedExpression(splitWrite.parent))
|
|
splitWrite = splitWrite.parent;
|
|
splitWrite =
|
|
splitWrite !== null && ts.isBinaryExpression(splitWrite.parent) ? splitWrite.parent.left : null;
|
|
const stmts = sf.statements.nodes;
|
|
const cjs = isCjsJsFile7(sf, program);
|
|
const symbolOf = (id: ts.Identifier): ts.Symbol | undefined => {
|
|
const parent = id.parent;
|
|
if (parent !== undefined && ts.isShorthandPropertyAssignment(parent) && parent.name === id) {
|
|
return checker.getShorthandAssignmentValueSymbol(parent);
|
|
}
|
|
return checker.getSymbolAtLocation(id);
|
|
};
|
|
// Stored callables and the scans that skip them.
|
|
const units = new Map<ts.Symbol, ts.Node[]>();
|
|
const stored = new Set<ts.Node>();
|
|
const addUnit = (name: ts.Identifier, node: ts.Node): void => {
|
|
const sym = checker.getSymbolAtLocation(name);
|
|
if (sym === undefined) return;
|
|
stored.add(node);
|
|
const list = units.get(sym);
|
|
if (list) list.push(node);
|
|
else units.set(sym, [node]);
|
|
};
|
|
const storedCallables = (e: ts.Expression, out: ts.Node[]): void => {
|
|
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
|
if (ts.isArrowFunction(e) || ts.isFunctionExpression(e) || ts.isClassExpression(e)) {
|
|
out.push(e);
|
|
} else if (ts.isObjectLiteralExpression(e)) {
|
|
for (const p of e.properties.nodes) {
|
|
if (
|
|
(ts.isMethodDeclaration(p) ||
|
|
ts.isGetAccessorDeclaration(p) ||
|
|
ts.isSetAccessorDeclaration(p)) &&
|
|
!ts.isComputedPropertyName(p.name)
|
|
) {
|
|
out.push(p);
|
|
} else if (ts.isPropertyAssignment(p) && !ts.isComputedPropertyName(p.name)) {
|
|
storedCallables(p.initializer, out);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
stmts.forEach((s, i) => {
|
|
if ((ts.isFunctionDeclaration(s) || ts.isClassDeclaration(s)) && s.name !== undefined) {
|
|
addUnit(s.name, s);
|
|
} else if (i < k && ts.isVariableStatement(s)) {
|
|
for (const d of s.declarationList.declarations.nodes) {
|
|
if (!ts.isIdentifier(d.name) || d.initializer === undefined) continue;
|
|
const out: ts.Node[] = [];
|
|
storedCallables(d.initializer, out);
|
|
for (const node of out) addUnit(d.name, node);
|
|
}
|
|
}
|
|
});
|
|
// Callbacks that only run as later jobs never run before the require:
|
|
// the timers, microtasks and promise reactions. (Event listeners can run
|
|
// synchronously from an emit, so they keep the conservative rule.)
|
|
const laterJob = (callee: ts.Expression): boolean => {
|
|
while (ts.isParenthesizedExpression(callee)) callee = callee.expression;
|
|
const name = ts.isIdentifier(callee)
|
|
? callee.text
|
|
: ts.isPropertyAccessExpression(callee)
|
|
? callee.name.text
|
|
: null;
|
|
return (
|
|
name !== null &&
|
|
(LATER_JOB_BUILTINS7.has(name) || name === "nextTick") &&
|
|
laterJobCallee7(checker, callee)
|
|
);
|
|
};
|
|
if (!hasTopLevelAwait7(sf)) {
|
|
const callbacks = (e: ts.Expression): void => {
|
|
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
|
if (ts.isArrowFunction(e) || ts.isFunctionExpression(e)) stored.add(e);
|
|
};
|
|
for (const s of stmts.slice(0, k)) {
|
|
ts.walkPreorder(s, (node) => {
|
|
if ((ts.isCallExpression(node) || ts.isNewExpression(node)) && laterJob(node.expression)) {
|
|
for (const arg of node.arguments?.nodes ?? []) callbacks(arg);
|
|
}
|
|
return undefined;
|
|
});
|
|
}
|
|
}
|
|
const isWrapperName = (e: ts.Node, name: string): e is ts.Identifier =>
|
|
ts.isIdentifier(e) && e.text === name && !sourceBinding7(program, e);
|
|
/** `this` at the module's top level: the original export object. */
|
|
const isModuleThis = (e: ts.Node): boolean => {
|
|
if (e.kind !== ts.SyntaxKind.ThisKeyword) return false;
|
|
for (let p = e.parent; p !== undefined && !ts.isSourceFile(p); p = p.parent) {
|
|
if (ts.isFunctionLike(p) && !ts.isArrowFunction(p)) return false;
|
|
if (ts.isClassExpression(p) || ts.isClassDeclaration(p)) return false;
|
|
}
|
|
return true;
|
|
};
|
|
const isExportObject = (e: ts.Expression): boolean =>
|
|
isWrapperName(e, "exports") ||
|
|
isModuleThis(e) ||
|
|
(isModuleExportsAccess(e) && isWrapperName(e.expression, "module"));
|
|
const replacesExports = stmts.some((st) => cjsExportAssignmentOf(st)?.kind === "table");
|
|
// Published values: by export name ("*" for a whole replacement), and by
|
|
// the stored callable whose properties hold them.
|
|
const exportLinks = new Map<string, ts.Node[]>();
|
|
const ownerLinks = new Map<ts.Symbol, ts.Node[]>();
|
|
const reachedKeys = new Set<string>();
|
|
const writtenKeys = new Set<string>();
|
|
const reachedUnits = new Set<ts.Node>();
|
|
const work: ts.Node[] = [];
|
|
let hit: ts.Identifier | null = null;
|
|
const reachUnit = (node: ts.Node): void => {
|
|
if (reachedUnits.has(node)) return;
|
|
reachedUnits.add(node);
|
|
work.push(node);
|
|
};
|
|
const reachSymbol = (sym: ts.Symbol): void => {
|
|
for (const node of units.get(sym) ?? []) reachUnit(node);
|
|
for (const node of ownerLinks.get(sym) ?? []) reachUnit(node);
|
|
};
|
|
let allReached = false;
|
|
const reachKey = (key: string | null): void => {
|
|
if (key === null) {
|
|
allReached = true;
|
|
for (const list of exportLinks.values()) for (const node of list) reachUnit(node);
|
|
return;
|
|
}
|
|
reachedKeys.add(key);
|
|
for (const name of [key, "*"]) for (const node of exportLinks.get(name) ?? []) reachUnit(node);
|
|
};
|
|
const keyReached = (key: string): boolean =>
|
|
allReached || reachedKeys.has(key) || (key === "*" && reachedKeys.size > 0);
|
|
/** A read of the export object: which published names it can observe. */
|
|
const exportRead = (node: ts.Expression): void => {
|
|
let use: ts.Node = node;
|
|
while (ts.isParenthesizedExpression(use.parent)) use = use.parent;
|
|
const parent = use.parent;
|
|
// A truthiness test reads no member (tsc's `this && this.__helper`).
|
|
if (
|
|
ts.isBinaryExpression(parent) &&
|
|
parent.left === use &&
|
|
parent.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken
|
|
) {
|
|
return;
|
|
}
|
|
if (ts.isTypeOfExpression(parent)) return;
|
|
if (ts.isPropertyAccessExpression(parent) && parent.expression === use) {
|
|
let target: ts.Node = parent;
|
|
while (ts.isParenthesizedExpression(target.parent)) target = target.parent;
|
|
const assignment = target.parent;
|
|
if (
|
|
ts.isBinaryExpression(assignment) &&
|
|
assignment.left === target &&
|
|
assignment.operatorToken.kind === ts.SyntaxKind.EqualsToken
|
|
) {
|
|
// A plain write reads no member. It can still run a setter: one
|
|
// published for that name (now or later), or a static one of a
|
|
// replacing class.
|
|
writtenKeys.add(parent.name.text);
|
|
for (const node of exportLinks.get(parent.name.text) ?? []) reachUnit(node);
|
|
for (const node of exportLinks.get("*") ?? []) {
|
|
if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) reachUnit(node);
|
|
}
|
|
return;
|
|
}
|
|
reachKey(parent.name.text);
|
|
return;
|
|
}
|
|
if (
|
|
ts.isElementAccessExpression(parent) &&
|
|
parent.expression === use &&
|
|
ts.isStringLiteralLike(parent.argumentExpression)
|
|
) {
|
|
reachKey(parent.argumentExpression.text);
|
|
return;
|
|
}
|
|
reachKey(null);
|
|
};
|
|
const scan = (root: ts.Node): void => {
|
|
ts.walkPreorder(root, (n) => {
|
|
if (hit !== null) return "stop";
|
|
if (n !== root && stored.has(n)) {
|
|
if (ts.isClassExpression(n) || ts.isClassDeclaration(n)) {
|
|
for (const part of classSyncParts7(n)) scan(part);
|
|
}
|
|
return "skip";
|
|
}
|
|
// A self-require answers this module's partially built exports.
|
|
if (cjs && ts.isCallExpression(n)) {
|
|
const spec = requireSpecOf7(n);
|
|
if (spec !== null && resolveImport7(program, sf, spec, "require") === sf) reachKey(null);
|
|
}
|
|
if (cjs && n.kind === ts.SyntaxKind.ThisKeyword && isModuleThis(n)) {
|
|
exportRead(n as ts.Expression);
|
|
return undefined;
|
|
}
|
|
if (!ts.isIdentifier(n)) return undefined;
|
|
if (inTypePosition7(n)) return undefined;
|
|
const named = n.parent;
|
|
if (
|
|
(ts.isPropertyAccessExpression(named) ||
|
|
ts.isPropertyAssignment(named) ||
|
|
ts.isMethodDeclaration(named) ||
|
|
ts.isGetAccessorDeclaration(named) ||
|
|
ts.isSetAccessorDeclaration(named) ||
|
|
ts.isPropertyDeclaration(named)) &&
|
|
named.name === n
|
|
) {
|
|
return undefined;
|
|
}
|
|
// A declaration's own name reads nothing.
|
|
if (named !== undefined && declaredName7(named) === n) return undefined;
|
|
if (cjs && isWrapperName(n, "exports")) {
|
|
exportRead(n);
|
|
return undefined;
|
|
}
|
|
if (cjs && isWrapperName(n, "module")) {
|
|
const parent = n.parent;
|
|
if (ts.isPropertyAccessExpression(parent) && parent.expression === n) {
|
|
if (parent.name.text === "exports") exportRead(parent);
|
|
return "skip";
|
|
}
|
|
reachKey(null);
|
|
return undefined;
|
|
}
|
|
const sym = symbolOf(n);
|
|
if (sym === undefined) return undefined;
|
|
if (bindings.has(sym)) {
|
|
if (n === splitWrite || exactEarlyRead?.(n) === true) return undefined;
|
|
hit = n;
|
|
return "stop";
|
|
}
|
|
reachSymbol(sym);
|
|
return undefined;
|
|
});
|
|
};
|
|
/** Links a published value: evaluating it reads identifiers, but the
|
|
* callables it names run only once they are read back. */
|
|
const publish = (value: ts.Expression, link: (node: ts.Node) => void): void => {
|
|
while (ts.isParenthesizedExpression(value)) value = value.expression;
|
|
if (ts.isIdentifier(value)) {
|
|
const sym = symbolOf(value);
|
|
if (sym !== undefined && bindings.has(sym)) {
|
|
hit = value;
|
|
return;
|
|
}
|
|
if (sym !== undefined) {
|
|
// What was stored on the value stays reachable through it.
|
|
for (const node of [...(units.get(sym) ?? []), ...(ownerLinks.get(sym) ?? [])]) link(node);
|
|
}
|
|
return;
|
|
}
|
|
if (ts.isFunctionExpression(value) || ts.isArrowFunction(value)) {
|
|
link(value);
|
|
return;
|
|
}
|
|
if (ts.isClassExpression(value)) {
|
|
for (const part of classSyncParts7(value)) scan(part);
|
|
link(value);
|
|
return;
|
|
}
|
|
if (ts.isObjectLiteralExpression(value)) {
|
|
for (const p of value.properties.nodes) {
|
|
if (ts.isShorthandPropertyAssignment(p)) {
|
|
const sym = checker.getShorthandAssignmentValueSymbol(p);
|
|
if (sym !== undefined && bindings.has(sym)) hit = p.name as ts.Identifier;
|
|
else if (sym !== undefined) {
|
|
for (const node of [...(units.get(sym) ?? []), ...(ownerLinks.get(sym) ?? [])]) {
|
|
link(node);
|
|
}
|
|
}
|
|
} else if (ts.isPropertyAssignment(p) && !ts.isComputedPropertyName(p.name)) {
|
|
publish(p.initializer, link);
|
|
} else if (
|
|
(ts.isMethodDeclaration(p) ||
|
|
ts.isGetAccessorDeclaration(p) ||
|
|
ts.isSetAccessorDeclaration(p)) &&
|
|
!ts.isComputedPropertyName(p.name)
|
|
) {
|
|
link(p);
|
|
} else scan(p);
|
|
}
|
|
return;
|
|
}
|
|
if (
|
|
ts.isStringLiteralLike(value) ||
|
|
ts.isNumericLiteral(value) ||
|
|
value.kind === ts.SyntaxKind.TrueKeyword ||
|
|
value.kind === ts.SyntaxKind.FalseKeyword ||
|
|
value.kind === ts.SyntaxKind.NullKeyword ||
|
|
(ts.isVoidExpression(value) && ts.isNumericLiteral(value.expression))
|
|
) {
|
|
return;
|
|
}
|
|
scan(value);
|
|
// Whatever an arbitrary expression evaluates to is opaque: treat it as
|
|
// reachable through the target too (scan already reached its parts).
|
|
};
|
|
/** Where a publication stores: an export name, or a stored callable. */
|
|
type Target = { kind: "key"; key: string } | { kind: "owner"; owner: ts.Symbol };
|
|
const targetOf = (left: ts.Expression): Target | null => {
|
|
if (isModuleExportsAccess(left) && isWrapperName(left.expression, "module")) {
|
|
return { kind: "key", key: "*" };
|
|
}
|
|
const names: string[] = [];
|
|
let e: ts.Expression = left;
|
|
for (;;) {
|
|
if (ts.isPropertyAccessExpression(e) && !e.questionDotToken) {
|
|
if (isExportObject(e)) break;
|
|
names.unshift(e.name.text);
|
|
e = e.expression;
|
|
} else if (
|
|
ts.isElementAccessExpression(e) &&
|
|
!e.questionDotToken &&
|
|
ts.isStringLiteralLike(e.argumentExpression)
|
|
) {
|
|
names.unshift(e.argumentExpression.text);
|
|
e = e.expression;
|
|
} else break;
|
|
}
|
|
if (names.length === 0) return null;
|
|
if (isExportObject(e)) {
|
|
// __proto__ and the interop marker are not plain data slots, and a
|
|
// member of a replaced export may be a read-only function property:
|
|
// those writes keep the conservative scan.
|
|
if (names[0] === "__proto__" || names[0] === "__esModule" || replacesExports) return null;
|
|
return { kind: "key", key: names[0]! };
|
|
}
|
|
if (ts.isIdentifier(e)) {
|
|
const sym = symbolOf(e);
|
|
if (sym !== undefined && units.has(sym) && !bindings.has(sym))
|
|
return { kind: "owner", owner: sym };
|
|
}
|
|
return null;
|
|
};
|
|
const linkTo = (target: Target, node: ts.Node): void => {
|
|
if (target.kind === "key") {
|
|
const replacingClass =
|
|
target.key === "*" &&
|
|
writtenKeys.size > 0 &&
|
|
(ts.isClassDeclaration(node) || ts.isClassExpression(node));
|
|
if (keyReached(target.key) || writtenKeys.has(target.key) || replacingClass) {
|
|
reachUnit(node);
|
|
}
|
|
const list = exportLinks.get(target.key);
|
|
if (list) list.push(node);
|
|
else exportLinks.set(target.key, [node]);
|
|
} else {
|
|
if ((units.get(target.owner) ?? []).some((u) => reachedUnits.has(u))) reachUnit(node);
|
|
if ((units.get(target.owner) ?? []).includes(node)) return;
|
|
const list = ownerLinks.get(target.owner);
|
|
if (list === undefined) ownerLinks.set(target.owner, [node]);
|
|
else if (!list.includes(node)) list.push(node);
|
|
}
|
|
};
|
|
/** A top-level publication statement, handled in place; false for any
|
|
* other statement. */
|
|
const publication = (s: ts.Statement): boolean => {
|
|
if (!cjs || !ts.isExpressionStatement(s)) return false;
|
|
const e = s.expression;
|
|
if (ts.isBinaryExpression(e) && e.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
|
const targets: Target[] = [];
|
|
let value: ts.Expression = e;
|
|
while (
|
|
ts.isBinaryExpression(value) &&
|
|
value.operatorToken.kind === ts.SyntaxKind.EqualsToken
|
|
) {
|
|
const target = targetOf(value.left);
|
|
if (target === null) return false;
|
|
targets.push(target);
|
|
value = value.right;
|
|
}
|
|
publish(value, (node) => {
|
|
for (const t of targets) linkTo(t, node);
|
|
});
|
|
return true;
|
|
}
|
|
// Object.defineProperty(exports, "name", { get() {...}, value: f, ... })
|
|
if (
|
|
ts.isCallExpression(e) &&
|
|
ts.isPropertyAccessExpression(e.expression) &&
|
|
isWrapperName(e.expression.expression, "Object") &&
|
|
e.expression.name.text === "defineProperty" &&
|
|
e.arguments.nodes.length === 3
|
|
) {
|
|
const [recv, key, desc] = e.arguments.nodes as unknown as [
|
|
ts.Expression,
|
|
ts.Expression,
|
|
ts.Expression,
|
|
];
|
|
if (!isExportObject(recv) || !ts.isStringLiteralLike(key)) return false;
|
|
if (!ts.isObjectLiteralExpression(desc)) return false;
|
|
const keyTarget: Target = { kind: "key", key: key.text };
|
|
const link = (node: ts.Node): void => linkTo(keyTarget, node);
|
|
for (const p of desc.properties.nodes) {
|
|
if (
|
|
(ts.isMethodDeclaration(p) || ts.isPropertyAssignment(p)) &&
|
|
ts.isIdentifier(p.name) &&
|
|
(p.name.text === "get" || p.name.text === "set")
|
|
) {
|
|
if (ts.isMethodDeclaration(p)) link(p);
|
|
else publish(p.initializer, link);
|
|
} else if (
|
|
ts.isPropertyAssignment(p) &&
|
|
ts.isIdentifier(p.name) &&
|
|
p.name.text === "value"
|
|
) {
|
|
publish(p.initializer, link);
|
|
} else scan(p);
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
const drain = (): void => {
|
|
while (hit === null && work.length > 0) {
|
|
const unit = work.pop()!;
|
|
checker.prefetchSymbolRoots([unit]);
|
|
scan(unit);
|
|
}
|
|
};
|
|
const roots = stmts.slice(0, k);
|
|
checker.prefetchSymbolRoots([...roots, ...precedingDecls]);
|
|
for (const s of roots) {
|
|
if (hit !== null) break;
|
|
if (ts.isFunctionDeclaration(s)) continue;
|
|
if (ts.isClassDeclaration(s)) {
|
|
for (const part of classSyncParts7(s)) scan(part);
|
|
} else if (!publication(s)) {
|
|
scan(s);
|
|
}
|
|
drain();
|
|
}
|
|
for (const d of precedingDecls) {
|
|
if (hit !== null) break;
|
|
scan(d);
|
|
drain();
|
|
}
|
|
return hit;
|
|
}
|
|
|
|
/** Bare `require("...")` EXPRESSION STATEMENTS anywhere below the top
|
|
* level of a file — inside top-level ifs/blocks/loops and inside function
|
|
* bodies (the lazy-require idiom). No bindings, pure side-effect loads. */
|
|
function nestedBareRequiresOf7(sf: ts.SourceFile): ts.CallExpression[] {
|
|
const out: ts.CallExpression[] = [];
|
|
// Iterative walk (walkPreorder): this sweep sees every JS file whole,
|
|
// including pathologically deep expression chains that must live to be
|
|
// fenced by name, not crash a recursive visit.
|
|
ts.walkPreorder(sf, (n) => {
|
|
if (
|
|
ts.isExpressionStatement(n) &&
|
|
!ts.isSourceFile(n.parent) &&
|
|
ts.isCallExpression(n.expression) &&
|
|
requireSpecOf7(n.expression) !== null
|
|
) {
|
|
out.push(n.expression);
|
|
}
|
|
});
|
|
return out;
|
|
}
|
|
|
|
/** INLINE `require("...")` calls in expression position anywhere in a file
|
|
* (`require("./x").f`, `require("debug")("ns")`, `module.exports =
|
|
* require("./y")`, `{ v: require("./v") }`): each evaluates its module at
|
|
* that position, exactly like a bare require statement. Top-level require
|
|
* statements and bare require statements are collected by their own
|
|
* scans; a require that initializes a nested declaration keeps its
|
|
* lowering fence and is not an edge. */
|
|
function inlineRequiresOf7(sf: ts.SourceFile): ts.CallExpression[] {
|
|
const out: ts.CallExpression[] = [];
|
|
ts.walkPreorder(sf, (n) => {
|
|
if (!ts.isCallExpression(n) || requireSpecOf7(n) === null) return;
|
|
let outer: ts.Node = n;
|
|
while (
|
|
outer.parent !== undefined &&
|
|
(ts.isParenthesizedExpression(outer.parent) ||
|
|
ts.isAsExpression(outer.parent) ||
|
|
ts.isTypeAssertion(outer.parent) ||
|
|
ts.isNonNullExpression(outer.parent))
|
|
) {
|
|
outer = outer.parent;
|
|
}
|
|
const parent = outer.parent;
|
|
if (parent !== undefined && ts.isExpressionStatement(parent)) return;
|
|
if (parent !== undefined && ts.isVariableDeclaration(parent) && parent.initializer === outer)
|
|
return;
|
|
out.push(n);
|
|
});
|
|
return out;
|
|
}
|
|
|
|
type RequireCallBindingKind7 = "ambient" | "createRequire" | "source";
|
|
|
|
function stripRequireCasts7(expr: ts.Expression): ts.Expression {
|
|
let cur = expr;
|
|
while (
|
|
ts.isParenthesizedExpression(cur) ||
|
|
ts.isAsExpression(cur) ||
|
|
ts.isTypeAssertion(cur) ||
|
|
ts.isNonNullExpression(cur)
|
|
) {
|
|
cur = cur.expression;
|
|
}
|
|
return cur;
|
|
}
|
|
|
|
/** The supported-builtin provenance of an imported identifier, narrowed to
|
|
* the one member this preflight distinction needs. Mirrors the lowering's
|
|
* builtinImportOf alias handling so a re-export facade remains supported. */
|
|
function isCreateRequireImport7(program: ts.Program, ident: ts.Identifier): boolean {
|
|
const checker = program.getTypeChecker();
|
|
const symbol = checker.getSymbolAtLocation(ident);
|
|
const decl = symbol ? symbol.declarations[0] : undefined;
|
|
if (decl === undefined || !ts.isImportSpecifier(decl)) return false;
|
|
const importDecl = decl.parent?.parent?.parent;
|
|
if (ts.isImportDeclaration(importDecl) && ts.isStringLiteral(importDecl.moduleSpecifier)) {
|
|
const module = canonicalBuiltinModule(importDecl.moduleSpecifier.text);
|
|
const member = decl.propertyName?.text ?? decl.name.text;
|
|
if (module === "module" && member === "createRequire") return true;
|
|
}
|
|
if (symbol !== undefined && (symbol.flags & ts.SymbolFlags.Alias) !== 0) {
|
|
const target = checker.getAliasedSymbol(symbol);
|
|
const targetDecl = target.declarations[0];
|
|
if (targetDecl !== undefined && ts.getSourceFileOfNode(targetDecl).isDeclarationFile) {
|
|
for (
|
|
let p: ts.Node | undefined = targetDecl.parent;
|
|
p !== undefined && !ts.isSourceFile(p);
|
|
p = p.parent
|
|
) {
|
|
if (ts.isModuleDeclaration(p) && ts.isStringLiteral(p.name)) {
|
|
return (
|
|
canonicalBuiltinModule(p.name.text) === "module" &&
|
|
ts.symbolName(target) === "createRequire"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** True only for createRequire bindings the lowering can erase. Mutable
|
|
* declaration syntax needs a whole-file proof: no writes or escapes, and
|
|
* no code before initialization can reach the binding. */
|
|
export function isCreateRequireBinding7(program: ts.Program, callee: ts.Identifier): boolean {
|
|
const checker = program.getTypeChecker();
|
|
const symbol = checker.getSymbolAtLocation(callee);
|
|
const decl = symbol
|
|
? symbol.declarations.find((d): d is ts.VariableDeclaration => ts.isVariableDeclaration(d))
|
|
: undefined;
|
|
if (
|
|
decl === undefined ||
|
|
decl.initializer === undefined ||
|
|
!ts.isVariableDeclarationList(decl.parent)
|
|
) {
|
|
return false;
|
|
}
|
|
return (
|
|
isCreateRequireBaseCall7(program, decl.initializer) &&
|
|
((decl.parent.flags & ts.NodeFlags.Const) !== 0 ||
|
|
stableCreateRequireBindingReason7(program, decl) === null)
|
|
);
|
|
}
|
|
|
|
/** How a use of a createRequire loader lowers:
|
|
* - "static": a literal call or a literal resolution query, which erases
|
|
* the loader value.
|
|
* - "runtime": in JavaScript source, a call with a computed specifier, a
|
|
* require.resolve call with a computed request, or `resolve` read as a
|
|
* value. The runtime resolves the specifier like Node
|
|
* (module-resolution.ts, scr_require.c).
|
|
* - "deferred": in JavaScript source, inside the try block of a try
|
|
* statement in the same function, the other shapes: spread arguments,
|
|
* require.resolve with another argument count, and `resolve.paths` with
|
|
* a computed request or read as a value. The call throws a runtime
|
|
* fence, and the resolve.paths value is a function that throws one when
|
|
* called (module-resolution.ts), so the statement's catch clause handles
|
|
* them (packages that look for optional native addons).
|
|
* - "computed": the "runtime" uses in TypeScript source, and the
|
|
* "deferred" shapes outside a try block.
|
|
* - "escaping": the loader itself as a value, writes, optional calls, and
|
|
* other members. */
|
|
const COMPUTED_CREATE_REQUIRE_REASON =
|
|
"its createRequire result is used with a computed specifier in TypeScript, or with spread arguments or resolve.paths outside a try block (static requires need literal specifiers)";
|
|
|
|
type CreateRequireUse7 = "static" | "runtime" | "deferred" | "computed" | "escaping";
|
|
|
|
function createRequireUse7(expr: ts.Expression): CreateRequireUse7 {
|
|
const use = createRequireUseShape7(expr);
|
|
return use === "runtime" && !isJsSourceFile(ts.getSourceFileOfNode(expr)) ? "computed" : use;
|
|
}
|
|
|
|
function createRequireUseShape7(expr: ts.Expression): CreateRequireUse7 {
|
|
let use: ts.Node = expr;
|
|
while (ts.isParenthesizedExpression(use.parent)) use = use.parent;
|
|
let member = false;
|
|
let paths = false;
|
|
if (
|
|
ts.isPropertyAccessExpression(use.parent) &&
|
|
use.parent.expression === use &&
|
|
use.parent.name.text === "resolve" &&
|
|
!use.parent.questionDotToken
|
|
) {
|
|
use = use.parent;
|
|
member = true;
|
|
if (
|
|
ts.isPropertyAccessExpression(use.parent) &&
|
|
use.parent.expression === use &&
|
|
use.parent.name.text === "paths" &&
|
|
!use.parent.questionDotToken
|
|
) {
|
|
use = use.parent;
|
|
paths = true;
|
|
}
|
|
}
|
|
const call = use.parent;
|
|
if (ts.isCallExpression(call) && call.expression === use) {
|
|
if (call.questionDotToken) return "escaping";
|
|
const args = call.arguments.nodes;
|
|
if (args.length === 1 && ts.isStringLiteralLike(args[0]!)) return "static";
|
|
if (
|
|
!paths &&
|
|
!args.some((arg) => ts.isSpreadElement(arg)) &&
|
|
!(member && (args.length === 0 || args.length > 2))
|
|
)
|
|
return "runtime";
|
|
} else if (!member || isAssignmentTarget7(use)) {
|
|
return "escaping";
|
|
} else if (!paths) {
|
|
return "runtime";
|
|
}
|
|
return insideTryBlock7(use) ? "deferred" : "computed";
|
|
}
|
|
|
|
function isAssignmentTarget7(node: ts.Node): boolean {
|
|
const parent = node.parent;
|
|
return (
|
|
ts.isBinaryExpression(parent) &&
|
|
parent.left === node &&
|
|
parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
|
|
parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment
|
|
);
|
|
}
|
|
|
|
/** True when the identifier is a reference position that WRITES: an
|
|
* assignment (or compound-assignment) target, a `++`/`--` operand, a
|
|
* for-in/for-of head, or an element of a destructuring pattern — through
|
|
* parentheses and pattern containers, exactly the shapes a sloppy-mode
|
|
* implicit global can be introduced by. */
|
|
export function isJsWriteTarget7(node: ts.Node): boolean {
|
|
let cur: ts.Node = node;
|
|
for (;;) {
|
|
const parent: ts.Node | undefined = cur.parent;
|
|
if (parent === undefined) return false;
|
|
if (ts.isParenthesizedExpression(parent)) {
|
|
cur = parent;
|
|
continue;
|
|
}
|
|
if (ts.isBinaryExpression(parent))
|
|
return (
|
|
parent.left === cur &&
|
|
parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
|
|
parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment
|
|
);
|
|
if (ts.isPrefixUnaryExpression(parent) || ts.isPostfixUnaryExpression(parent))
|
|
return (
|
|
parent.operand === cur &&
|
|
(parent.operator === ts.SyntaxKind.PlusPlusToken ||
|
|
parent.operator === ts.SyntaxKind.MinusMinusToken)
|
|
);
|
|
if (ts.isForInStatement(parent) || ts.isForOfStatement(parent))
|
|
return parent.initializer === cur;
|
|
if (ts.isArrayLiteralExpression(parent) || ts.isObjectLiteralExpression(parent)) {
|
|
cur = parent;
|
|
continue;
|
|
}
|
|
if (
|
|
(ts.isShorthandPropertyAssignment(parent) && parent.name === cur) ||
|
|
(ts.isPropertyAssignment(parent) && parent.initializer === cur) ||
|
|
((ts.isSpreadElement(parent) || ts.isSpreadAssignment(parent)) && parent.expression === cur)
|
|
) {
|
|
cur = parent;
|
|
continue;
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/** Nothing binds `node`: the tsc had no symbol, and a shorthand pattern's
|
|
* name has no VALUE binding either (its property symbol is the target, not a
|
|
* declaration of the name being written). */
|
|
function undeclaredJsName7(
|
|
checker: ReturnType<ts.Program["getTypeChecker"]>,
|
|
node: ts.Identifier,
|
|
): boolean {
|
|
const parent = node.parent;
|
|
if (parent !== undefined && ts.isShorthandPropertyAssignment(parent) && parent.name === node)
|
|
return checker.getShorthandAssignmentValueSymbol(parent) === undefined;
|
|
return checker.getSymbolAtLocation(node) === undefined;
|
|
}
|
|
|
|
/** A `.js` WRITE to a name nothing declares (plan D-2/D-5). Sloppy mode
|
|
* resolves such a write by creating a property on Node's global object —
|
|
* process-wide state a native binary has no object for — so the program is
|
|
* refused at build time, under SC1091, whose text names the Node-island
|
|
* route those semantics need. A READ of the same name is admitted instead:
|
|
* it throws Node's own catchable `ReferenceError` at the access
|
|
* (lowering/expressions/identifiers.ts). TypeScript input never reaches
|
|
* either path — an undeclared name is a compile error there. */
|
|
function jsUndeclaredWriteDiags(
|
|
program: ts.Program,
|
|
files: readonly ts.SourceFile[],
|
|
): ScrDiagnostic[] {
|
|
const diags: ScrDiagnostic[] = [];
|
|
const checker = program.getTypeChecker();
|
|
for (const sf of files) {
|
|
if (!isJsSourceFileName(sf.fileName)) continue;
|
|
const stack: ts.Node[] = [sf];
|
|
while (stack.length > 0) {
|
|
const node = stack.pop()!;
|
|
// An empty name is a parse-recovery artifact (`@import x = require(…)`
|
|
// in a JSDoc prologue leaves one behind), never a reference a program
|
|
// writes; the syntax error that created it is already reported.
|
|
if (
|
|
ts.isIdentifier(node) &&
|
|
node.text !== "" &&
|
|
isJsWriteTarget7(node) &&
|
|
undeclaredJsName7(checker, node)
|
|
)
|
|
diags.push(
|
|
unsupportedDiag(
|
|
"SC1091",
|
|
{ file: sf.fileName, start: node.getStart(sf), end: node.end },
|
|
`writing the undeclared name '${node.text}' in a JavaScript source`,
|
|
),
|
|
);
|
|
ts.forEachChild(node, (child) => {
|
|
stack.push(child);
|
|
return undefined;
|
|
});
|
|
}
|
|
}
|
|
return diags;
|
|
}
|
|
|
|
/** True when `node` is inside the try block of a try statement in the same
|
|
* function (a catch clause or finally block does not count). A class field
|
|
* initializer runs when the class is instantiated, so it is a boundary too. */
|
|
function insideTryBlock7(node: ts.Node): boolean {
|
|
for (let child = node, parent = node.parent; parent; child = parent, parent = parent.parent) {
|
|
if (ts.isTryStatement(parent) && parent.tryBlock === child) return true;
|
|
if (
|
|
ts.isFunctionLike(parent) ||
|
|
ts.isClassStaticBlockDeclaration(parent) ||
|
|
ts.isPropertyDeclaration(parent)
|
|
)
|
|
return false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function stableCreateRequireBindingReason7(
|
|
program: ts.Program,
|
|
decl: ts.VariableDeclaration,
|
|
): string | null {
|
|
const cache = program.analysis.createRequireReasons;
|
|
const cached = cache.get(decl);
|
|
if (cached !== undefined) return cached;
|
|
const check = (): string | null => {
|
|
const stmt = decl.parent?.parent;
|
|
if (
|
|
!ts.isIdentifier(decl.name) ||
|
|
!ts.isVariableStatement(stmt) ||
|
|
!ts.isSourceFile(stmt.parent)
|
|
) {
|
|
return "its mutable createRequire binding is outside the module's top level";
|
|
}
|
|
const checker = program.getTypeChecker();
|
|
const symbol = checker.getSymbolAtLocation(decl.name);
|
|
if (!symbol || symbol.declarations.length !== 1)
|
|
return "its createRequire binding is redeclared";
|
|
if ((ts.getCombinedModifierFlags(decl) & ts.ModifierFlags.Export) !== 0)
|
|
return "its createRequire result escapes through an exported binding";
|
|
const sf = ts.getSourceFileOfNode(decl);
|
|
const refs = identifierOccurrences7(sf, decl.name.text);
|
|
checker.prefetchSymbolNodesExact(refs);
|
|
for (const ref of refs) {
|
|
if (ref === decl.name || inTypePosition7(ref)) continue;
|
|
let target = ts.isShorthandPropertyAssignment(ref.parent)
|
|
? checker.getShorthandAssignmentValueSymbol(ref.parent)
|
|
: checker.getSymbolAtLocation(ref);
|
|
if (target && (target.flags & ts.SymbolFlags.Alias) !== 0)
|
|
target = checker.getAliasedSymbol(target);
|
|
if (target !== symbol) continue;
|
|
const use = createRequireUse7(ref);
|
|
if (use === "computed") return COMPUTED_CREATE_REQUIRE_REASON;
|
|
if (use === "escaping") return "its createRequire binding is reassigned or escapes";
|
|
}
|
|
const preceding = stmt.declarationList.declarations.nodes.slice(
|
|
0,
|
|
stmt.declarationList.declarations.nodes.indexOf(decl),
|
|
);
|
|
if (
|
|
bindingEarlyUse7(program, sf, sf.statements.nodes.indexOf(stmt), decl, preceding) !== null
|
|
) {
|
|
return "its createRequire binding can be used before initialization";
|
|
}
|
|
return null;
|
|
};
|
|
const reason = check();
|
|
cache.set(decl, reason);
|
|
return reason;
|
|
}
|
|
|
|
function isCreateRequireBaseCall7(program: ts.Program, expr: ts.Expression): boolean {
|
|
const init = stripRequireCasts7(expr);
|
|
if (
|
|
!ts.isCallExpression(init) ||
|
|
init.questionDotToken !== undefined ||
|
|
!ts.isIdentifier(init.expression) ||
|
|
!isCreateRequireImport7(program, init.expression) ||
|
|
init.arguments.nodes.length !== 1
|
|
) {
|
|
return false;
|
|
}
|
|
const base = stripRequireCasts7(init.arguments.nodes[0]!);
|
|
if (ts.isIdentifier(base) && base.text === "__filename") return !sourceBinding7(program, base);
|
|
return (
|
|
ts.isPropertyAccessExpression(base) &&
|
|
base.questionDotToken === undefined &&
|
|
ts.isMetaProperty(base.expression) &&
|
|
base.expression.keywordToken === ts.SyntaxKind.ImportKeyword &&
|
|
(base.name.text === "url" || base.name.text === "filename")
|
|
);
|
|
}
|
|
|
|
/** Refuse require indirection the lowering cannot erase, while
|
|
* admitting literal calls through the same stable/inline shapes as program
|
|
* code. Inspect symbols, not names: nested shadows are unrelated bindings. */
|
|
function npmStaticModuleImportReason7(
|
|
program: ts.Program,
|
|
stmt: ts.ImportDeclaration,
|
|
): string | null {
|
|
const clause = stmt.importClause;
|
|
if (!clause) return null;
|
|
const bindings = clause.namedBindings;
|
|
if (clause.name || (bindings && !ts.isNamedImports(bindings))) {
|
|
return "its node:module namespace or default import has no static createRequire usage proof";
|
|
}
|
|
if (!bindings || !ts.isNamedImports(bindings)) return null;
|
|
const checker = program.getTypeChecker();
|
|
const sf = ts.getSourceFileOfNode(stmt);
|
|
for (const specifier of bindings.elements.nodes) {
|
|
if ((specifier.propertyName?.text ?? specifier.name.text) !== "createRequire") continue;
|
|
const imported = checker.getSymbolAtLocation(specifier.name);
|
|
if (!imported) return "its createRequire import could not be resolved";
|
|
checker.prefetchSymbolNodesExact(identifierOccurrences7(sf, specifier.name.text));
|
|
let reason: string | null = null;
|
|
const checkUse = (expr: ts.Expression): void => {
|
|
const use = createRequireUse7(expr);
|
|
if (use === "computed") reason = COMPUTED_CREATE_REQUIRE_REASON;
|
|
else if (use === "escaping")
|
|
reason = "its createRequire result escapes (static requires need literal specifiers)";
|
|
};
|
|
ts.walkPreorder(sf, (node) => {
|
|
if (
|
|
reason ||
|
|
!ts.isIdentifier(node) ||
|
|
node.text !== specifier.name.text ||
|
|
node === specifier.name ||
|
|
checker.getSymbolAtLocation(node) !== imported
|
|
)
|
|
return;
|
|
const call = node.parent;
|
|
if (
|
|
!ts.isCallExpression(call) ||
|
|
call.expression !== node ||
|
|
!isCreateRequireBaseCall7(program, call)
|
|
) {
|
|
reason =
|
|
"its createRequire import is used outside a supported call based on the current file";
|
|
return;
|
|
}
|
|
const decl = call.parent;
|
|
if (
|
|
ts.isVariableDeclaration(decl) &&
|
|
decl.initializer === call &&
|
|
ts.isIdentifier(decl.name)
|
|
) {
|
|
if ((ts.getCombinedModifierFlags(decl) & ts.ModifierFlags.Export) !== 0) {
|
|
reason = "its createRequire result escapes through an exported binding";
|
|
return;
|
|
}
|
|
if (!isCreateRequireBinding7(program, decl.name)) {
|
|
reason = stableCreateRequireBindingReason7(program, decl);
|
|
return;
|
|
}
|
|
const sym = checker.getSymbolAtLocation(decl.name);
|
|
const name = decl.name.text;
|
|
checker.prefetchSymbolNodesExact(identifierOccurrences7(sf, name));
|
|
ts.walkPreorder(sf, (ref) => {
|
|
if (
|
|
ts.isIdentifier(ref) &&
|
|
ref.text === name &&
|
|
ref !== decl.name &&
|
|
checker.getSymbolAtLocation(ref) === sym
|
|
)
|
|
checkUse(ref);
|
|
});
|
|
} else {
|
|
checkUse(call);
|
|
}
|
|
});
|
|
if (reason) return reason;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** Classifies a require-shaped call as Node's ambient CommonJS global, the
|
|
* one supported createRequire source binding, or another source binding.
|
|
* The scanners still collect createRequire calls because they carry static
|
|
* module edges; other source bindings get a pointed fence before lowering
|
|
* can mistake them for module syntax. */
|
|
function requireCallBindingKind7(program: ts.Program, node: ts.Node): RequireCallBindingKind7 {
|
|
const raw = ts.isVariableDeclaration(node) ? node.initializer : node;
|
|
const call = raw && ts.isExpression(raw) ? stripRequireCasts7(raw) : raw;
|
|
if (!call || !ts.isCallExpression(call) || !ts.isIdentifier(call.expression)) return "ambient";
|
|
const checker = program.getTypeChecker();
|
|
const symbol = checker.getSymbolAtLocation(call.expression);
|
|
if (symbol === undefined) return "ambient";
|
|
const decls = symbol.declarations;
|
|
if (decls.every((d) => ts.getSourceFileOfNode(d).isDeclarationFile)) return "ambient";
|
|
return isCreateRequireBinding7(program, call.expression) ? "createRequire" : "source";
|
|
}
|
|
|
|
function esmRequireFeature7(kind: Exclude<RequireCallBindingKind7, "createRequire">): string {
|
|
return kind === "ambient"
|
|
? "require() in an ES module (Node throws ReferenceError — use import)"
|
|
: "calls through a source binding named 'require' (this spelling is reserved for CommonJS/createRequire module edges — rename the binding)";
|
|
}
|
|
|
|
const CJS_WRAPPER_NAMES = new Set(["require", "module", "exports", "__dirname", "__filename"]);
|
|
|
|
function bindingDeclaresCjsWrapper7(name: ts.BindingName): ts.Identifier | null {
|
|
if (ts.isIdentifier(name)) return CJS_WRAPPER_NAMES.has(name.text) ? name : null;
|
|
for (const el of name.elements.nodes) {
|
|
if (el.name === undefined) continue;
|
|
const hit = bindingDeclaresCjsWrapper7(el.name);
|
|
if (hit !== null) return hit;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function insideFunctionLike7(node: ts.Node): boolean {
|
|
for (let p = node.parent; p !== undefined && !ts.isSourceFile(p); p = p.parent) {
|
|
if (ts.isFunctionLike(p)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** The first syntax marker Node's ambiguous-file detector recognizes. This
|
|
* cannot use ts.isExternalModule: moduleDetection=force deliberately gives
|
|
* EVERY executable file module-local checker scope, including CommonJS.
|
|
* Runtime classification remains Node's own narrower question: value-level
|
|
* import/export forms, import.meta, top-level await/for-await, or a top-level
|
|
* lexical declaration that would redeclare a CommonJS wrapper binding. */
|
|
function nodeEsmSyntaxMarker7(sf: ts.SourceFile): ts.Node | null {
|
|
for (const stmt of sf.statements.nodes) {
|
|
if (ts.isImportDeclaration(stmt) && !erasedTypeOnlyImport(stmt)) return stmt;
|
|
if (ts.isExportDeclaration(stmt) && !stmt.isTypeOnly) {
|
|
return stmt;
|
|
}
|
|
if (ts.isExportAssignment(stmt)) return stmt;
|
|
if (
|
|
ts.canHaveModifiers(stmt) &&
|
|
ts.getModifiers(stmt)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) === true
|
|
) {
|
|
return stmt;
|
|
}
|
|
if (
|
|
ts.isVariableStatement(stmt) &&
|
|
(stmt.declarationList.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) !== 0
|
|
) {
|
|
for (const decl of stmt.declarationList.declarations.nodes) {
|
|
const hit = bindingDeclaresCjsWrapper7(decl.name);
|
|
if (hit !== null) return hit;
|
|
}
|
|
}
|
|
if (
|
|
ts.isClassDeclaration(stmt) &&
|
|
stmt.name !== undefined &&
|
|
CJS_WRAPPER_NAMES.has(stmt.name.text)
|
|
) {
|
|
return stmt.name;
|
|
}
|
|
}
|
|
|
|
let found: ts.Node | null = null;
|
|
ts.walkPreorder(sf, (node) => {
|
|
if (ts.isMetaProperty(node) && node.keywordToken === ts.SyntaxKind.ImportKeyword) {
|
|
found = node;
|
|
return "stop";
|
|
}
|
|
// Only await syntax needs an ancestor walk. Doing this for every node
|
|
// makes a deeply nested expression take quadratic time to classify.
|
|
if (
|
|
(ts.isAwaitExpression(node) ||
|
|
(ts.isForOfStatement(node) && node.awaitModifier !== undefined)) &&
|
|
!insideFunctionLike7(node)
|
|
) {
|
|
found = node;
|
|
return "stop";
|
|
}
|
|
return undefined;
|
|
});
|
|
return found;
|
|
}
|
|
|
|
/** True when Node would treat this file as an ES MODULE (never defining
|
|
* require/__dirname there): fixed extensions first, then an explicit
|
|
* nearest-package type, then Node's syntax markers for ambiguous .js/.ts
|
|
* files. TypeScript's external-module bit is forced for checker scoping and
|
|
* therefore intentionally plays no part in this runtime decision. */
|
|
function isNodeEsmFile7(sf: ts.SourceFile, program?: ts.Program): boolean {
|
|
const cached = program?.analysis.nodeEsmFiles.get(sf);
|
|
if (cached !== undefined) return cached;
|
|
let result: boolean;
|
|
if (sf.fileName.endsWith(".cjs") || sf.fileName.endsWith(".cts")) {
|
|
result = false;
|
|
} else if (sf.fileName.endsWith(".mjs") || sf.fileName.endsWith(".mts")) {
|
|
result = true;
|
|
} else {
|
|
const packageType = nearestPackageType(sf.fileName);
|
|
result =
|
|
packageType === "module"
|
|
? true
|
|
: packageType === "commonjs"
|
|
? false
|
|
: nodeEsmSyntaxMarker7(sf) !== null;
|
|
}
|
|
program?.analysis.nodeEsmFiles.set(sf, result);
|
|
return result;
|
|
}
|
|
|
|
/** A JavaScript source file Node treats as CommonJS (not an ES module):
|
|
* ESM namespace/default machinery must not claim it — its export surface
|
|
* is module.exports, read through the CJS interop paths and their own
|
|
* fences. */
|
|
function isCjsJsFile7(sf: ts.SourceFile, program?: ts.Program): boolean {
|
|
return isJsSourceFileName(sf.fileName) && !isNodeEsmFile7(sf, program);
|
|
}
|
|
|
|
/** The exported spelling of isCjsJsFile7 (the lowering's gate for
|
|
* module.exports surfaces — `ts.isExternalModule` is NOT the CJS test:
|
|
* tsgo marks CJS files with export assignments as external modules). */
|
|
export function isCjsJsFile(sf: ts.SourceFile, program?: ts.Program): boolean {
|
|
return isCjsJsFile7(sf, program);
|
|
}
|
|
|
|
/** The self-import TDZ fences: for `import { x } from "<self>"` (default
|
|
* and namespace forms included), any TOP-LEVEL reference lexically above
|
|
* the aliased declaration observes Node's temporal dead zone — the direct
|
|
* spelling of the same read is tsc's own TS2448, which the import
|
|
* indirection dodges, so preflight restores the bar with SC1030. Function
|
|
* aliases hoist (initialized at link — readable anywhere); references
|
|
* inside function-like bodies follow the direct spelling's existing
|
|
* policy (tsc accepts those too) and are not fenced here. Namespace
|
|
* bindings fence per MEMBER access (`self.x` above x's declaration);
|
|
* the bare namespace binding itself is never TDZ. */
|
|
function selfImportTdzFences7(
|
|
program: ts.Program,
|
|
sf: ts.SourceFile,
|
|
clause: ts.ImportClause,
|
|
diags: ScrDiagnostic[],
|
|
): void {
|
|
const checker = program.getTypeChecker();
|
|
const insideFunction = (node: ts.Node): boolean => {
|
|
for (let p = node.parent; p !== undefined && !ts.isSourceFile(p); p = p.parent) {
|
|
if (ts.isFunctionLike(p)) return true;
|
|
}
|
|
return false;
|
|
};
|
|
// The aliased declaration's TDZ horizon: the position after which reads
|
|
// are initialized. Hoisted functions have none.
|
|
const tdzEndOf = (aliased: ts.Symbol): number | null => {
|
|
const d = aliased.valueDeclaration ?? aliased.declarations[0];
|
|
if (d === undefined || ts.getSourceFileOfNode(d) !== sf) return null;
|
|
if (ts.isFunctionDeclaration(d)) return null;
|
|
return d.end;
|
|
};
|
|
const fence = (ref: ts.Identifier, name: string): void => {
|
|
diags.push(
|
|
unsupportedDiag(
|
|
"SC1030",
|
|
{ file: sf.fileName, start: ts.tokenStartOfNode(ref), end: ref.end },
|
|
`the self-import binding '${name}' read above its declaration (the import aliases this module's OWN binding — Node initializes it when the declaration executes, so this read throws ReferenceError; move the read below the declaration)`,
|
|
),
|
|
);
|
|
};
|
|
const checkNamed = (bindingName: ts.Identifier): void => {
|
|
const sym = checker.getSymbolAtLocation(bindingName);
|
|
if (sym === undefined || !(sym.flags & ts.SymbolFlags.Alias)) return;
|
|
checker.prefetchSymbolNodesExact(identifierOccurrences7(sf, bindingName.text));
|
|
const end = tdzEndOf(checker.getAliasedSymbol(sym));
|
|
if (end === null) return;
|
|
const visit = (node: ts.Node): void => {
|
|
if (
|
|
ts.isIdentifier(node) &&
|
|
node !== bindingName &&
|
|
node.text === bindingName.text &&
|
|
ts.tokenStartOfNode(node) < end &&
|
|
!insideFunction(node) &&
|
|
checker.getSymbolAtLocation(node) === sym
|
|
) {
|
|
fence(node, bindingName.text);
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
};
|
|
visit(sf);
|
|
};
|
|
if (clause.name !== undefined) checkNamed(clause.name);
|
|
if (clause.namedBindings !== undefined && ts.isNamedImports(clause.namedBindings)) {
|
|
for (const el of clause.namedBindings.elements.nodes) {
|
|
if (!el.isTypeOnly) checkNamed(el.name);
|
|
}
|
|
}
|
|
if (clause.namedBindings !== undefined && ts.isNamespaceImport(clause.namedBindings)) {
|
|
const nsName = clause.namedBindings.name;
|
|
const nsSym = checker.getSymbolAtLocation(nsName);
|
|
if (nsSym === undefined) return;
|
|
checker.prefetchSymbolNodesExact(identifierOccurrences7(sf, nsName.text));
|
|
const visit = (node: ts.Node): void => {
|
|
if (
|
|
ts.isPropertyAccessExpression(node) &&
|
|
ts.isIdentifier(node.expression) &&
|
|
node.expression.text === nsName.text &&
|
|
ts.isIdentifier(node.name) &&
|
|
!insideFunction(node) &&
|
|
checker.getSymbolAtLocation(node.expression) === nsSym
|
|
) {
|
|
const memberSym = checker.getSymbolAtLocation(node.name);
|
|
if (memberSym !== undefined) {
|
|
const end = tdzEndOf(memberSym);
|
|
if (end !== null && ts.tokenStartOfNode(node) < end)
|
|
fence(node.expression, `${nsName.text}.${node.name.text}`);
|
|
}
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
};
|
|
visit(sf);
|
|
}
|
|
}
|
|
|
|
/** True when every reference to the namespace-import binding `nsName`
|
|
* (outside its own declaration) is exactly a bare expression statement's
|
|
* expression — the shape whose lowering is Node's no-op. A self-import's
|
|
* references all live in the importing file, so one file-local scan
|
|
* answers completely. */
|
|
function nsBindingUsesAreBareStatements7(
|
|
program: ts.Program,
|
|
sf: ts.SourceFile,
|
|
nsName: ts.Identifier,
|
|
): boolean {
|
|
const checker = program.getTypeChecker();
|
|
const bindingSym = checker.getSymbolAtLocation(nsName);
|
|
if (bindingSym === undefined) return false;
|
|
checker.prefetchSymbolNodesExact(identifierOccurrences7(sf, nsName.text));
|
|
let bareOnly = true;
|
|
const visit = (node: ts.Node): void => {
|
|
if (!bareOnly) return;
|
|
if (
|
|
ts.isIdentifier(node) &&
|
|
node !== nsName &&
|
|
node.text === nsName.text &&
|
|
checker.getSymbolAtLocation(node) === bindingSym
|
|
) {
|
|
if (
|
|
!(
|
|
node.parent !== undefined &&
|
|
ts.isExpressionStatement(node.parent) &&
|
|
node.parent.expression === node
|
|
)
|
|
) {
|
|
bareOnly = false;
|
|
return;
|
|
}
|
|
}
|
|
ts.forEachChild(node, visit);
|
|
};
|
|
visit(sf);
|
|
return bareOnly;
|
|
}
|
|
|
|
/* ── benign-cycle admission ──────────────────────────────────────────────
|
|
* Node runs legal ESM cycles all over the ecosystem: the revisited module
|
|
* is a cache hit, and the only hazard is a READ of a binding before its
|
|
* declaration executed (TDZ ReferenceError for let/const/class, a stale
|
|
* `undefined` for var). Cycles where such a read is REACHABLE are admitted
|
|
* by the exact initialization rule (exactCycleRefusal7 below), which
|
|
* models the TDZ with initialization checks. A cycle needs no checks when
|
|
* partial initialization is provably unobservable — the DECLARATION-ONLY
|
|
* INIT WINDOW rule:
|
|
*
|
|
* 1. Every module of the cycle cluster (the strongly-connected component
|
|
* — any member may be mid-initialization while another's top level
|
|
* runs, and every module that can even NAME a cluster binding during
|
|
* the window is itself in the cluster) is an ES module whose top
|
|
* level is INERT: declarations and expressions that cannot call into
|
|
* user code (nonInertTopLevel7).
|
|
* 2. Every use of each binding the cycle-closing statement imports sits
|
|
* in a DEFERRED position — inside a function body, a parameter
|
|
* default, or an instance-field initializer (inDeferredPosition7).
|
|
*
|
|
* Together: cluster bindings are read only from function bodies, and no
|
|
* top level in the window can call a function, so the first possible read
|
|
* happens after every member initialized — the guarded %init calls then
|
|
* reproduce Node's cache-hit order exactly, and Node's TDZ/stale-slot
|
|
* outcomes are unreachable. */
|
|
|
|
/** A module edge, carrying what cycle admission needs when it turns out
|
|
* to close one. `stmt` is the importing/re-exporting statement; absent
|
|
* for require() edges, which are never admitted (their CJS home fails the
|
|
* cluster check anyway). */
|
|
export interface CycleEdge {
|
|
dep: ts.SourceFile;
|
|
stmt?: ts.ImportDeclaration | ts.ExportDeclaration;
|
|
/** An inline require() in a deferred position (a function body): Node
|
|
* evaluates the dependency when that code runs, after this module's own
|
|
* top level, so the edge orders the dependency after the module and
|
|
* never closes a cycle through the module's initialization. */
|
|
lazy?: true;
|
|
/** The inline require() call behind the edge (inline edges only). */
|
|
call?: ts.CallExpression;
|
|
}
|
|
|
|
/** True when the reference at `node` can only evaluate AFTER the module
|
|
* graph finished initializing: inside a function-like BODY, inside a
|
|
* parameter (default initializers evaluate at call time), or inside an
|
|
* INSTANCE field initializer (runs at construction, and no admitted top
|
|
* level constructs). Static field initializers, static blocks, heritage
|
|
* clauses, computed names, and decorators all run at the class statement
|
|
* itself — not deferred. */
|
|
function inDeferredPosition7(node: ts.Node): boolean {
|
|
let child: ts.Node = node;
|
|
for (
|
|
let p: ts.Node | undefined = node.parent;
|
|
p !== undefined && !ts.isSourceFile(p);
|
|
p = p.parent
|
|
) {
|
|
if (p.kind === ts.SyntaxKind.Decorator) return false;
|
|
if (ts.isClassStaticBlockDeclaration(p)) return false;
|
|
if (ts.isFunctionLike(p)) {
|
|
if ((p as { body?: ts.Node }).body === child) return true;
|
|
if (ts.isParameter(child)) return true;
|
|
}
|
|
if (
|
|
ts.isPropertyDeclaration(p) &&
|
|
p.initializer === child &&
|
|
(ts.getCombinedModifierFlags(p) & ts.ModifierFlags.Static) === 0
|
|
) {
|
|
return true;
|
|
}
|
|
child = p;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** True for identifiers in pure TYPE positions (annotations, `typeof`
|
|
* queries, interface/alias bodies) — erased at runtime, never a read. A
|
|
* CLASS `extends` heritage expression is the one TypeNode-shaped position
|
|
* that IS a runtime read. */
|
|
function inTypePosition7(node: ts.Node): boolean {
|
|
for (
|
|
let p: ts.Node | undefined = node.parent;
|
|
p !== undefined && !ts.isSourceFile(p);
|
|
p = p.parent
|
|
) {
|
|
if (ts.isTypeNode(p)) {
|
|
const heritage = p.parent;
|
|
const isClassExtends =
|
|
p.kind === ts.SyntaxKind.ExpressionWithTypeArguments &&
|
|
heritage !== undefined &&
|
|
ts.isHeritageClause(heritage) &&
|
|
heritage.token === ts.SyntaxKind.ExtendsKeyword &&
|
|
(ts.isClassDeclaration(heritage.parent) || ts.isClassExpression(heritage.parent));
|
|
if (!isClassExtends) return true;
|
|
}
|
|
if (ts.isTypeAliasDeclaration(p) || ts.isInterfaceDeclaration(p)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** The first top-level construct of `sf` that could execute USER code
|
|
* during a cycle's init window, or null when the module's top level is
|
|
* declaration-only ("inert"). The expression whitelist is deliberately
|
|
* conservative — anything that could CALL user code (directly, through a
|
|
* callback-invoking builtin, a getter, an iterator, or an implicit
|
|
* coercion of a user object) disqualifies the module's cycles. */
|
|
function nonInertTopLevel7(program: ts.Program, sf: ts.SourceFile): ts.Node | null {
|
|
const nonInert = topLevelInertness7(program);
|
|
for (const stmt of sf.statements.nodes) {
|
|
const offence = nonInert(stmt);
|
|
if (offence !== null) return offence;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** The per-statement form of nonInertTopLevel7: answers, for one
|
|
* top-level statement, the first construct that could execute user code
|
|
* while the statement runs, or null when the statement is inert. */
|
|
function topLevelInertness7(program: ts.Program): (stmt: ts.Statement) => ts.Node | null {
|
|
const checker = program.getTypeChecker();
|
|
const PRIM =
|
|
ts.TypeFlags.StringLike |
|
|
ts.TypeFlags.NumberLike |
|
|
ts.TypeFlags.BooleanLike |
|
|
ts.TypeFlags.BigIntLike |
|
|
ts.TypeFlags.ESSymbolLike |
|
|
ts.TypeFlags.EnumLike |
|
|
ts.TypeFlags.Undefined |
|
|
ts.TypeFlags.Null |
|
|
ts.TypeFlags.Void |
|
|
ts.TypeFlags.Never;
|
|
/** Coercion-safe: the expression's type is primitive in every arm, so
|
|
* ToString/ToNumber/ToPrimitive can never reach a user valueOf. */
|
|
const primitiveTyped = (e: ts.Expression): boolean => {
|
|
const t = checker.getTypeAtLocation(e);
|
|
const parts = t.isUnionType() ? ts.constituentTypes(t) : [t];
|
|
return parts.every((p) => (p.flags & PRIM) !== 0);
|
|
};
|
|
/** The identifier's symbol lives entirely in declaration files — a
|
|
* global builtin (process, WeakSet, Object, …) whose property reads and
|
|
* calls are runtime-implemented, never user code. */
|
|
const dtsRooted = (e: ts.Expression): boolean => {
|
|
let root: ts.Expression = e;
|
|
while (ts.isPropertyAccessExpression(root) || ts.isElementAccessExpression(root))
|
|
root = root.expression;
|
|
if (ts.isMetaProperty(root)) return true; // import.meta
|
|
if (!ts.isIdentifier(root)) return false;
|
|
let sym = checker.getSymbolAtLocation(root);
|
|
if (sym === undefined) return false;
|
|
if (sym.flags & ts.SymbolFlags.Alias) sym = checker.getAliasedSymbol(sym);
|
|
const decls = sym.declarations;
|
|
return decls.length > 0 && decls.every((d) => ts.getSourceFileOfNode(d).isDeclarationFile);
|
|
};
|
|
const hasDecorator = (n: ts.Node): boolean =>
|
|
((n as { modifiers?: ts.NodeArray<ts.Node> }).modifiers?.nodes ?? []).some(
|
|
(m) => m.kind === ts.SyntaxKind.Decorator,
|
|
);
|
|
/** No function-like node anywhere in the subtree — arguments to builtin
|
|
* calls must not smuggle a callback the builtin could invoke. */
|
|
const containsFunctionLike = (n: ts.Node): boolean => {
|
|
let found = false;
|
|
ts.walkPreorder(n, (c) => {
|
|
if (ts.isFunctionLike(c) || ts.isClassExpression(c) || ts.isClassDeclaration(c)) {
|
|
found = true;
|
|
return "stop";
|
|
}
|
|
return undefined;
|
|
});
|
|
return found;
|
|
};
|
|
const inertClass = (c: ts.ClassLikeDeclaration): ts.Node | null => {
|
|
if (hasDecorator(c)) return c;
|
|
for (const h of c.heritageClauses?.nodes ?? []) {
|
|
if (h.token !== ts.SyntaxKind.ExtendsKeyword) continue;
|
|
for (const t of h.types.nodes) if (!inert(t.expression)) return t.expression;
|
|
}
|
|
for (const m of c.members.nodes) {
|
|
if (ts.isClassStaticBlockDeclaration(m)) return m;
|
|
if (hasDecorator(m)) return m;
|
|
const memberName = (m as { name?: ts.PropertyName }).name;
|
|
if (memberName !== undefined && ts.isComputedPropertyName(memberName)) {
|
|
if (!inert(memberName.expression) || !primitiveTyped(memberName.expression))
|
|
return memberName;
|
|
}
|
|
if (
|
|
ts.isPropertyDeclaration(m) &&
|
|
(ts.getCombinedModifierFlags(m) & ts.ModifierFlags.Static) !== 0 &&
|
|
m.initializer !== undefined &&
|
|
!inert(m.initializer)
|
|
) {
|
|
return m.initializer;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
const COERCING_FREE = new Set<number>([
|
|
ts.SyntaxKind.EqualsEqualsEqualsToken,
|
|
ts.SyntaxKind.ExclamationEqualsEqualsToken,
|
|
ts.SyntaxKind.AmpersandAmpersandToken,
|
|
ts.SyntaxKind.BarBarToken,
|
|
ts.SyntaxKind.QuestionQuestionToken,
|
|
ts.SyntaxKind.CommaToken,
|
|
ts.SyntaxKind.InstanceOfKeyword,
|
|
]);
|
|
const inert = (e: ts.Expression): boolean => {
|
|
if (
|
|
ts.isStringLiteral(e) ||
|
|
ts.isNumericLiteral(e) ||
|
|
ts.isBigIntLiteral(e) ||
|
|
ts.isRegularExpressionLiteral(e) ||
|
|
ts.isNoSubstitutionTemplateLiteral(e) ||
|
|
e.kind === ts.SyntaxKind.TrueKeyword ||
|
|
e.kind === ts.SyntaxKind.FalseKeyword ||
|
|
e.kind === ts.SyntaxKind.NullKeyword ||
|
|
ts.isIdentifier(e) ||
|
|
ts.isMetaProperty(e) ||
|
|
ts.isArrowFunction(e) ||
|
|
ts.isFunctionExpression(e)
|
|
) {
|
|
return true;
|
|
}
|
|
if (ts.isClassExpression(e)) return inertClass(e) === null;
|
|
if (
|
|
ts.isParenthesizedExpression(e) ||
|
|
ts.isAsExpression(e) ||
|
|
ts.isSatisfiesExpression(e) ||
|
|
ts.isNonNullExpression(e)
|
|
) {
|
|
return inert(e.expression);
|
|
}
|
|
if (ts.isPropertyAccessExpression(e)) return dtsRooted(e);
|
|
if (ts.isElementAccessExpression(e)) {
|
|
return dtsRooted(e) && inert(e.argumentExpression) && primitiveTyped(e.argumentExpression);
|
|
}
|
|
if (ts.isTemplateExpression(e)) {
|
|
return e.templateSpans.nodes.every(
|
|
(s) => inert(s.expression) && primitiveTyped(s.expression),
|
|
);
|
|
}
|
|
if (ts.isVoidExpression(e) || ts.isTypeOfExpression(e)) return inert(e.expression);
|
|
if (ts.isPrefixUnaryExpression(e)) {
|
|
if (e.operator === ts.SyntaxKind.ExclamationToken) return inert(e.operand);
|
|
if (
|
|
e.operator === ts.SyntaxKind.PlusToken ||
|
|
e.operator === ts.SyntaxKind.MinusToken ||
|
|
e.operator === ts.SyntaxKind.TildeToken
|
|
) {
|
|
return inert(e.operand) && primitiveTyped(e.operand);
|
|
}
|
|
return false;
|
|
}
|
|
if (ts.isBinaryExpression(e)) {
|
|
if (!inert(e.left) || !inert(e.right)) return false;
|
|
if (COERCING_FREE.has(e.operatorToken.kind)) return true;
|
|
return primitiveTyped(e.left) && primitiveTyped(e.right);
|
|
}
|
|
if (ts.isConditionalExpression(e))
|
|
return inert(e.condition) && inert(e.whenTrue) && inert(e.whenFalse);
|
|
if (ts.isArrayLiteralExpression(e))
|
|
return e.elements.nodes.every((el) => !ts.isSpreadElement(el) && inert(el));
|
|
if (ts.isObjectLiteralExpression(e)) {
|
|
return e.properties.nodes.every((p) => {
|
|
if (ts.isShorthandPropertyAssignment(p)) return true;
|
|
if (!ts.isPropertyAssignment(p) || ts.isComputedPropertyName(p.name)) return false;
|
|
return inert(p.initializer);
|
|
});
|
|
}
|
|
if (ts.isCallExpression(e) || ts.isNewExpression(e)) {
|
|
if (!dtsRooted(e.expression) || !ts.isIdentifier(chainRoot7(e.expression))) return false;
|
|
const args = e.arguments?.nodes ?? [];
|
|
// A CALLABLE argument is admissible when it is itself dts-rooted:
|
|
// a builtin-owned function value (the `promisify(fs.readFile)`
|
|
// at every cycle member's top level) is runtime-implemented — even
|
|
// if the builtin callee invokes it, no user code runs and no
|
|
// cluster binding is observable. Function literals and user
|
|
// callables keep the refusal.
|
|
return args.every(
|
|
(a) =>
|
|
inert(a) &&
|
|
!containsFunctionLike(a) &&
|
|
(checker.getCallSignatures(checker.getTypeAtLocation(a)).length === 0 || dtsRooted(a)),
|
|
);
|
|
}
|
|
return false;
|
|
};
|
|
return (stmt: ts.Statement): ts.Node | null => {
|
|
if (
|
|
ts.isImportDeclaration(stmt) ||
|
|
ts.isExportDeclaration(stmt) ||
|
|
ts.isImportEqualsDeclaration(stmt) ||
|
|
ts.isFunctionDeclaration(stmt) ||
|
|
ts.isInterfaceDeclaration(stmt) ||
|
|
ts.isTypeAliasDeclaration(stmt) ||
|
|
ts.isEmptyStatement(stmt) ||
|
|
(ts.getCombinedModifierFlags(stmt as unknown as ts.Declaration) &
|
|
ts.ModifierFlags.Ambient) !==
|
|
0
|
|
) {
|
|
return null;
|
|
}
|
|
if (ts.isEnumDeclaration(stmt)) {
|
|
return (
|
|
stmt.members.nodes.find((m) => m.initializer !== undefined && !inert(m.initializer)) ?? null
|
|
);
|
|
}
|
|
if (ts.isClassDeclaration(stmt)) return inertClass(stmt);
|
|
if (ts.isVariableStatement(stmt)) {
|
|
for (const d of stmt.declarationList.declarations.nodes) {
|
|
if (!ts.isIdentifier(d.name)) return d.name; // patterns can invoke getters/iterators
|
|
if (d.initializer !== undefined && !inert(d.initializer)) return d.initializer;
|
|
}
|
|
return null;
|
|
}
|
|
if (ts.isExpressionStatement(stmt)) return inert(stmt.expression) ? null : stmt;
|
|
if (ts.isExportAssignment(stmt) && !stmt.isExportEquals) {
|
|
return inert(stmt.expression) ? null : stmt;
|
|
}
|
|
return stmt;
|
|
};
|
|
}
|
|
|
|
/** The head of a property/element-access chain. */
|
|
function chainRoot7(e: ts.Expression): ts.Expression {
|
|
let root: ts.Expression = e;
|
|
while (ts.isPropertyAccessExpression(root) || ts.isElementAccessExpression(root))
|
|
root = root.expression;
|
|
return root;
|
|
}
|
|
|
|
/** Identifier occurrences a binding-identity preflight scan will query.
|
|
* Gathered without checker traffic so managed deferred bodies can be
|
|
* warmed in one exact symbol batch before the semantic walk. */
|
|
function identifierOccurrences7(root: ts.Node, text: string): ts.Identifier[] {
|
|
const out: ts.Identifier[] = [];
|
|
ts.walkPreorder(root, (node) => {
|
|
if (ts.isIdentifier(node) && node.text === text) out.push(node);
|
|
return undefined;
|
|
});
|
|
return out;
|
|
}
|
|
|
|
/** The first use of a binding this cycle-closing import introduces that
|
|
* sits OUTSIDE a deferred position (a read there can observe the
|
|
* partially-initialized exporter), or null when every use defers.
|
|
* Re-export statements bind nothing locally — always null: the alias is
|
|
* link-time plumbing, never an evaluation-time read. Export specifiers
|
|
* of an imported binding are the same alias story, not reads. */
|
|
function backEdgeUseOffence7(
|
|
program: ts.Program,
|
|
stmt: ts.ImportDeclaration | ts.ExportDeclaration,
|
|
occurrences: ReadonlyMap<string, readonly ts.Identifier[]>,
|
|
): { name: string; node: ts.Node } | null {
|
|
if (!ts.isImportDeclaration(stmt) || stmt.importClause === undefined) return null;
|
|
const checker = program.getTypeChecker();
|
|
const clause = stmt.importClause;
|
|
const bindingNames: ts.Identifier[] = [];
|
|
if (clause.name !== undefined) bindingNames.push(clause.name);
|
|
if (clause.namedBindings !== undefined) {
|
|
if (ts.isNamespaceImport(clause.namedBindings)) bindingNames.push(clause.namedBindings.name);
|
|
else
|
|
for (const el of clause.namedBindings.elements.nodes)
|
|
if (!el.isTypeOnly) bindingNames.push(el.name);
|
|
}
|
|
for (const bindingName of bindingNames) {
|
|
const sym = checker.getSymbolAtLocation(bindingName);
|
|
if (sym === undefined) continue;
|
|
const references = occurrences.get(bindingName.text) ?? [];
|
|
checker.prefetchSymbolNodesExact(references);
|
|
for (const node of references) {
|
|
if (
|
|
!(node.parent !== undefined && ts.isExportSpecifier(node.parent)) &&
|
|
checker.getSymbolAtLocation(node) === sym &&
|
|
!inTypePosition7(node) &&
|
|
!inDeferredPosition7(node)
|
|
) {
|
|
return { name: bindingName.text, node };
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function makeCycleAdmission(
|
|
program: ts.Program,
|
|
edgesOf: (sf: ts.SourceFile) => readonly CycleEdge[],
|
|
): (importer: ts.SourceFile, e: CycleEdge) => string | null {
|
|
// Tarjan over the same edges the order walk uses (self-edges skipped —
|
|
// Node's self-reference rule makes those benign before admission is
|
|
// ever asked). State persists across lazily-added roots: a later
|
|
// strongconnect over an unvisited root composes with earlier runs
|
|
// exactly like the classic all-roots loop.
|
|
const sccOf = new Map<ts.SourceFile, ts.SourceFile[]>();
|
|
const index = new Map<ts.SourceFile, number>();
|
|
const low = new Map<ts.SourceFile, number>();
|
|
const onStack = new Set<ts.SourceFile>();
|
|
const tstack: ts.SourceFile[] = [];
|
|
let next = 0;
|
|
const strongconnect = (v: ts.SourceFile): void => {
|
|
index.set(v, next);
|
|
low.set(v, next);
|
|
next++;
|
|
tstack.push(v);
|
|
onStack.add(v);
|
|
for (const e of edgesOf(v)) {
|
|
const w = e.dep;
|
|
if (w === v) continue;
|
|
if (!index.has(w)) {
|
|
strongconnect(w);
|
|
low.set(v, Math.min(low.get(v)!, low.get(w)!));
|
|
} else if (onStack.has(w)) {
|
|
low.set(v, Math.min(low.get(v)!, index.get(w)!));
|
|
}
|
|
}
|
|
if (low.get(v) === index.get(v)) {
|
|
const comp: ts.SourceFile[] = [];
|
|
for (;;) {
|
|
const w = tstack.pop()!;
|
|
onStack.delete(w);
|
|
comp.push(w);
|
|
if (w === v) break;
|
|
}
|
|
for (const w of comp) sccOf.set(w, comp);
|
|
}
|
|
};
|
|
const lineOf = (node: ts.Node): string => {
|
|
const nsf = ts.getSourceFileOfNode(node);
|
|
return `${nsf.fileName}:${ts.getLineAndCharacterOfPosition(nsf, ts.tokenStartOfNode(node)).line + 1}`;
|
|
};
|
|
// Cluster verdict memo (keyed by the component array identity): true
|
|
// when every member is an ES module passing the inert-top-level bar.
|
|
const sccVerdict = new Map<ts.SourceFile[], boolean>();
|
|
// The exact-initialization verdict, same keying (exactCycleRefusal7).
|
|
const exactVerdict = new Map<ts.SourceFile[], string | null>();
|
|
// A module can close many cycle edges, each importing many bindings.
|
|
// Index its identifiers once for this admission pass. ASTs are immutable;
|
|
// the pass owns the index so it cannot retain a disposed program.
|
|
const bindingUses = new Map<ts.SourceFile, Map<string, ts.Identifier[]>>();
|
|
const usesOf = (sf: ts.SourceFile): Map<string, ts.Identifier[]> => {
|
|
const cached = bindingUses.get(sf);
|
|
if (cached !== undefined) return cached;
|
|
const uses = new Map<string, ts.Identifier[]>();
|
|
ts.walkPreorder(sf, (node) => {
|
|
if (ts.isImportDeclaration(node)) return "skip";
|
|
if (ts.isIdentifier(node)) {
|
|
const references = uses.get(node.text);
|
|
if (references !== undefined) references.push(node);
|
|
else uses.set(node.text, [node]);
|
|
}
|
|
return undefined;
|
|
});
|
|
bindingUses.set(sf, uses);
|
|
return uses;
|
|
};
|
|
// The CommonJS cluster verdict, same keying (cjsCycleRefusal7).
|
|
const cjsVerdict = new Map<ts.SourceFile[], string | null>();
|
|
return (importer: ts.SourceFile, e: CycleEdge): string | null => {
|
|
if (e.stmt === undefined) {
|
|
if (!index.has(importer)) strongconnect(importer);
|
|
const comp = sccOf.get(e.dep);
|
|
if (comp === undefined || comp.length < 2 || !comp.includes(importer)) {
|
|
return "the cycle closes through a require() edge";
|
|
}
|
|
if (!cjsVerdict.has(comp)) cjsVerdict.set(comp, cjsCycleRefusal7(program, comp, lineOf));
|
|
const reason = cjsVerdict.get(comp) ?? null;
|
|
return reason === null ? null : `the cycle closes through a require() edge: ${reason}`;
|
|
}
|
|
// Cheap per-edge admission: nothing readable binds through the edge.
|
|
if (ts.isImportDeclaration(e.stmt)) {
|
|
const clause = e.stmt.importClause;
|
|
if (
|
|
clause === undefined ||
|
|
typeOnlyNamedImport(e.stmt) ||
|
|
(clause.name === undefined &&
|
|
(clause.namedBindings === undefined ||
|
|
(ts.isNamespaceImport(clause.namedBindings) &&
|
|
nsBindingUsesAreBareStatements7(program, importer, clause.namedBindings.name))))
|
|
) {
|
|
return null;
|
|
}
|
|
}
|
|
if (!index.has(importer)) strongconnect(importer);
|
|
const comp = sccOf.get(e.dep);
|
|
if (comp === undefined || comp.length < 2 || !comp.includes(importer)) {
|
|
return "the cycle's module cluster could not be analyzed";
|
|
}
|
|
if (!sccVerdict.has(comp)) {
|
|
sccVerdict.set(
|
|
comp,
|
|
comp.every((m) => !isCjsJsFile7(m, program) && nonInertTopLevel7(program, m) === null),
|
|
);
|
|
}
|
|
if (
|
|
sccVerdict.get(comp) === true &&
|
|
backEdgeUseOffence7(program, e.stmt, usesOf(importer)) === null
|
|
) {
|
|
return null;
|
|
}
|
|
// The EXACT INITIALIZATION rule: the cycle runs in Node's evaluation
|
|
// order and every binding that can be observed before its declaration
|
|
// runs carries an initialization check (cycleEarlyBindings).
|
|
if (!exactVerdict.has(comp)) {
|
|
const reason = exactCycleRefusal7(program, comp, lineOf);
|
|
exactVerdict.set(comp, reason);
|
|
if (reason === null) {
|
|
const members = program.analysis.exactCycleMembers;
|
|
for (const m of comp) members.add(m);
|
|
}
|
|
}
|
|
return exactVerdict.get(comp) ?? null;
|
|
};
|
|
}
|
|
|
|
/** The reason a strongly-connected cluster of CommonJS modules cannot be
|
|
* initialized exactly through its require() cycle, or null when it can.
|
|
* Node answers a require of a module that is still evaluating with that
|
|
* module's exports object as it stands, while the compiled bindings alias
|
|
* the exporter's final declarations. The two agree when:
|
|
* - every member settles its exports before anything can enter the
|
|
* cycle: all of its export statements come before its first top-level
|
|
* statement that can run another module's code (a require, or anything
|
|
* but literals, plain reads, function and class values, and
|
|
* standard-library globals called with literal arguments), and every
|
|
* other use of its export object is a plain member read; and
|
|
* - no member's top-level code can reach its binding of another member,
|
|
* so no member runs another member's code while the cluster is still
|
|
* initializing. Each module then initializes exactly as it would
|
|
* outside the cycle, and its own require bindings keep the acyclic
|
|
* analysis (requireEarlyReference).
|
|
* Other cycles keep the SC1016 fence. */
|
|
function cjsCycleRefusal7(
|
|
program: ts.Program,
|
|
comp: readonly ts.SourceFile[],
|
|
lineOf: (node: ts.Node) => string,
|
|
): string | null {
|
|
const members = new Set(comp);
|
|
const builtinRoot = (e: ts.Expression): boolean => {
|
|
while (ts.isPropertyAccessExpression(e)) e = e.expression;
|
|
return ts.isIdentifier(e) && !sourceBinding7(program, e);
|
|
};
|
|
const literal = (e: ts.Node): boolean =>
|
|
ts.isStringLiteralLike(e) ||
|
|
ts.isNumericLiteral(e) ||
|
|
ts.isRegularExpressionLiteral(e) ||
|
|
e.kind === ts.SyntaxKind.TrueKeyword ||
|
|
e.kind === ts.SyntaxKind.FalseKeyword ||
|
|
e.kind === ts.SyntaxKind.NullKeyword;
|
|
/** An expression whose evaluation runs no user code and requires
|
|
* nothing: literals, plain reads, function and class values, literal
|
|
* containers of those, and standard-library globals called with
|
|
* literal arguments. */
|
|
const inertExpr = (e: ts.Node): boolean => {
|
|
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
|
if (literal(e) || ts.isIdentifier(e)) return true;
|
|
if (ts.isFunctionExpression(e) || ts.isArrowFunction(e)) return true;
|
|
if (ts.isClassExpression(e)) return classSyncParts7(e).every(inertPart);
|
|
if (ts.isObjectLiteralExpression(e)) {
|
|
return e.properties.nodes.every((p) =>
|
|
ts.isShorthandPropertyAssignment(p)
|
|
? true
|
|
: ts.isPropertyAssignment(p)
|
|
? !ts.isComputedPropertyName(p.name) && inertExpr(p.initializer)
|
|
: (ts.isMethodDeclaration(p) ||
|
|
ts.isGetAccessorDeclaration(p) ||
|
|
ts.isSetAccessorDeclaration(p)) &&
|
|
!ts.isComputedPropertyName(p.name),
|
|
);
|
|
}
|
|
if (ts.isArrayLiteralExpression(e)) {
|
|
return e.elements.nodes.every((el) => !ts.isSpreadElement(el) && inertExpr(el));
|
|
}
|
|
if (ts.isPropertyAccessExpression(e)) return builtinRoot(e);
|
|
if (ts.isVoidExpression(e)) return literal(e.expression);
|
|
if (ts.isCallExpression(e) || ts.isNewExpression(e)) {
|
|
return (
|
|
requireSpecOf7(e) === null &&
|
|
builtinRoot(e.expression) &&
|
|
(e.arguments?.nodes ?? []).every(literal)
|
|
);
|
|
}
|
|
return false;
|
|
};
|
|
const inertPart = (part: ts.Node): boolean => {
|
|
if (ts.isHeritageClause(part)) return part.types.nodes.every((t) => inertExpr(t.expression));
|
|
if (ts.isComputedPropertyName(part)) return inertExpr(part.expression);
|
|
return ts.isExpression(part) && inertExpr(part);
|
|
};
|
|
const exportObject = (e: ts.Expression): boolean =>
|
|
(ts.isIdentifier(e) && e.text === "exports" && !sourceBinding7(program, e)) ||
|
|
(isModuleExportsAccess(e) &&
|
|
ts.isIdentifier(e.expression) &&
|
|
!sourceBinding7(program, e.expression));
|
|
/** A top-level statement that only publishes inert values. */
|
|
const exportStatement = (stmt: ts.Statement): boolean => {
|
|
if (!ts.isExpressionStatement(stmt)) return false;
|
|
if (isEsModuleStamp(stmt.expression)) return true;
|
|
const cjs = cjsExportAssignmentOf(stmt);
|
|
if (cjs !== null) {
|
|
let value: ts.Expression = cjs.expr;
|
|
while (
|
|
ts.isBinaryExpression(value) &&
|
|
value.operatorToken.kind === ts.SyntaxKind.EqualsToken
|
|
) {
|
|
const left = value.left;
|
|
if (
|
|
!isModuleExportsAccess(left) &&
|
|
!(ts.isPropertyAccessExpression(left) && exportObject(left.expression))
|
|
)
|
|
return false;
|
|
value = value.right;
|
|
}
|
|
return inertExpr(value);
|
|
}
|
|
const call = stmt.expression;
|
|
return (
|
|
ts.isCallExpression(call) &&
|
|
ts.isPropertyAccessExpression(call.expression) &&
|
|
ts.isIdentifier(call.expression.expression) &&
|
|
call.expression.expression.text === "Object" &&
|
|
!sourceBinding7(program, call.expression.expression) &&
|
|
call.expression.name.text === "defineProperty" &&
|
|
call.arguments.nodes.length === 3 &&
|
|
exportObject(call.arguments.nodes[0]!) &&
|
|
ts.isStringLiteralLike(call.arguments.nodes[1]!) &&
|
|
inertExpr(call.arguments.nodes[2]!)
|
|
);
|
|
};
|
|
for (const m of comp) {
|
|
if (!isCjsJsFile7(m, program)) {
|
|
return `${m.fileName} is not a CommonJS module — require cycles are admitted between CommonJS modules only`;
|
|
}
|
|
const stmts = m.statements.nodes;
|
|
// The first statement that can run another module's code.
|
|
let firstEntry = stmts.length;
|
|
for (let i = 0; i < stmts.length; i++) {
|
|
const st = stmts[i]!;
|
|
const inert =
|
|
ts.isFunctionDeclaration(st) ||
|
|
ts.isEmptyStatement(st) ||
|
|
(ts.isExpressionStatement(st) && ts.isStringLiteral(st.expression)) ||
|
|
(ts.isClassDeclaration(st) && classSyncParts7(st).every(inertPart)) ||
|
|
(ts.isVariableStatement(st) &&
|
|
st.declarationList.declarations.nodes.every(
|
|
(d) =>
|
|
ts.isIdentifier(d.name) && (d.initializer === undefined || inertExpr(d.initializer)),
|
|
)) ||
|
|
exportStatement(st);
|
|
if (!inert) {
|
|
firstEntry = i;
|
|
break;
|
|
}
|
|
}
|
|
// Every other use of the export object must be a plain member read:
|
|
// anything else can change what a module of the cycle observes.
|
|
let stray: ts.Node | null = null;
|
|
const settled = new Set(stmts.slice(0, firstEntry).filter(exportStatement));
|
|
// A bundler export table the rewrite certified (npmStaticLiveExportTable)
|
|
// stands for the live getter object the original installed before its
|
|
// first require: the tail table settles as if it ran there.
|
|
const tail = stmts[stmts.length - 1];
|
|
if (
|
|
tail !== undefined &&
|
|
npmStaticLiveExportTable(m.fileName) &&
|
|
cjsExportAssignmentOf(tail)?.kind === "table"
|
|
)
|
|
settled.add(tail);
|
|
ts.walkPreorder(m, (n) => {
|
|
if (stray !== null) return "stop";
|
|
if (ts.isStatement(n) && settled.has(n)) return "skip";
|
|
let use: ts.Expression | null = null;
|
|
if (n.kind === ts.SyntaxKind.ThisKeyword) {
|
|
let moduleThis = true;
|
|
for (let p = n.parent; p !== undefined && !ts.isSourceFile(p); p = p.parent) {
|
|
if (
|
|
(ts.isFunctionLike(p) && !ts.isArrowFunction(p)) ||
|
|
ts.isClassDeclaration(p) ||
|
|
ts.isClassExpression(p)
|
|
) {
|
|
moduleThis = false;
|
|
break;
|
|
}
|
|
}
|
|
if (moduleThis) use = n as ts.Expression;
|
|
} else if (
|
|
ts.isIdentifier(n) &&
|
|
(n.text === "exports" || n.text === "module") &&
|
|
!(ts.isPropertyAccessExpression(n.parent) && n.parent.name === n) &&
|
|
!sourceBinding7(program, n)
|
|
) {
|
|
if (n.text === "exports") use = n;
|
|
else if (!ts.isPropertyAccessExpression(n.parent) || n.parent.expression !== n) stray = n;
|
|
else if (n.parent.name.text === "exports") use = n.parent;
|
|
}
|
|
if (use === null) return undefined;
|
|
const parent = use.parent;
|
|
const read =
|
|
ts.isPropertyAccessExpression(parent) &&
|
|
parent.expression === use &&
|
|
!(
|
|
ts.isBinaryExpression(parent.parent) &&
|
|
parent.parent.left === parent &&
|
|
parent.parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
|
|
parent.parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment
|
|
) &&
|
|
!(ts.isCallExpression(parent.parent) && parent.parent.expression === parent) &&
|
|
!ts.isDeleteExpression(parent.parent);
|
|
if (!read) stray = use;
|
|
return undefined;
|
|
});
|
|
if (stray !== null) {
|
|
return `${m.fileName} uses its exports at ${lineOf(stray)} outside the export statements that precede its first require, so a module of the cycle can observe them before they settle`;
|
|
}
|
|
// Bindings of other members must stay unreachable from top-level code.
|
|
for (const st of stmts) {
|
|
for (const req of requiresOf7(st, program)) {
|
|
if (req.decl === null) continue;
|
|
const dep = resolveImport7(program, m, req.spec, "require");
|
|
if (dep === null || !members.has(dep)) continue;
|
|
const reached = requireBindingEarlyUse7(program, m, stmts.length, req.decl, [], (id) =>
|
|
functionExportRead7(id, dep),
|
|
);
|
|
if (reached !== null) {
|
|
return `the top-level code of ${m.fileName} can use its binding '${reached.text}' of a module of the cycle (${lineOf(reached)}) while the cycle initializes`;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** True when `use` initializes a top-level const whose every other
|
|
* reference is a value of an export statement (`module.exports = { … }`
|
|
* or `exports.n = …`). */
|
|
function publishedAlias7(use: ts.Node, decl: ts.Node | undefined): boolean {
|
|
if (
|
|
decl === undefined ||
|
|
!ts.isVariableDeclaration(decl) ||
|
|
decl.initializer !== use ||
|
|
!ts.isIdentifier(decl.name)
|
|
)
|
|
return false;
|
|
const list = decl.parent;
|
|
const stmt = list?.parent;
|
|
const sf = stmt?.parent;
|
|
if (
|
|
list === undefined ||
|
|
(list.flags & ts.NodeFlags.Const) === 0 ||
|
|
stmt === undefined ||
|
|
!ts.isVariableStatement(stmt) ||
|
|
sf === undefined ||
|
|
!ts.isSourceFile(sf)
|
|
)
|
|
return false;
|
|
const alias = decl.name;
|
|
const published = new Set<ts.Node>();
|
|
for (const st of sf.statements.nodes) {
|
|
const cjs = cjsExportAssignmentOf(st);
|
|
if (cjs === null) continue;
|
|
if (cjs.kind === "member") published.add(cjs.value);
|
|
else if (cjs.obj !== null) {
|
|
for (const p of cjs.obj.properties.nodes) {
|
|
if (ts.isPropertyAssignment(p) && !ts.isComputedPropertyName(p.name))
|
|
published.add(p.initializer);
|
|
else if (ts.isShorthandPropertyAssignment(p)) published.add(p.name);
|
|
}
|
|
}
|
|
}
|
|
let kept = false;
|
|
ts.walkPreorder(sf, (n) => {
|
|
if (kept) return "stop";
|
|
if (!ts.isIdentifier(n) || n === alias || n.text !== alias.text) return undefined;
|
|
const p = n.parent;
|
|
if (
|
|
p !== undefined &&
|
|
((ts.isPropertyAccessExpression(p) && p.name === n) ||
|
|
(ts.isPropertyAssignment(p) && p.name === n))
|
|
)
|
|
return undefined;
|
|
if (!published.has(n)) kept = true;
|
|
return undefined;
|
|
});
|
|
return !kept;
|
|
}
|
|
|
|
/** True for a plain read `binding.name` (not a call, not a write, not a
|
|
* value that is stored or passed on) of the cycle member `dep` whose
|
|
* export `name` is a top-level function declaration nothing reassigns.
|
|
* Every member settles its exports before entering the cycle, so Node
|
|
* reads that same function whenever the read runs, and the compiled read
|
|
* resolves to the declaration itself. */
|
|
function functionExportRead7(id: ts.Identifier, dep: ts.SourceFile): boolean {
|
|
const access = id.parent;
|
|
if (!ts.isPropertyAccessExpression(access) || access.expression !== id) return false;
|
|
// The value the read yields: through parentheses and the right operand
|
|
// of a comma, so `(0, binding.name)(...)` is the call it spells.
|
|
let use: ts.Node = access;
|
|
for (;;) {
|
|
const p = use.parent;
|
|
if (ts.isParenthesizedExpression(p)) use = p;
|
|
else if (
|
|
ts.isBinaryExpression(p) &&
|
|
p.operatorToken.kind === ts.SyntaxKind.CommaToken &&
|
|
p.right === use
|
|
)
|
|
use = p;
|
|
else break;
|
|
}
|
|
const parent = use.parent;
|
|
// Only a use that neither runs the function nor keeps it is a read: a
|
|
// stored alias (`const f = binding.name`, `{ f: binding.name }`) or an
|
|
// argument can be called while the cycle initializes. A top-level const
|
|
// that only publishes it (`module.exports = { name: f }`, the static
|
|
// rewrite's re-export spelling) runs nothing: a module that reads the
|
|
// export back checks that read itself.
|
|
const observes =
|
|
publishedAlias7(use, parent) ||
|
|
ts.isExpressionStatement(parent) ||
|
|
ts.isTypeOfExpression(parent) ||
|
|
ts.isVoidExpression(parent) ||
|
|
(ts.isPrefixUnaryExpression(parent) && parent.operator === ts.SyntaxKind.ExclamationToken) ||
|
|
(ts.isBinaryExpression(parent) &&
|
|
(parent.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken ||
|
|
parent.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsEqualsToken ||
|
|
parent.operatorToken.kind === ts.SyntaxKind.EqualsEqualsToken ||
|
|
parent.operatorToken.kind === ts.SyntaxKind.ExclamationEqualsToken)) ||
|
|
((ts.isIfStatement(parent) || ts.isWhileStatement(parent) || ts.isDoStatement(parent)) &&
|
|
parent.expression === use) ||
|
|
(ts.isConditionalExpression(parent) && parent.condition === use);
|
|
if (!observes) return false;
|
|
const name = access.name.text;
|
|
// The exported value's identifier: a table entry or a member export.
|
|
const values: ts.Expression[] = [];
|
|
for (const stmt of dep.statements.nodes) {
|
|
if (!ts.isExpressionStatement(stmt) || !ts.isBinaryExpression(stmt.expression)) continue;
|
|
const e = stmt.expression;
|
|
if (e.operatorToken.kind !== ts.SyntaxKind.EqualsToken) continue;
|
|
if (isModuleExportsAccess(e.left)) {
|
|
if (!ts.isObjectLiteralExpression(e.right)) return false;
|
|
for (const p of e.right.properties.nodes) {
|
|
if (ts.isSpreadAssignment(p)) return false;
|
|
if (ts.isShorthandPropertyAssignment(p) && p.name.text === name)
|
|
values.push(p.name as ts.Identifier);
|
|
else if (
|
|
ts.isPropertyAssignment(p) &&
|
|
(ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) &&
|
|
p.name.text === name
|
|
)
|
|
values.push(p.initializer);
|
|
}
|
|
} else if (
|
|
ts.isPropertyAccessExpression(e.left) &&
|
|
e.left.name.text === name &&
|
|
((ts.isIdentifier(e.left.expression) && e.left.expression.text === "exports") ||
|
|
isModuleExportsAccess(e.left.expression))
|
|
) {
|
|
values.push(e.right);
|
|
}
|
|
}
|
|
const value = values.length === 1 ? values[0]! : null;
|
|
if (value === null || !ts.isIdentifier(value)) return false;
|
|
const fnName = value.text;
|
|
if (
|
|
!dep.statements.nodes.some(
|
|
(st) => ts.isFunctionDeclaration(st) && st.name?.text === fnName && st.body !== undefined,
|
|
)
|
|
)
|
|
return false;
|
|
let rebound = false;
|
|
ts.walkPreorder(dep, (n) => {
|
|
if (rebound) return "stop";
|
|
if (
|
|
(ts.isBinaryExpression(n) &&
|
|
ts.isIdentifier(n.left) &&
|
|
n.left.text === fnName &&
|
|
n.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
|
|
n.operatorToken.kind <= ts.SyntaxKind.LastAssignment) ||
|
|
(ts.isVariableDeclaration(n) && ts.isIdentifier(n.name) && n.name.text === fnName) ||
|
|
(ts.isFunctionDeclaration(n) && n.name?.text === fnName && !ts.isSourceFile(n.parent))
|
|
)
|
|
rebound = true;
|
|
return undefined;
|
|
});
|
|
return !rebound;
|
|
}
|
|
|
|
/* ── exact cyclic initialization ─────────────────────────────────────────
|
|
* Cycles that fail the declaration-only rule still evaluate exactly like
|
|
* Node when their shape is representable: the guarded %init calls already
|
|
* run every module body in ES evaluation order (depth-first postorder,
|
|
* a module mid-evaluation answered from the cache), function declarations
|
|
* are callable from the start, and live bindings are shared globals. What
|
|
* remains is the temporal dead zone: a let/const binding read before its
|
|
* declaration ran throws ReferenceError. cycleEarlyBindings finds every
|
|
* such binding whose read CAN run early; the lowering gives each one an
|
|
* initialization flag and checks it at every read or write that can run
|
|
* before the declaration. The shapes below have no such representation
|
|
* and keep the SC1016 fence. */
|
|
|
|
/** The modules of `program` whose import cycles were admitted by the
|
|
* exact initialization rule (not the declaration-only rule). */
|
|
export function exactCycleMembers(program: ts.Program): ReadonlySet<ts.SourceFile> {
|
|
return program.analysis.exactCycleMembers;
|
|
}
|
|
|
|
/** True when the module's top level (outside every function-like body)
|
|
* contains `await` or `for await`. */
|
|
function hasTopLevelAwait7(sf: ts.SourceFile): boolean {
|
|
let found = false;
|
|
ts.walkPreorder(sf, (node) => {
|
|
if (found) return "stop";
|
|
if (node !== sf && ts.isFunctionLike(node)) return "skip";
|
|
if (
|
|
ts.isAwaitExpression(node) ||
|
|
(ts.isForOfStatement(node) && node.awaitModifier !== undefined)
|
|
) {
|
|
found = true;
|
|
return "stop";
|
|
}
|
|
return undefined;
|
|
});
|
|
return found;
|
|
}
|
|
|
|
/** The reason a strongly-connected module cluster cannot be initialized
|
|
* exactly, or null when it can. */
|
|
function exactCycleRefusal7(
|
|
program: ts.Program,
|
|
comp: readonly ts.SourceFile[],
|
|
lineOf: (node: ts.Node) => string,
|
|
): string | null {
|
|
const members = new Set(comp);
|
|
const targetOf = (stmt: ts.ImportDeclaration | ts.ExportDeclaration): ts.SourceFile | null => {
|
|
const spec = stmt.moduleSpecifier;
|
|
if (spec === undefined || !ts.isStringLiteral(spec)) return null;
|
|
const sf = ts.getSourceFileOfNode(stmt);
|
|
return resolveImport7(program, sf, spec.text) ?? npmStaticDepSf7(program, sf, spec.text);
|
|
};
|
|
for (const m of comp) {
|
|
if (isCjsJsFile7(m, program) || /\.c[jt]s$/.test(m.fileName)) {
|
|
return `${m.fileName} is a CommonJS module — cyclic initialization covers ES modules only`;
|
|
}
|
|
if (hasTopLevelAwait7(m)) {
|
|
return `${m.fileName} uses top-level await inside the cycle`;
|
|
}
|
|
for (const stmt of m.statements.nodes) {
|
|
if (ts.isExportAssignment(stmt) && !stmt.isExportEquals) {
|
|
return `the default export expression at ${lineOf(stmt)} has no initialization state inside the cycle`;
|
|
}
|
|
// `export * from` binds each name to the declaring member's own
|
|
// binding, like a named re-export, so only `export * as ns` (a
|
|
// namespace object of a member) has state of its own.
|
|
if (
|
|
ts.isExportDeclaration(stmt) &&
|
|
!stmt.isTypeOnly &&
|
|
stmt.exportClause !== undefined &&
|
|
ts.isNamespaceExport(stmt.exportClause)
|
|
) {
|
|
const dep = targetOf(stmt);
|
|
if (dep !== null && members.has(dep)) {
|
|
return `the namespace re-export at ${lineOf(stmt)} names a module of the cycle`;
|
|
}
|
|
}
|
|
if (
|
|
ts.isImportDeclaration(stmt) &&
|
|
stmt.importClause !== undefined &&
|
|
!stmt.importClause.isTypeOnly &&
|
|
stmt.importClause.namedBindings !== undefined &&
|
|
ts.isNamespaceImport(stmt.importClause.namedBindings)
|
|
) {
|
|
const dep = targetOf(stmt);
|
|
const nsName = stmt.importClause.namedBindings.name;
|
|
if (
|
|
dep !== null &&
|
|
members.has(dep) &&
|
|
!nsBindingUsesAreBareStatements7(program, m, nsName)
|
|
) {
|
|
return `the namespace import '${nsName.text}' at ${lineOf(stmt)} names a module of the cycle`;
|
|
}
|
|
}
|
|
if (ts.isClassDeclaration(stmt) && stmt.name === undefined) {
|
|
return `the anonymous default class at ${lineOf(stmt)} has no initialization state inside the cycle`;
|
|
}
|
|
const ambient =
|
|
(ts.getCombinedModifierFlags(stmt as unknown as ts.Declaration) &
|
|
ts.ModifierFlags.Ambient) !==
|
|
0;
|
|
if (ambient) continue;
|
|
if (
|
|
ts.isEnumDeclaration(stmt) &&
|
|
(ts.getCombinedModifierFlags(stmt) & ts.ModifierFlags.Const) === 0
|
|
) {
|
|
return `the enum '${stmt.name.text}' at ${lineOf(stmt)} has no initialization state inside the cycle`;
|
|
}
|
|
if (ts.isModuleDeclaration(stmt) && !typeOnlyNamespace7(stmt)) {
|
|
return `the namespace at ${lineOf(stmt)} has no initialization state inside the cycle`;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** A module-scope binding of an exactly-initialized cycle that some read
|
|
* or write can reach before its declaration ran. */
|
|
export interface CycleEarlyBinding {
|
|
symbol: ts.Symbol;
|
|
/** The declaration's name. */
|
|
name: ts.Identifier;
|
|
kind: "lexical" | "var" | "class";
|
|
module: ts.SourceFile;
|
|
/** One reference that can run before the declaration. */
|
|
reference: ts.Identifier;
|
|
/** A reference in a position the lowering may resolve at compile time
|
|
* from the binding's type (a property key, a `typeof` operand). */
|
|
folded?: ts.Identifier;
|
|
/** Modules whose TOP-LEVEL code runs only after the declaration ran:
|
|
* modules outside the cycle and cycle members evaluated later. */
|
|
settled: ReadonlySet<ts.SourceFile>;
|
|
/** The local name each importing file gives the binding when it differs
|
|
* from the declared one, by file name: Node's ReferenceError names it. */
|
|
localNames?: ReadonlyMap<string, string>;
|
|
}
|
|
|
|
/** Every binding of the program's exactly-initialized cycles that can be
|
|
* observed before its declaration executes. `order` is the compiled module
|
|
* order; `singleRoot` is false when more than one module graph root runs
|
|
* (fork or worker entries), so a cycle can be entered at different
|
|
* members. With one root and a purely ES-module graph the evaluation order
|
|
* is static and the analysis is precise up to "user code may run": a
|
|
* binding needs its check when its declaration completes after some
|
|
* top-level code that can call user functions and a deferred position
|
|
* (function body, parameter default, instance field) references it, or
|
|
* when the top level of a cycle member evaluated before it reads it.
|
|
* Otherwise every deferred reference and every top-level reference from
|
|
* another member counts. */
|
|
export function cycleEarlyBindings(
|
|
program: ts.Program,
|
|
entry: ts.SourceFile,
|
|
order: readonly ts.SourceFile[],
|
|
singleRoot: boolean,
|
|
): CycleEarlyBinding[] {
|
|
const exact = program.analysis.exactCycleMembers;
|
|
if (exact.size === 0) return [];
|
|
// Every lowering pass of one compile asks with the same inputs.
|
|
const key = `${entry.fileName}\0${order.map((sf) => sf.fileName).join("\0")}\0${singleRoot}`;
|
|
const memo = program.analysis.cycleEarlyBindings;
|
|
if (memo !== null && memo.key === key) return memo.bindings;
|
|
const bindings = computeCycleEarlyBindings7(program, entry, order, singleRoot, exact);
|
|
program.analysis.cycleEarlyBindings = { key, bindings };
|
|
return bindings;
|
|
}
|
|
|
|
function computeCycleEarlyBindings7(
|
|
program: ts.Program,
|
|
entry: ts.SourceFile,
|
|
order: readonly ts.SourceFile[],
|
|
singleRoot: boolean,
|
|
exact: ReadonlySet<ts.SourceFile>,
|
|
): CycleEarlyBinding[] {
|
|
const inOrder = new Set(order);
|
|
const members = [...exact].filter((m) => inOrder.has(m));
|
|
if (members.length === 0) return [];
|
|
const memberSet = new Set(members);
|
|
const depsOf = (sf: ts.SourceFile): ts.SourceFile[] =>
|
|
orderedImportsOf(program, sf).flatMap(({ dep }) =>
|
|
dep !== null && dep !== sf && inOrder.has(dep) ? [dep] : [],
|
|
);
|
|
// Strongly-connected components among the exactly-initialized members.
|
|
const comps: ts.SourceFile[][] = [];
|
|
{
|
|
const index = new Map<ts.SourceFile, number>();
|
|
const low = new Map<ts.SourceFile, number>();
|
|
const onStack = new Set<ts.SourceFile>();
|
|
const stack: ts.SourceFile[] = [];
|
|
let next = 0;
|
|
const connect = (v: ts.SourceFile): void => {
|
|
index.set(v, next);
|
|
low.set(v, next);
|
|
next++;
|
|
stack.push(v);
|
|
onStack.add(v);
|
|
for (const w of depsOf(v)) {
|
|
if (!memberSet.has(w)) continue;
|
|
if (!index.has(w)) {
|
|
connect(w);
|
|
low.set(v, Math.min(low.get(v)!, low.get(w)!));
|
|
} else if (onStack.has(w)) {
|
|
low.set(v, Math.min(low.get(v)!, index.get(w)!));
|
|
}
|
|
}
|
|
if (low.get(v) === index.get(v)) {
|
|
const comp: ts.SourceFile[] = [];
|
|
for (;;) {
|
|
const w = stack.pop()!;
|
|
onStack.delete(w);
|
|
comp.push(w);
|
|
if (w === v) break;
|
|
}
|
|
if (comp.length > 1) comps.push(comp);
|
|
}
|
|
};
|
|
for (const m of members) if (!index.has(m)) connect(m);
|
|
}
|
|
if (comps.length === 0) return [];
|
|
// The static evaluation order: Node's depth-first postorder from the
|
|
// entry over ES import edges. Known only for one root and a graph with
|
|
// no CommonJS module (a require() evaluates its target mid-body).
|
|
const position = new Map<ts.SourceFile, number>();
|
|
if (
|
|
singleRoot &&
|
|
!order.some((sf) => isCjsJsFile7(sf, program) || /\.c[jt]s$/.test(sf.fileName))
|
|
) {
|
|
const seen = new Set<ts.SourceFile>();
|
|
const visit = (sf: ts.SourceFile): void => {
|
|
seen.add(sf);
|
|
for (const dep of depsOf(sf)) if (!seen.has(dep)) visit(dep);
|
|
position.set(sf, position.size);
|
|
};
|
|
visit(entry);
|
|
}
|
|
const checker = program.getTypeChecker();
|
|
const nonInert = topLevelInertness7(program);
|
|
const out: CycleEarlyBinding[] = [];
|
|
for (const comp of comps) {
|
|
const known = comp.every((m) => position.has(m));
|
|
const evaluation = known
|
|
? comp.slice().sort((a, b) => position.get(a)! - position.get(b)!)
|
|
: comp.slice();
|
|
const candidates = new Map<ts.Symbol, CycleCandidate7>();
|
|
const names = new Set<string>();
|
|
let dirty = !known;
|
|
evaluation.forEach((m, rank) => {
|
|
for (const stmt of m.statements.nodes) {
|
|
const offence = nonInert(stmt);
|
|
const exposed = dirty || offence !== null;
|
|
const declared: { name: ts.Identifier; kind: CycleEarlyBinding["kind"] }[] = [];
|
|
if (
|
|
ts.isVariableStatement(stmt) &&
|
|
(ts.getCombinedModifierFlags(stmt as unknown as ts.Declaration) &
|
|
ts.ModifierFlags.Ambient) ===
|
|
0
|
|
) {
|
|
const lexical =
|
|
(stmt.declarationList.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) !== 0;
|
|
for (const d of stmt.declarationList.declarations.nodes) {
|
|
for (const name of boundNames7(d.name)) {
|
|
declared.push({ name, kind: lexical ? "lexical" : "var" });
|
|
}
|
|
}
|
|
} else if (ts.isClassDeclaration(stmt) && stmt.name !== undefined) {
|
|
declared.push({ name: stmt.name, kind: "class" });
|
|
}
|
|
for (const { name, kind } of declared) {
|
|
const symbol = checker.getSymbolAtLocation(name);
|
|
if (symbol === undefined) continue;
|
|
candidates.set(symbol, {
|
|
symbol,
|
|
name,
|
|
kind,
|
|
module: m,
|
|
exposed,
|
|
rank,
|
|
start: ts.tokenStartOfNode(stmt),
|
|
end: stmt.end,
|
|
reference: null,
|
|
deferredReference: null,
|
|
folded: null,
|
|
localNames: new Map(),
|
|
});
|
|
names.add(name.text);
|
|
}
|
|
if (offence !== null) dirty = true;
|
|
}
|
|
});
|
|
if (candidates.size === 0) continue;
|
|
const rankOf = new Map(evaluation.map((m, i) => [m, i] as const));
|
|
// References: every identifier spelled like a candidate or like a
|
|
// local name the file imports one under (`import { x as y }`, a
|
|
// default import), resolved through import aliases to the
|
|
// declaration's symbol.
|
|
for (const sf of order) {
|
|
const local = new Set<string>();
|
|
for (const stmt of sf.statements.nodes) {
|
|
if (!ts.isImportDeclaration(stmt) || stmt.importClause === undefined) continue;
|
|
const clause = stmt.importClause;
|
|
const bound: ts.Identifier[] = clause.name !== undefined ? [clause.name] : [];
|
|
if (clause.namedBindings !== undefined && ts.isNamedImports(clause.namedBindings))
|
|
for (const element of clause.namedBindings.elements.nodes) bound.push(element.name);
|
|
for (const name of bound) {
|
|
if (names.has(name.text)) continue;
|
|
const imported = checker.getSymbolAtLocation(name);
|
|
const candidate =
|
|
imported === undefined ? undefined : candidates.get(checker.getAliasedSymbol(imported));
|
|
if (candidate === undefined) continue;
|
|
local.add(name.text);
|
|
candidate.localNames.set(sf.fileName, name.text);
|
|
}
|
|
}
|
|
const refs: ts.Identifier[] = [];
|
|
ts.walkPreorder(sf, (node) => {
|
|
if (ts.isImportDeclaration(node)) return "skip";
|
|
if (ts.isIdentifier(node) && (names.has(node.text) || local.has(node.text)))
|
|
refs.push(node);
|
|
return undefined;
|
|
});
|
|
if (refs.length === 0) continue;
|
|
checker.prefetchSymbolNodesExact(refs);
|
|
for (const ref of refs) {
|
|
const parent = ref.parent;
|
|
if (parent !== undefined && ts.isExportSpecifier(parent)) continue;
|
|
if (inTypePosition7(ref)) continue;
|
|
let symbol = checker.getSymbolAtLocation(ref);
|
|
if (
|
|
parent !== undefined &&
|
|
ts.isShorthandPropertyAssignment(parent) &&
|
|
parent.name === ref
|
|
) {
|
|
symbol = checker.getShorthandAssignmentValueSymbol(parent) ?? symbol;
|
|
}
|
|
if (symbol !== undefined && symbol.flags & ts.SymbolFlags.Alias) {
|
|
symbol = checker.getAliasedSymbol(symbol);
|
|
}
|
|
const c = symbol === undefined ? undefined : candidates.get(symbol);
|
|
if (c === undefined || ref === c.name) continue;
|
|
if (c.folded === null && foldablePosition7(ref)) c.folded = ref;
|
|
if (c.reference !== null) continue;
|
|
// A class body names its own inner binding, initialized throughout.
|
|
if (
|
|
c.kind === "class" &&
|
|
sf === c.module &&
|
|
ts.tokenStartOfNode(ref) >= c.start &&
|
|
ref.end <= c.end
|
|
)
|
|
continue;
|
|
if (inDeferredPosition7(ref)) {
|
|
if (c.exposed && c.deferredReference === null) c.deferredReference = ref;
|
|
continue;
|
|
}
|
|
let early: boolean;
|
|
if (sf === c.module) early = ts.tokenStartOfNode(ref) < c.start;
|
|
else if (!memberSet.has(sf) || !rankOf.has(sf)) early = false;
|
|
else early = !known || rankOf.get(sf)! < c.rank;
|
|
if (early) c.reference = ref;
|
|
}
|
|
}
|
|
// With a static evaluation order, a deferred reference only runs early
|
|
// when the code that runs before the declaration completes can reach
|
|
// the function holding it.
|
|
const reached =
|
|
known && [...candidates.values()].some((c) => c.reference === null && c.deferredReference)
|
|
? cycleReachedDeferredReferences7(program, evaluation, candidates)
|
|
: null;
|
|
for (const c of candidates.values()) {
|
|
if (c.reference === null && c.deferredReference !== null)
|
|
c.reference = reached === null ? c.deferredReference : (reached.get(c.symbol) ?? null);
|
|
if (c.reference === null) continue;
|
|
const settled = new Set<ts.SourceFile>(order.filter((sf) => !rankOf.has(sf)));
|
|
if (known) for (const m of evaluation.slice(c.rank + 1)) settled.add(m);
|
|
out.push({
|
|
symbol: c.symbol,
|
|
name: c.name,
|
|
kind: c.kind,
|
|
module: c.module,
|
|
reference: c.reference,
|
|
...(c.folded !== null ? { folded: c.folded } : {}),
|
|
settled,
|
|
...(c.localNames.size > 0 ? { localNames: c.localNames } : {}),
|
|
});
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** Collects into `units` the callables an initializer expression stores
|
|
* (see moduleEarlyBindingsOf7), recording in `immediate` the parts of a
|
|
* stored node that run where it is evaluated. */
|
|
function storedCallables7(
|
|
checker: ts.TypeChecker,
|
|
e: ts.Expression,
|
|
units: ts.Node[],
|
|
immediate: Map<ts.Node, ts.Node[]>,
|
|
): void {
|
|
while (
|
|
ts.isParenthesizedExpression(e) ||
|
|
ts.isAsExpression(e) ||
|
|
ts.isSatisfiesExpression(e) ||
|
|
ts.isNonNullExpression(e)
|
|
)
|
|
e = e.expression;
|
|
if (ts.isArrowFunction(e) || ts.isFunctionExpression(e)) {
|
|
units.push(e);
|
|
} else if (ts.isClassExpression(e)) {
|
|
units.push(e);
|
|
immediate.set(e, classSyncParts7(e));
|
|
} else if (ts.isObjectLiteralExpression(e)) {
|
|
for (const p of e.properties.nodes) {
|
|
if (
|
|
ts.isMethodDeclaration(p) ||
|
|
ts.isGetAccessorDeclaration(p) ||
|
|
ts.isSetAccessorDeclaration(p)
|
|
) {
|
|
// A computed key evaluates where the literal does; a plain
|
|
// member-chain key (`[Equal.symbol]`) runs no user code.
|
|
if (!ts.isComputedPropertyName(p.name)) units.push(p);
|
|
else if (memberChainKey7(p.name.expression)) {
|
|
units.push(p);
|
|
immediate.set(p, [p.name]);
|
|
}
|
|
} else if (ts.isPropertyAssignment(p))
|
|
storedCallables7(checker, p.initializer, units, immediate);
|
|
}
|
|
} else if (ts.isArrayLiteralExpression(e)) {
|
|
for (const el of e.elements.nodes)
|
|
if (!ts.isSpreadElement(el)) storedCallables7(checker, el, units, immediate);
|
|
} else if (ts.isCallExpression(e)) {
|
|
// `const f = wrap(callback)` where wrap only returns closures over
|
|
// the callback: it runs when the stored result is called.
|
|
e.arguments.nodes.forEach((arg, index) => {
|
|
let fn: ts.Expression = arg;
|
|
while (ts.isParenthesizedExpression(fn)) fn = fn.expression;
|
|
if (
|
|
(ts.isArrowFunction(fn) || ts.isFunctionExpression(fn)) &&
|
|
returnsOnlyClosuresOver7(checker, e.expression, index)
|
|
)
|
|
units.push(fn);
|
|
});
|
|
}
|
|
}
|
|
|
|
/** A binding of an import cycle and what the reference scan found. */
|
|
interface CycleCandidate7 {
|
|
symbol: ts.Symbol;
|
|
name: ts.Identifier;
|
|
kind: CycleEarlyBinding["kind"];
|
|
module: ts.SourceFile;
|
|
/** User code can run between the cycle's start and the end of the
|
|
* declaration (always true when the order is unknown). */
|
|
exposed: boolean;
|
|
rank: number;
|
|
start: number;
|
|
end: number;
|
|
/** A reference read at top level before the declaration ran. */
|
|
reference: ts.Identifier | null;
|
|
/** A deferred reference (function body, parameter default, instance
|
|
* field) when user code may run before the declaration completes. */
|
|
deferredReference: ts.Identifier | null;
|
|
folded: ts.Identifier | null;
|
|
localNames: Map<string, string>;
|
|
}
|
|
|
|
/** For one import cycle with a static evaluation order: the first deferred
|
|
* reference to each candidate that code running before the candidate's
|
|
* declaration completes can reach, or null when the walk cannot be precise
|
|
* (top-level await, a default-exported function or class declaration).
|
|
*
|
|
* The cycle's top-level code runs in evaluation order. A stored callable
|
|
* (a function or class declaration, or a function, class or object-literal
|
|
* method stored by a variable initializer) becomes reachable once
|
|
* reachable code names its binding, through an import alias or a
|
|
* namespace member too; every other function-like in reachable code counts
|
|
* as reachable, since it may be handed to a callee. A namespace import of
|
|
* a cycle member used other than for a member read makes every stored
|
|
* callable reachable. moduleEarlyBindingsOf7 is the single-module form. */
|
|
function cycleReachedDeferredReferences7(
|
|
program: ts.Program,
|
|
evaluation: readonly ts.SourceFile[],
|
|
candidates: ReadonlyMap<ts.Symbol, CycleCandidate7>,
|
|
): Map<ts.Symbol, ts.Identifier> | null {
|
|
const checker = program.getTypeChecker();
|
|
const members = new Set(evaluation);
|
|
for (const sf of evaluation) {
|
|
if (hasTopLevelAwait7(sf)) return null;
|
|
for (const stmt of sf.statements.nodes) {
|
|
if (
|
|
(ts.isFunctionDeclaration(stmt) || ts.isClassDeclaration(stmt)) &&
|
|
(ts.getCombinedModifierFlags(stmt as unknown as ts.Declaration) &
|
|
ts.ModifierFlags.Default) !==
|
|
0
|
|
)
|
|
return null;
|
|
}
|
|
}
|
|
const stored = new Set<ts.Node>();
|
|
const immediate = new Map<ts.Node, ts.Node[]>();
|
|
const storedNames: ts.Identifier[] = [];
|
|
const unitsOfName = new Map<ts.Identifier, ts.Node[]>();
|
|
const store = (name: ts.Identifier, units: ts.Node[]): void => {
|
|
if (units.length === 0) return;
|
|
for (const u of units) stored.add(u);
|
|
storedNames.push(name);
|
|
unitsOfName.set(name, units);
|
|
};
|
|
for (const sf of evaluation) {
|
|
for (const stmt of sf.statements.nodes) {
|
|
if (
|
|
(ts.getCombinedModifierFlags(stmt as unknown as ts.Declaration) &
|
|
ts.ModifierFlags.Ambient) !==
|
|
0
|
|
)
|
|
continue;
|
|
if (ts.isFunctionDeclaration(stmt) && stmt.name !== undefined && stmt.body !== undefined) {
|
|
store(stmt.name, [stmt]);
|
|
} else if (ts.isClassDeclaration(stmt) && stmt.name !== undefined) {
|
|
store(stmt.name, [stmt]);
|
|
} else if (ts.isVariableStatement(stmt)) {
|
|
for (const d of stmt.declarationList.declarations.nodes) {
|
|
if (ts.isIdentifier(d.name) && d.initializer !== undefined) {
|
|
const units: ts.Node[] = [];
|
|
storedCallables7(checker, d.initializer, units, immediate);
|
|
store(d.name, units);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
const unitsBySymbol = new Map<ts.Symbol, ts.Node[]>();
|
|
const allUnits: ts.Node[] = [];
|
|
for (const name of storedNames) {
|
|
const symbol = checker.getSymbolAtLocation(name);
|
|
if (symbol === undefined) continue;
|
|
const units = unitsOfName.get(name)!;
|
|
unitsBySymbol.set(symbol, [...(unitsBySymbol.get(symbol) ?? []), ...units]);
|
|
for (const unit of units) allUnits.push(unit);
|
|
}
|
|
// A named class expression's inner name (`class Impl { static x =
|
|
// Impl.m(); }`) names the class inside its own body, as the binding it
|
|
// is stored in names it outside.
|
|
const innerNames: ts.Identifier[] = [];
|
|
for (const unit of allUnits) {
|
|
if (!ts.isClassExpression(unit) || unit.name === undefined) continue;
|
|
const symbol = checker.getSymbolAtLocation(unit.name);
|
|
if (symbol === undefined) continue;
|
|
innerNames.push(unit.name);
|
|
unitsBySymbol.set(symbol, [...(unitsBySymbol.get(symbol) ?? []), unit]);
|
|
}
|
|
// The identifier texts worth resolving: declared names, and the local
|
|
// names of imports (named, default and namespace) in cycle members.
|
|
const names = new Set<string>();
|
|
for (const c of candidates.values()) names.add(c.name.text);
|
|
for (const name of storedNames) names.add(name.text);
|
|
for (const name of innerNames) names.add(name.text);
|
|
for (const sf of evaluation) {
|
|
for (const stmt of sf.statements.nodes) {
|
|
if (!ts.isImportDeclaration(stmt) || stmt.importClause === undefined) continue;
|
|
const clause = stmt.importClause;
|
|
if (clause.name !== undefined) names.add(clause.name.text);
|
|
const bindings = clause.namedBindings;
|
|
if (bindings === undefined) continue;
|
|
if (ts.isNamedImports(bindings))
|
|
for (const element of bindings.elements.nodes) names.add(element.name.text);
|
|
else names.add(bindings.name.text);
|
|
}
|
|
}
|
|
const idents: ts.Identifier[] = [];
|
|
for (const sf of evaluation) {
|
|
ts.walkPreorder(sf, (node) => {
|
|
if (ts.isImportDeclaration(node)) return "skip";
|
|
if (ts.isIdentifier(node) && names.has(node.text)) idents.push(node);
|
|
return undefined;
|
|
});
|
|
}
|
|
checker.prefetchSymbolNodesExact(idents);
|
|
const symbolOf = (id: ts.Identifier): ts.Symbol | undefined => {
|
|
const parent = id.parent;
|
|
let symbol = checker.getSymbolAtLocation(id);
|
|
if (parent !== undefined && ts.isShorthandPropertyAssignment(parent) && parent.name === id) {
|
|
symbol = checker.getShorthandAssignmentValueSymbol(parent) ?? symbol;
|
|
}
|
|
if (symbol !== undefined && symbol.flags & ts.SymbolFlags.Alias) {
|
|
symbol = checker.getAliasedSymbol(symbol);
|
|
}
|
|
return symbol;
|
|
};
|
|
const isMemberNamespace = (symbol: ts.Symbol): boolean =>
|
|
(symbol.flags & ts.SymbolFlags.ValueModule) !== 0 &&
|
|
symbol.declarations.some((d) => ts.isSourceFile(d) && members.has(d));
|
|
const declared = new Set<ts.Symbol>();
|
|
const reachedUnits = new Set<ts.Node>();
|
|
const out = new Map<ts.Symbol, ts.Identifier>();
|
|
const work: ts.Node[] = [];
|
|
const reach = (unit: ts.Node): void => {
|
|
if (reachedUnits.has(unit)) return;
|
|
reachedUnits.add(unit);
|
|
work.push(unit);
|
|
};
|
|
const visit = (root: ts.Node): void => {
|
|
ts.walkPreorder(root, (node) => {
|
|
if (node !== root && stored.has(node)) {
|
|
for (const part of immediate.get(node) ?? []) visit(part);
|
|
return "skip";
|
|
}
|
|
if (ts.isImportDeclaration(node)) return "skip";
|
|
if (node.kind === ts.SyntaxKind.ThisKeyword) {
|
|
// `this` in a static initializer or block names the class, so it
|
|
// reaches the class's methods as naming the class does.
|
|
const owner = staticThisClass7(node);
|
|
if (owner !== undefined && stored.has(owner)) reach(owner);
|
|
return undefined;
|
|
}
|
|
if (!ts.isIdentifier(node) || !names.has(node.text)) return undefined;
|
|
if (inTypePosition7(node)) return undefined;
|
|
const parent = node.parent;
|
|
if (parent !== undefined && ts.isExportSpecifier(parent)) return undefined;
|
|
if (parent !== undefined && declaredName7(parent) === node) return undefined;
|
|
const symbol = symbolOf(node);
|
|
if (symbol === undefined) return undefined;
|
|
if (isMemberNamespace(symbol)) {
|
|
// `ns.member` resolves through the member's own identifier; any
|
|
// other use can reach every export.
|
|
if (
|
|
!(
|
|
parent !== undefined &&
|
|
ts.isPropertyAccessExpression(parent) &&
|
|
parent.expression === node
|
|
)
|
|
)
|
|
for (const unit of allUnits) reach(unit);
|
|
return undefined;
|
|
}
|
|
const c = candidates.get(symbol);
|
|
if (
|
|
c !== undefined &&
|
|
node !== c.name &&
|
|
!declared.has(symbol) &&
|
|
!out.has(symbol) &&
|
|
inDeferredPosition7(node) &&
|
|
!(
|
|
c.kind === "class" &&
|
|
ts.getSourceFileOfNode(node) === c.module &&
|
|
ts.tokenStartOfNode(node) >= c.start &&
|
|
node.end <= c.end
|
|
)
|
|
)
|
|
out.set(symbol, node);
|
|
for (const unit of unitsBySymbol.get(symbol) ?? []) reach(unit);
|
|
return undefined;
|
|
});
|
|
};
|
|
const drain = (): void => {
|
|
while (work.length > 0) visit(work.pop()!);
|
|
};
|
|
const declare = (name: ts.BindingName): void => {
|
|
for (const id of boundNames7(name)) {
|
|
const symbol = checker.getSymbolAtLocation(id);
|
|
if (symbol !== undefined) declared.add(symbol);
|
|
}
|
|
};
|
|
for (const sf of evaluation) {
|
|
for (const stmt of sf.statements.nodes) {
|
|
if (ts.isClassDeclaration(stmt)) {
|
|
for (const part of classSyncParts7(stmt)) visit(part);
|
|
drain();
|
|
if (stmt.name !== undefined) declare(stmt.name);
|
|
} else if (ts.isVariableStatement(stmt)) {
|
|
for (const d of stmt.declarationList.declarations.nodes) {
|
|
visit(d);
|
|
drain();
|
|
declare(d.name);
|
|
}
|
|
} else if (!ts.isFunctionDeclaration(stmt)) visit(stmt);
|
|
drain();
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** Top-level let/const and class bindings, in modules outside the
|
|
* exactly-initialized cycles, that code running before their declaration
|
|
* completes can read.
|
|
* Function declarations are callable from the module's start, so a call
|
|
* made by earlier top-level code (or by the declaration's own initializer)
|
|
* can observe the temporal dead zone. Reachability is syntactic: a stored
|
|
* callable (a function or class declaration, or a function, class or
|
|
* object-literal method stored by a variable initializer) becomes reachable
|
|
* once reachable code names its binding, and every other function-like in
|
|
* reachable code counts as reachable because it may be handed to a callee.
|
|
* A binding is reported only when one of its references in a deferred
|
|
* position is reachable before the declaration ran, so modules that call
|
|
* nothing early report nothing. Bindings of exactly-initialized cycles are
|
|
* covered by cycleEarlyBindings. */
|
|
export function moduleEarlyBindings(
|
|
program: ts.Program,
|
|
order: readonly ts.SourceFile[],
|
|
): CycleEarlyBinding[] {
|
|
const key = order.map((sf) => sf.fileName).join("\0");
|
|
const memo = program.analysis.moduleEarlyBindings;
|
|
if (memo !== null && memo.key === key) return memo.bindings;
|
|
const exact = program.analysis.exactCycleMembers;
|
|
const out: CycleEarlyBinding[] = [];
|
|
for (const sf of order) {
|
|
if (sf.isDeclarationFile || exact.has(sf)) continue;
|
|
for (const binding of moduleEarlyBindingsOf7(program, sf, order)) out.push(binding);
|
|
}
|
|
program.analysis.moduleEarlyBindings = { key, bindings: out };
|
|
return out;
|
|
}
|
|
|
|
function moduleEarlyBindingsOf7(
|
|
program: ts.Program,
|
|
sf: ts.SourceFile,
|
|
order: readonly ts.SourceFile[],
|
|
): CycleEarlyBinding[] {
|
|
const ambient = (stmt: ts.Statement): boolean =>
|
|
(ts.getCombinedModifierFlags(stmt as unknown as ts.Declaration) & ts.ModifierFlags.Ambient) !==
|
|
0;
|
|
// Candidate bindings, by declaring statement.
|
|
interface Candidate {
|
|
name: ts.Identifier;
|
|
index: number;
|
|
start: number;
|
|
/** A class declaration: its own body sees the inner binding. */
|
|
cls?: ts.ClassDeclaration;
|
|
}
|
|
const candidates: Candidate[] = [];
|
|
const candidateNames = new Set<string>();
|
|
// Stored callables, by the name of the binding that stores them.
|
|
const stored = new Set<ts.Node>();
|
|
const storedBy = new Map<string, { name: ts.Identifier; units: ts.Node[] }[]>();
|
|
// Parts of a stored node that run where it is evaluated: a class's
|
|
// heritage, computed keys, decorators and static initializers, and a
|
|
// stored method's computed key.
|
|
const immediate = new Map<ts.Node, ts.Node[]>();
|
|
const store = (name: ts.Identifier, units: ts.Node[]): void => {
|
|
if (units.length === 0) return;
|
|
for (const u of units) stored.add(u);
|
|
const list = storedBy.get(name.text) ?? [];
|
|
list.push({ name, units });
|
|
storedBy.set(name.text, list);
|
|
};
|
|
const checker = program.getTypeChecker();
|
|
const storedIn = (e: ts.Expression, units: ts.Node[]): void =>
|
|
storedCallables7(checker, e, units, immediate);
|
|
sf.statements.nodes.forEach((stmt, index) => {
|
|
if (ambient(stmt)) return;
|
|
if (ts.isFunctionDeclaration(stmt) && stmt.name !== undefined && stmt.body !== undefined) {
|
|
store(stmt.name, [stmt]);
|
|
} else if (ts.isClassDeclaration(stmt) && stmt.name !== undefined) {
|
|
store(stmt.name, [stmt]);
|
|
candidates.push({ name: stmt.name, index, start: ts.tokenStartOfNode(stmt), cls: stmt });
|
|
candidateNames.add(stmt.name.text);
|
|
} else if (ts.isVariableStatement(stmt)) {
|
|
const lexical = (stmt.declarationList.flags & (ts.NodeFlags.Let | ts.NodeFlags.Const)) !== 0;
|
|
for (const d of stmt.declarationList.declarations.nodes) {
|
|
if (ts.isIdentifier(d.name) && d.initializer !== undefined) {
|
|
const units: ts.Node[] = [];
|
|
storedIn(d.initializer, units);
|
|
store(d.name, units);
|
|
}
|
|
if (!lexical) continue;
|
|
for (const name of boundNames7(d.name)) {
|
|
candidates.push({ name, index, start: ts.tokenStartOfNode(stmt) });
|
|
candidateNames.add(name.text);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
if (candidates.length === 0) return [];
|
|
// Resolve every identifier that could name a candidate or a stored
|
|
// callable in one batch.
|
|
const idents: ts.Identifier[] = [];
|
|
ts.walkPreorder(sf, (node) => {
|
|
if (ts.isImportDeclaration(node)) return "skip";
|
|
if (ts.isIdentifier(node) && (candidateNames.has(node.text) || storedBy.has(node.text)))
|
|
idents.push(node);
|
|
return undefined;
|
|
});
|
|
checker.prefetchSymbolNodesExact(idents);
|
|
const symbolOf = (id: ts.Identifier): ts.Symbol | undefined => {
|
|
const parent = id.parent;
|
|
let symbol = checker.getSymbolAtLocation(id);
|
|
if (parent !== undefined && ts.isShorthandPropertyAssignment(parent) && parent.name === id) {
|
|
symbol = checker.getShorthandAssignmentValueSymbol(parent) ?? symbol;
|
|
}
|
|
if (symbol !== undefined && symbol.flags & ts.SymbolFlags.Alias) {
|
|
symbol = checker.getAliasedSymbol(symbol);
|
|
}
|
|
return symbol;
|
|
};
|
|
// A callback handed to a timer, a microtask queue, a promise reaction or
|
|
// a host (Node) API runs as a later job or event. Without top-level await
|
|
// the module's body completes before any job runs, so such a callback is
|
|
// never early. JavaScript builtins (array callbacks, the promise
|
|
// executor, replacers) and the host APIs that call back synchronously
|
|
// keep the conservative rule.
|
|
if (!hasTopLevelAwait7(sf)) {
|
|
const callbacks = (e: ts.Expression): void => {
|
|
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
|
if (ts.isArrowFunction(e) || ts.isFunctionExpression(e)) stored.add(e);
|
|
else if (ts.isObjectLiteralExpression(e)) {
|
|
for (const p of e.properties.nodes) {
|
|
if (ts.isMethodDeclaration(p)) stored.add(p);
|
|
else if (ts.isPropertyAssignment(p)) callbacks(p.initializer);
|
|
}
|
|
}
|
|
};
|
|
ts.walkPreorder(sf, (node) => {
|
|
if (
|
|
(ts.isCallExpression(node) || ts.isNewExpression(node)) &&
|
|
laterJobCallee7(checker, node.expression)
|
|
) {
|
|
for (const arg of node.arguments?.nodes ?? []) callbacks(arg);
|
|
}
|
|
return undefined;
|
|
});
|
|
}
|
|
const unitsBySymbol = new Map<ts.Symbol, ts.Node[]>();
|
|
for (const list of storedBy.values()) {
|
|
for (const { name, units } of list) {
|
|
const symbol = checker.getSymbolAtLocation(name);
|
|
if (symbol === undefined) continue;
|
|
unitsBySymbol.set(symbol, [...(unitsBySymbol.get(symbol) ?? []), ...units]);
|
|
}
|
|
}
|
|
const candidateSymbols = new Map<ts.Symbol, Candidate>();
|
|
for (const c of candidates) {
|
|
const symbol = checker.getSymbolAtLocation(c.name);
|
|
if (symbol !== undefined) candidateSymbols.set(symbol, c);
|
|
}
|
|
// Walk reachable code, collecting the candidate references it holds.
|
|
const reachedUnits = new Set<ts.Node>();
|
|
const reachedRefs = new Map<ts.Symbol, ts.Identifier>();
|
|
const work: ts.Node[] = [];
|
|
const visit = (root: ts.Node): void => {
|
|
ts.walkPreorder(root, (node) => {
|
|
if (node !== root && stored.has(node)) {
|
|
for (const part of immediate.get(node) ?? []) visit(part);
|
|
return "skip";
|
|
}
|
|
if (ts.isImportDeclaration(node)) return "skip";
|
|
if (!ts.isIdentifier(node)) return undefined;
|
|
if (!candidateNames.has(node.text) && !storedBy.has(node.text)) return undefined;
|
|
if (inTypePosition7(node)) return undefined;
|
|
const parent = node.parent;
|
|
if (parent !== undefined && ts.isExportSpecifier(parent)) return undefined;
|
|
if (parent !== undefined && declaredName7(parent) === node) return undefined;
|
|
const symbol = symbolOf(node);
|
|
if (symbol === undefined) return undefined;
|
|
const c = candidateSymbols.get(symbol);
|
|
if (
|
|
c !== undefined &&
|
|
node !== c.name &&
|
|
!reachedRefs.has(symbol) &&
|
|
!(c.cls !== undefined && ts.tokenStartOfNode(node) >= c.cls.pos && node.end <= c.cls.end)
|
|
) {
|
|
if (inDeferredPosition7(node) || ts.tokenStartOfNode(node) < c.start)
|
|
reachedRefs.set(symbol, node);
|
|
}
|
|
for (const unit of unitsBySymbol.get(symbol) ?? []) {
|
|
if (reachedUnits.has(unit)) continue;
|
|
reachedUnits.add(unit);
|
|
work.push(unit);
|
|
}
|
|
return undefined;
|
|
});
|
|
};
|
|
const drain = (): void => {
|
|
while (work.length > 0) visit(work.pop()!);
|
|
};
|
|
const out: CycleEarlyBinding[] = [];
|
|
const settled = new Set(order.filter((m) => m !== sf));
|
|
let next = 0;
|
|
const report = (declaration: ts.Node): void => {
|
|
while (
|
|
next < candidates.length &&
|
|
ts.tokenStartOfNode(candidates[next]!.name) >= ts.tokenStartOfNode(declaration) &&
|
|
candidates[next]!.name.end <= declaration.end
|
|
) {
|
|
const c = candidates[next++]!;
|
|
const symbol = checker.getSymbolAtLocation(c.name);
|
|
const reference = symbol === undefined ? undefined : reachedRefs.get(symbol);
|
|
if (symbol === undefined || reference === undefined) continue;
|
|
out.push({
|
|
symbol,
|
|
name: c.name,
|
|
kind: c.cls !== undefined ? "class" : "lexical",
|
|
module: sf,
|
|
reference,
|
|
settled,
|
|
});
|
|
}
|
|
};
|
|
for (const stmt of sf.statements.nodes) {
|
|
if (ts.isClassDeclaration(stmt)) {
|
|
// The binding initializes once the class definition, static
|
|
// initializers included, has run.
|
|
for (const part of classSyncParts7(stmt)) visit(part);
|
|
drain();
|
|
report(stmt);
|
|
} else if (ts.isVariableStatement(stmt)) {
|
|
// Declarators run in order: a later declarator's initializer cannot
|
|
// observe an earlier binding before it was initialized.
|
|
for (const d of stmt.declarationList.declarations.nodes) {
|
|
visit(d);
|
|
drain();
|
|
report(d);
|
|
}
|
|
} else if (!ts.isFunctionDeclaration(stmt)) visit(stmt);
|
|
drain();
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** True when the program function `callee` names reads its parameter
|
|
* `index` only inside function expressions it returns directly, and never
|
|
* through `arguments` in its own body: calling it cannot run that argument
|
|
* before the returned closure is called. */
|
|
function returnsOnlyClosuresOver7(
|
|
checker: ts.TypeChecker,
|
|
callee: ts.Expression,
|
|
index: number,
|
|
): boolean {
|
|
while (ts.isParenthesizedExpression(callee)) callee = callee.expression;
|
|
const name = ts.isIdentifier(callee)
|
|
? callee
|
|
: ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.name)
|
|
? callee.name
|
|
: undefined;
|
|
if (name === undefined) return false;
|
|
let symbol = checker.getSymbolAtLocation(name);
|
|
if (symbol !== undefined && symbol.flags & ts.SymbolFlags.Alias) {
|
|
symbol = checker.getAliasedSymbol(symbol);
|
|
}
|
|
const decls = symbol === undefined ? [] : symbol.declarations;
|
|
if (decls.length !== 1) return false;
|
|
const decl = decls[0]!;
|
|
if (ts.getSourceFileOfNode(decl).isDeclarationFile) return false;
|
|
let fn: ts.Node | undefined;
|
|
if (ts.isFunctionDeclaration(decl)) fn = decl;
|
|
else if (ts.isVariableDeclaration(decl) && decl.initializer !== undefined) {
|
|
let init: ts.Expression = decl.initializer;
|
|
while (ts.isParenthesizedExpression(init)) init = init.expression;
|
|
if (ts.isFunctionExpression(init) || ts.isArrowFunction(init)) fn = init;
|
|
}
|
|
if (fn === undefined) return false;
|
|
const like = fn as ts.FunctionLikeDeclaration;
|
|
const body = like.body;
|
|
const param = like.parameters.nodes[index];
|
|
if (body === undefined || !ts.isBlock(body) || param === undefined) return false;
|
|
if (!ts.isIdentifier(param.name) || param.dotDotDotToken !== undefined) return false;
|
|
if (like.parameters.nodes.slice(0, index).some((p) => p.dotDotDotToken !== undefined))
|
|
return false;
|
|
const paramName = param.name.text;
|
|
let deferred = true;
|
|
ts.walkPreorder(body, (node) => {
|
|
if (!deferred) return "stop";
|
|
if (ts.isFunctionLike(node) || ts.isClassExpression(node) || ts.isClassDeclaration(node)) {
|
|
let child: ts.Node = node;
|
|
while (ts.isParenthesizedExpression(child.parent)) child = child.parent;
|
|
const returned = ts.isReturnStatement(child.parent) && child.parent.expression === child;
|
|
const uses = (part: ts.Node): boolean => {
|
|
let found = false;
|
|
ts.walkPreorder(part, (inner) => {
|
|
if (ts.isIdentifier(inner) && inner.text === paramName) found = true;
|
|
return found ? "stop" : undefined;
|
|
});
|
|
return found;
|
|
};
|
|
// A returned closure may use the parameter; any other nested
|
|
// function could escape and run before the result is called. A
|
|
// returned class still evaluates its heritage, computed keys,
|
|
// decorators and static initializers when the callee runs.
|
|
if (returned) {
|
|
if (
|
|
(ts.isClassExpression(node) || ts.isClassDeclaration(node)) &&
|
|
classSyncParts7(node).some(uses)
|
|
)
|
|
deferred = false;
|
|
return "skip";
|
|
}
|
|
if (uses(node)) deferred = false;
|
|
return "skip";
|
|
}
|
|
if (ts.isIdentifier(node) && (node.text === paramName || node.text === "arguments")) {
|
|
if (node !== param.name) deferred = false;
|
|
}
|
|
return undefined;
|
|
});
|
|
return deferred;
|
|
}
|
|
|
|
/** The parts of a class that run where the class is evaluated: heritage
|
|
* expressions, decorators, computed member keys, static field
|
|
* initializers and static blocks. Methods and instance fields wait for a
|
|
* call or a construction. */
|
|
/** The class whose static field initializer or static block evaluates `this`
|
|
* (through arrow functions, which keep the enclosing `this`), or undefined
|
|
* when another function or a class's instance side binds it. */
|
|
function staticThisClass7(node: ts.Node): ts.ClassLikeDeclaration | undefined {
|
|
let child = node;
|
|
for (let n = node.parent; n !== undefined; child = n, n = n.parent) {
|
|
if (ts.isArrowFunction(n)) continue;
|
|
if (ts.isClassStaticBlockDeclaration(n)) return n.parent as ts.ClassLikeDeclaration;
|
|
if (ts.isPropertyDeclaration(n)) {
|
|
if (n.initializer !== child) return undefined;
|
|
return (ts.getCombinedModifierFlags(n) & ts.ModifierFlags.Static) !== 0
|
|
? (n.parent as ts.ClassLikeDeclaration)
|
|
: undefined;
|
|
}
|
|
if (ts.isComputedPropertyName(n)) {
|
|
// A computed key evaluates in the scope around the class.
|
|
const member = n.parent;
|
|
const cls = member?.parent;
|
|
if (cls === undefined) return undefined;
|
|
child = cls;
|
|
n = cls;
|
|
continue;
|
|
}
|
|
if (
|
|
ts.isFunctionLike(n) ||
|
|
ts.isClassDeclaration(n) ||
|
|
ts.isClassExpression(n) ||
|
|
ts.isSourceFile(n)
|
|
)
|
|
return undefined;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
function classSyncParts7(c: ts.ClassLikeDeclaration): ts.Node[] {
|
|
const decorators = (n: ts.Node): ts.Node[] =>
|
|
((n as { modifiers?: ts.NodeArray<ts.Node> }).modifiers?.nodes ?? []).filter(
|
|
(m) => m.kind === ts.SyntaxKind.Decorator,
|
|
);
|
|
const parts: ts.Node[] = [...decorators(c), ...(c.heritageClauses?.nodes ?? [])];
|
|
for (const m of c.members.nodes) {
|
|
parts.push(...decorators(m));
|
|
const memberName = (m as { name?: ts.PropertyName }).name;
|
|
if (memberName !== undefined && ts.isComputedPropertyName(memberName)) parts.push(memberName);
|
|
if (ts.isClassStaticBlockDeclaration(m)) parts.push(m);
|
|
else if (
|
|
ts.isPropertyDeclaration(m) &&
|
|
m.initializer !== undefined &&
|
|
(ts.getCombinedModifierFlags(m) & ts.ModifierFlags.Static) !== 0
|
|
)
|
|
parts.push(m.initializer);
|
|
}
|
|
return parts;
|
|
}
|
|
|
|
/** An identifier or a chain of plain property reads. */
|
|
function memberChainKey7(e: ts.Expression): boolean {
|
|
while (ts.isPropertyAccessExpression(e)) e = e.expression;
|
|
return ts.isIdentifier(e);
|
|
}
|
|
|
|
/** The name a declaration node binds, if any. */
|
|
function declaredName7(node: ts.Node): ts.Node | undefined {
|
|
if (
|
|
ts.isVariableDeclaration(node) ||
|
|
ts.isFunctionDeclaration(node) ||
|
|
ts.isFunctionExpression(node) ||
|
|
ts.isClassDeclaration(node) ||
|
|
ts.isClassExpression(node) ||
|
|
ts.isBindingElement(node) ||
|
|
ts.isParameter(node)
|
|
)
|
|
return node.name;
|
|
return undefined;
|
|
}
|
|
|
|
/** A declared builtin whose callback arguments run only as later jobs or
|
|
* events: the timers, queueMicrotask, promise reactions, and Node APIs
|
|
* other than the ones that call back before returning. */
|
|
function laterJobCallee7(checker: ts.TypeChecker, callee: ts.Expression): boolean {
|
|
while (ts.isParenthesizedExpression(callee)) callee = callee.expression;
|
|
let name: ts.Identifier;
|
|
if (ts.isIdentifier(callee)) name = callee;
|
|
else if (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.name)) {
|
|
name = callee.name;
|
|
} else return false;
|
|
if (SYNC_CALLBACK_HOST_APIS7.has(name.text)) return false;
|
|
let root: ts.Expression = callee;
|
|
while (ts.isPropertyAccessExpression(root)) root = root.expression;
|
|
if (ts.isIdentifier(root) && root.text === "assert") return false;
|
|
let symbol = checker.getSymbolAtLocation(name);
|
|
if (symbol !== undefined && symbol.flags & ts.SymbolFlags.Alias) {
|
|
symbol = checker.getAliasedSymbol(symbol);
|
|
}
|
|
if (symbol === undefined) return false;
|
|
const decls = symbol.declarations;
|
|
if (decls.length === 0 || !decls.every((d) => ts.getSourceFileOfNode(d).isDeclarationFile))
|
|
return false;
|
|
if (LATER_JOB_BUILTINS7.has(name.text)) return true;
|
|
// The JavaScript standard library's callbacks mostly run synchronously;
|
|
// package declarations describe user code.
|
|
return decls.every((d) => {
|
|
const file = ts.getSourceFileOfNode(d).fileName;
|
|
return isNodeTypesPath(file) || isFallbackDtsPath(file);
|
|
});
|
|
}
|
|
|
|
/** Standard-library functions whose callbacks run as later jobs. */
|
|
const LATER_JOB_BUILTINS7 = new Set([
|
|
"setTimeout",
|
|
"setInterval",
|
|
"setImmediate",
|
|
"queueMicrotask",
|
|
"then",
|
|
"catch",
|
|
"finally",
|
|
]);
|
|
|
|
/** Host APIs that can invoke a callback argument before returning. */
|
|
const SYNC_CALLBACK_HOST_APIS7 = new Set([
|
|
"apply",
|
|
"bind",
|
|
"call",
|
|
"doesNotReject",
|
|
"doesNotThrow",
|
|
"emit",
|
|
"enterWith",
|
|
"exit",
|
|
"rejects",
|
|
"run",
|
|
"runInAsyncScope",
|
|
"throws",
|
|
]);
|
|
|
|
/** True for a reference whose value the lowering can take from the
|
|
* binding's checker type instead of its storage: a property key (element
|
|
* access argument, computed name, `in` operand, through parentheses,
|
|
* casts and template spans) or a `typeof` operand. */
|
|
function foldablePosition7(ref: ts.Identifier): boolean {
|
|
let child: ts.Node = ref;
|
|
for (let p: ts.Node | undefined = ref.parent; p !== undefined; p = p.parent) {
|
|
if (
|
|
ts.isParenthesizedExpression(p) ||
|
|
ts.isAsExpression(p) ||
|
|
ts.isSatisfiesExpression(p) ||
|
|
ts.isNonNullExpression(p) ||
|
|
ts.isTemplateSpan(p) ||
|
|
ts.isTemplateExpression(p)
|
|
) {
|
|
child = p;
|
|
continue;
|
|
}
|
|
if (ts.isTypeOfExpression(p)) return true;
|
|
if (ts.isElementAccessExpression(p)) return p.argumentExpression === child;
|
|
if (ts.isComputedPropertyName(p)) return true;
|
|
if (ts.isBinaryExpression(p))
|
|
return p.operatorToken.kind === ts.SyntaxKind.InKeyword && p.left === child;
|
|
return false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** The identifiers a binding name (identifier or destructuring pattern)
|
|
* declares. */
|
|
function boundNames7(name: ts.BindingName): ts.Identifier[] {
|
|
if (ts.isIdentifier(name)) return [name];
|
|
const out: ts.Identifier[] = [];
|
|
for (const el of name.elements.nodes) {
|
|
if (ts.isOmittedExpression(el) || el.name === undefined) continue;
|
|
out.push(...boundNames7(el.name));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** A namespace declaration whose body holds only types (interfaces, type
|
|
* aliases, nested type-only namespaces) — no runtime object exists. */
|
|
function typeOnlyNamespace7(decl: ts.ModuleDeclaration): boolean {
|
|
let body = decl.body;
|
|
while (body !== undefined && ts.isModuleDeclaration(body)) body = body.body;
|
|
if (body === undefined || !ts.isModuleBlock(body)) return true;
|
|
return body.statements.nodes.every(
|
|
(s) =>
|
|
ts.isInterfaceDeclaration(s) ||
|
|
ts.isTypeAliasDeclaration(s) ||
|
|
(ts.isModuleDeclaration(s) && typeOnlyNamespace7(s)),
|
|
);
|
|
}
|
|
|
|
/** Resolves an import specifier from `from` to a source file of the
|
|
* program, or null (unresolvable / outside the program). Exported (as
|
|
* resolveImport) for the lowering: CommonJS require statements lower to
|
|
* guarded %init calls of exactly the module preflight resolved here. */
|
|
function resolveImport7(
|
|
program: ts.Program,
|
|
from: ts.SourceFile,
|
|
specifier: string,
|
|
resolutionKind: "import" | "require" = "import",
|
|
): ts.SourceFile | null {
|
|
const resolved = resolveProjectModule(from.fileName, specifier);
|
|
if (resolved !== null) return program.getSourceFile(resolved) ?? null;
|
|
const npm = resolveNpmImport7(from.fileName, specifier, resolutionKind);
|
|
return npm && isNpmStaticPackage(npm.packageName)
|
|
? npmStaticProgramDep(program, npm.packageName, npm.typesFile)
|
|
: null;
|
|
}
|
|
|
|
function resolveNpmImport7(
|
|
fromFileName: string,
|
|
specifier: string,
|
|
resolutionKind: "import" | "require" = "import",
|
|
): { packageName: string; version?: string; typesFile: string } | null {
|
|
if (isRelativeSpecifier(specifier) || specifier.startsWith("node:")) {
|
|
return null;
|
|
}
|
|
// Node resolves a bare core-module name (`string_decoder`, `events`,
|
|
// `fs/promises`) to the builtin before it looks in node_modules, so an
|
|
// installed npm package of the same name never answers it. Prefix-only
|
|
// builtins (`node:test`) are listed with their prefix, so a bare `test`
|
|
// still names the package.
|
|
if (nodeBuiltinNames.has(specifier)) return null;
|
|
// --provenance-sources: a registered specifier is NOT an npm import —
|
|
// its attested source compiles as program modules (resolveProjectModule
|
|
|
|
if (provenanceEntryFor(specifier) !== null) return null;
|
|
// Shimmed and never-loaded packages (package-shims.ts) are not npm
|
|
// imports either: the shim compiles as a program module, and an unloaded
|
|
// package's require() is Node's MODULE_NOT_FOUND.
|
|
if (isShimmedImport(fromFileName, specifier)) return null;
|
|
const resolved = resolveBareModule(fromFileName, specifier, undefined, resolutionKind);
|
|
if (!resolved) return null;
|
|
if (!isNodeModulesPath(resolved.typesFile)) {
|
|
// A workspace-linked package (the node_modules entry is a symlink into
|
|
// the project — every monorepo tool's internal-package install): Node
|
|
// resolves and runs it exactly like any installed package, so it IS an
|
|
// npm import; the realpath'd answer escaping node_modules must not
|
|
// refuse the edge. Registered so path-keyed package attribution
|
|
// (workspace-registry.ts) recognizes the package's real files. Anything else
|
|
// whose answer left node_modules stays refused.
|
|
if (resolved.workspaceDir === undefined) return null;
|
|
registerWorkspacePackage(resolved.packageName, resolved.workspaceDir);
|
|
}
|
|
return {
|
|
packageName: resolved.packageName,
|
|
...(resolved.version !== undefined ? { version: resolved.version } : {}),
|
|
typesFile: resolved.typesFile,
|
|
};
|
|
}
|
|
|
|
function locOf7(node: ts.Node): { file: string; start: number; end: number } {
|
|
const sf = ts.getSourceFileOfNode(node);
|
|
return { file: sf.fileName, start: ts.tokenStartOfNode(node), end: node.end };
|
|
}
|
|
|
|
/** The program file holding an opted-in package's resolved runtime JS:
|
|
* the file itself, or its same-stem TypeScript twin. */
|
|
function npmStaticProgramFile(program: ts.Program, resolvedFile: string): ts.SourceFile | null {
|
|
let dep = program.getSourceFile(resolvedFile) ?? null;
|
|
if (dep === null) {
|
|
// A workspace-linked package shipping its .ts SOURCE beside the
|
|
// compiled .js: the checker's resolution preferred the source (it
|
|
// outranks the sibling .js under bundler resolution), so the program
|
|
// holds the .ts twin at the same stem — the same module, in its
|
|
// richer spelling.
|
|
for (const ext of [".ts", ".tsx", ".mts", ".cts"]) {
|
|
dep = program.getSourceFile(resolvedFile.replace(/\.(js|mjs|cjs)$/, ext)) ?? null;
|
|
if (dep !== null) break;
|
|
}
|
|
}
|
|
return dep;
|
|
}
|
|
|
|
/** The PROGRAM source file an opted-in --npm-static package's resolved
|
|
* entry maps to, or null with the package marked an offender (resolution
|
|
* still answering declarations, or the file missing from the type-checked
|
|
* program — a path-identity mismatch between the two resolvers). */
|
|
function npmStaticProgramDep(
|
|
program: ts.Program,
|
|
packageName: string,
|
|
resolvedFile: string,
|
|
): ts.SourceFile | null {
|
|
if (!isJsSourceFileName(resolvedFile)) {
|
|
reportNpmStaticOffender(
|
|
packageName,
|
|
`its import resolves to '${resolvedFile}', not to runtime JavaScript`,
|
|
);
|
|
return null;
|
|
}
|
|
const dep = npmStaticProgramFile(program, resolvedFile);
|
|
if (dep === null) {
|
|
reportNpmStaticOffender(
|
|
packageName,
|
|
`its entry '${resolvedFile}' did not join the type-checked program`,
|
|
);
|
|
return null;
|
|
}
|
|
return dep;
|
|
}
|
|
|
|
/** `const process = require('node:process')` (identifier binding) and the
|
|
* bare side-effect `require('node:process')`: Node's process module IS
|
|
* the global process object, so the binding is a stdlib-global alias
|
|
* (surfaces.ts registers it; reads lower through the process surface) and
|
|
* the bare load is a no-op. These forms are exempt from the SC1010
|
|
* builtin fence; every other shape (destructuring, subpath) keeps it. */
|
|
function processModuleAliasRequire7(spec: string, decl: ts.VariableDeclaration | null): boolean {
|
|
if (spec !== "process" && spec !== "node:process") return false;
|
|
return decl === null || ts.isIdentifier(decl.name);
|
|
}
|
|
|
|
/** `import process from 'node:process'`: the ESM spelling of the same
|
|
* global-object alias as processModuleAliasRequire7. Keep this exemption
|
|
* deliberately narrow: named and namespace imports need a represented
|
|
* module namespace, while the default binding is exactly globalThis.process. */
|
|
function processModuleAliasImport7(spec: string, stmt: ts.ImportDeclaration): boolean {
|
|
if (spec !== "process" && spec !== "node:process") return false;
|
|
const clause = stmt.importClause;
|
|
return clause !== undefined && clause.name !== undefined && clause.namedBindings === undefined;
|
|
}
|
|
|
|
/** The whole TS7-lane lifecycle for one entry: spawn (or share) a tsgo
|
|
* host, build the lowering-world program, run preflight, and release its
|
|
* snapshot. The caller owns the supplied host and can reuse or close it. */
|
|
export function checkPreflightTs7(
|
|
entryPath: string,
|
|
host: ts.Ts7Host,
|
|
): { diags: ScrDiagnostic[]; moduleOrder: string[] } {
|
|
const load = loadProgram7(host, resolve(entryPath));
|
|
try {
|
|
const { diags, moduleOrder } = preflight7(load);
|
|
return { diags, moduleOrder: moduleOrder.map((sf) => sf.fileName) };
|
|
} finally {
|
|
load.disposeAll();
|
|
}
|
|
}
|
|
|
|
function preflight7(load: LoadResult): {
|
|
diags: ScrDiagnostic[];
|
|
moduleOrder: ts.SourceFile[];
|
|
startupCrash: StartupCrash | null;
|
|
inlineRequireCycleCalls: ReadonlySet<ts.Node>;
|
|
moduleEdges?: ReadonlyMap<ts.SourceFile, readonly CycleEdge[]>;
|
|
dynamicImportRejections?: ReadonlyMap<ts.SourceFile, StartupCrash>;
|
|
} {
|
|
const { program, entry } = load;
|
|
const diags: ScrDiagnostic[] = [...load.configDiags];
|
|
|
|
// An explicit entry can itself live in an installed package. Its own
|
|
// source files are program code, including relative barrels and sibling
|
|
// modules; their location must not erase the module graph.
|
|
// Match the full installed directory, not just the package name, and
|
|
// exclude nested installations so dependencies keep their npm policy.
|
|
const entryPackageFile = entryPackageFilePredicate(entry.fileName);
|
|
|
|
// Workspace-linked packages register BEFORE the tsc gate. Their files
|
|
// live at realpaths OUTSIDE node_modules (the monorepo-tool symlink
|
|
// shape), so nothing path-shaped marks them as npm surface — yet their
|
|
// shipped JS follows the same foreign-tsconfig rules as node_modules JS.
|
|
// The lazy
|
|
// registration inside the import-fence walk below runs AFTER errorsOf,
|
|
// too late for the gate, so one pass over the program's import edges
|
|
// registers them up front: every bare specifier any program file loads —
|
|
// import/export declarations, dynamic import("literal") (tsgo chases
|
|
// those into the program too; a CLI reaches its workspace sibling exactly
|
|
// that way), require("literal") — resolved with the own resolver,
|
|
// workspace answers recorded.
|
|
{
|
|
const supportedBuiltins = new Set<string>(SUPPORTED_NODE_MODULES);
|
|
const probed = new Set<string>();
|
|
const probe = (fromFile: string, spec: string): void => {
|
|
if (
|
|
isRelativeSpecifier(spec) ||
|
|
spec.startsWith("#") ||
|
|
spec.startsWith("node:") ||
|
|
supportedBuiltins.has(spec)
|
|
) {
|
|
return;
|
|
}
|
|
const key = `${dirname(fromFile)} ${spec}`;
|
|
if (probed.has(key)) return;
|
|
probed.add(key);
|
|
const r = resolveBareModule(fromFile, spec);
|
|
if (r !== null && r.workspaceDir !== undefined)
|
|
registerWorkspacePackage(r.packageName, r.workspaceDir);
|
|
};
|
|
for (const sf of program.getImplementationSourceFiles()) {
|
|
if (sf.isDeclarationFile || sf.fileName.endsWith(".json")) continue;
|
|
for (const specifier of moduleSourceCandidates(program, sf).specifiers)
|
|
probe(sf.fileName, specifier);
|
|
}
|
|
}
|
|
|
|
const toPassthrough = (d: ts.Diagnostic): ScrDiagnostic => {
|
|
const message = ts.flattenDiagnosticMessageText(d, "\n");
|
|
// File-less (global/options) diagnostics anchor at the entry with a
|
|
// zero span, exactly like the 5.9.3 lane's `d.start ?? 0` fallback.
|
|
const file = d.fileName ?? entry.fileName;
|
|
const start = d.fileName !== undefined ? d.pos : 0;
|
|
const end = d.fileName !== undefined ? d.end : 0;
|
|
const diag = tscPassthroughDiag(message, { file, start, end });
|
|
const hint = typeWorldHint(d, message, load);
|
|
if (hint !== null) {
|
|
diag.typeWorld = true;
|
|
diag.hint = hint;
|
|
}
|
|
return diag;
|
|
};
|
|
/* TS7 checker change (not finding 5, same discipline): tsgo types a
|
|
* NAMESPACE import as the spec's non-callable module namespace object
|
|
* even when the module is declared with the callable `export =` shape —
|
|
* 5.9.3 bound the callable value directly, so `import * as a from
|
|
* "node:assert"; a(x)` typechecked there and draws TS2349 here. Those
|
|
* callable module objects are scriptc's OWN declared surface, and calls
|
|
* through the namespace binding are fenced per site by the lowering (the
|
|
* assert-fences fixture pins the wording), so tsgo's extra TS2349 on
|
|
* exactly those callees — an identifier that is a namespace-import
|
|
* binding of a supported builtin module — is suppressed to the 5.9.3
|
|
* verdict. Nothing else rides this: any other non-callable call keeps
|
|
* tsc's voice in both lanes. */
|
|
const builtinNamespaceNames = new Map<string, Set<string>>();
|
|
const namespaceCalleeSuppressed = (p: ts.Program, d: ts.Diagnostic): boolean => {
|
|
if (d.code !== 2349 || d.fileName === undefined) return false;
|
|
let names = builtinNamespaceNames.get(d.fileName);
|
|
if (!names) {
|
|
names = new Set();
|
|
builtinNamespaceNames.set(d.fileName, names);
|
|
const sf = p.getSourceFile(d.fileName);
|
|
for (const stmt of sf?.statements?.nodes ?? []) {
|
|
if (!ts.isImportDeclaration(stmt) || !ts.isStringLiteral(stmt.moduleSpecifier)) continue;
|
|
if (canonicalBuiltinModule(stmt.moduleSpecifier.text) === null) continue;
|
|
const bindings = stmt.importClause?.namedBindings;
|
|
if (bindings && ts.isNamespaceImport(bindings)) names.add(bindings.name.text);
|
|
}
|
|
}
|
|
if (names.size === 0) return false;
|
|
const sf = p.getSourceFile(d.fileName);
|
|
return sf !== undefined && names.has(sf.text.slice(d.pos, d.end));
|
|
};
|
|
|
|
const npmStaticFileSuppressed = (d: ts.Diagnostic): boolean =>
|
|
d.fileName !== undefined && npmStaticPackageOfPath(d.fileName) !== null;
|
|
|
|
const unselectedPackageJsFile = (file: string): boolean =>
|
|
isJsSourceFileName(file) &&
|
|
!entryPackageFile(file) &&
|
|
npmStaticPackageOfPath(file) === null &&
|
|
(isNodeModulesPath(file) || workspacePackageOfPath(file) !== null);
|
|
const nodeModulesJsSuppressed = (d: ts.Diagnostic): boolean =>
|
|
d.fileName !== undefined && unselectedPackageJsFile(d.fileName);
|
|
/* JSDoc TYPE positions in JS files are documentation Node never reads:
|
|
* a name-resolution failure THERE (2304/2552 — the pattern's utilities.js
|
|
* spells a mapped type over a @template name it never declared) types
|
|
* as the error-any and the per-site fences apply, same stance as the
|
|
* strictness families above. A 2300 duplicate-identifier PAIR formed by
|
|
* a @typedef and a same-named class/function is the same story — the
|
|
* typedef half sits inside the comment, and the code half is suppressed
|
|
* exactly when its partner (same file, same name) was comment-side, so
|
|
* a REAL duplicate declaration (both halves in code — Node's own
|
|
* SyntaxError) keeps tsc's voice. */
|
|
const insideBlockComment = (text: string, pos: number): boolean => {
|
|
const open = text.lastIndexOf("/*", pos);
|
|
if (open < 0) return false;
|
|
const close = text.indexOf("*/", open + 2);
|
|
return close === -1 || close >= pos;
|
|
};
|
|
const jsdocTypeSuppressed = (
|
|
p: ts.Program,
|
|
d: ts.Diagnostic,
|
|
commentDup: Set<string>,
|
|
): boolean => {
|
|
if (d.fileName === undefined || !isJsSourceFileName(d.fileName)) return false;
|
|
if (d.code !== 2304 && d.code !== 2552 && d.code !== 2300 && d.code !== 1003) return false;
|
|
if (d.pos === undefined) return false;
|
|
const sf = p.getSourceFile(d.fileName);
|
|
if (!sf) return false;
|
|
if (insideBlockComment(sf.text, d.pos)) return true;
|
|
return d.code === 2300 && commentDup.has(`${d.fileName}:${sf.text.slice(d.pos, d.end)}`);
|
|
};
|
|
|
|
/* `typeof window` / `typeof self` in a JavaScript file: the operand of
|
|
* typeof may name a global nothing declares (Node answers "undefined"
|
|
* and never throws), the feature-detection idiom every isomorphic
|
|
* package uses, and the guarded uses of that name (`typeof window < "u"
|
|
* ? window : …`) are code Node never runs. A name the file feature-tests
|
|
* this way is therefore not a type error in JavaScript; a reached use
|
|
* still meets the lowering's located refusal. TypeScript files keep the
|
|
* error and its hint. */
|
|
const typeofTestedNameSuppressed = (p: ts.Program, d: ts.Diagnostic): boolean => {
|
|
if (d.code !== 2304 && d.code !== 2552 && d.code !== 2584) return false;
|
|
if (d.fileName === undefined || !isJsSourceFileName(d.fileName)) return false;
|
|
if (d.pos === undefined || d.end === undefined) return false;
|
|
const sf = p.getSourceFile(d.fileName);
|
|
if (!sf) return false;
|
|
const name = sf.text.slice(d.pos, d.end);
|
|
if (!/^[A-Za-z_$][\w$]*$/.test(name)) return false;
|
|
return new RegExp(`\\btypeof\\s*\\(?\\s*${name.replace(/\$/g, "\\$")}(?![\\w$])`).test(sf.text);
|
|
};
|
|
|
|
// A `return` at the top level of a CommonJS module (TS1108): Node runs
|
|
// the module as a function body, so the return ends the module's
|
|
// evaluation. ES modules and TypeScript keep the error.
|
|
const cjsTopLevelReturnSuppressed = (p: ts.Program, d: ts.Diagnostic): boolean => {
|
|
if (d.code !== 1108 || d.fileName === undefined || !isJsSourceFileName(d.fileName))
|
|
return false;
|
|
const sf = p.getSourceFile(d.fileName);
|
|
return sf !== undefined && isCjsJsFile7(sf, p);
|
|
};
|
|
|
|
const workspaceImplicitAnySuppressed = (p: ts.Program, d: ts.Diagnostic): boolean => {
|
|
if (d.code !== 7016 || d.fileName === undefined || d.pos === undefined || d.end === undefined)
|
|
return false;
|
|
const sf = p.getSourceFile(d.fileName);
|
|
if (!sf) return false;
|
|
const spec = sf.text.slice(d.pos, d.end).replace(/^['"]|['"]$/g, "");
|
|
const prefix = spec.startsWith("@")
|
|
? spec.split("/").slice(0, 2).join("/")
|
|
: spec.split("/")[0]!;
|
|
return isWorkspacePackageName(prefix);
|
|
};
|
|
// A JavaScript file that TypeScript checks keeps the type errors that
|
|
// protect its JSDoc types: a value of another type reaching a typed slot,
|
|
// a call with the wrong arity, and a subclass member that does not
|
|
// match its base.
|
|
const projectCheckJs = load.typeWorld?.projectCheckJs ?? false;
|
|
const enforcedInCheckedJs = (p: ts.Program, d: ts.Diagnostic): boolean => {
|
|
if (!CHECKED_JS_ENFORCED_CODES.has(d.code) || d.fileName === undefined) return false;
|
|
const sf = p.getSourceFile(d.fileName);
|
|
return sf !== undefined && isCheckedJsSourceFile(sf, projectCheckJs);
|
|
};
|
|
// keptErrors drops every diagnostic of these files: the gate does not ask
|
|
// the checker for them.
|
|
const diagnosticsKept = (fileName: string): boolean =>
|
|
npmStaticPackageOfPath(fileName) === null &&
|
|
!unselectedPackageJsFile(fileName) &&
|
|
!isPackageShimFile(fileName);
|
|
const errorsOf = (p: ts.Program): ts.Diagnostic[] =>
|
|
keptErrors(p, ts.getPreEmitDiagnostics(p, diagnosticsKept));
|
|
const keptErrors = (p: ts.Program, all: readonly ts.Diagnostic[]): ts.Diagnostic[] => {
|
|
// First pass: every comment-side 2300's (file, name) — the partners
|
|
// the second pass forgives.
|
|
const commentDup = new Set<string>();
|
|
for (const d of all) {
|
|
if (d.code !== 2300 || d.fileName === undefined || d.pos === undefined || d.end === undefined)
|
|
continue;
|
|
if (!isJsSourceFileName(d.fileName)) continue;
|
|
const sf = p.getSourceFile(d.fileName);
|
|
if (sf && insideBlockComment(sf.text, d.pos))
|
|
commentDup.add(`${d.fileName}:${sf.text.slice(d.pos, d.end)}`);
|
|
}
|
|
return all.filter(
|
|
(d) =>
|
|
d.category === ts.DiagnosticCategory.Error &&
|
|
(!suppressedJsStrictness7(d) || enforcedInCheckedJs(p, d)) &&
|
|
!npmStaticFileSuppressed(d) &&
|
|
!nodeModulesJsSuppressed(d) &&
|
|
(d.fileName === undefined || !isPackageShimFile(d.fileName)) &&
|
|
!namespaceCalleeSuppressed(p, d) &&
|
|
!inferredJsDiagnosticSuppressed(p, d) &&
|
|
!isNpmStaticSubclassArgument(p, d) &&
|
|
!workspaceImplicitAnySuppressed(p, d) &&
|
|
!typeofTestedNameSuppressed(p, d) &&
|
|
!cjsTopLevelReturnSuppressed(p, d) &&
|
|
!jsdocTypeSuppressed(p, d, commentDup),
|
|
);
|
|
};
|
|
|
|
// The tsc gate, with a second chance: the LOWERING world's checker
|
|
// includes the divergence overrides (JSON.parse(): unknown, the Promise
|
|
// executor shape, ...), which are TIGHTER than the standard lib — a
|
|
// project that typechecks clean under its own tsc can still error here
|
|
// (any-typed JSON.parse results, most commonly). Those errors are OURS,
|
|
// not the project's, so they must not fail preflight: when the lowering
|
|
// world has errors, the PROJECT world (same program without the override
|
|
// declarations) is built and consulted. Clean there → preflight passes
|
|
// and the override-affected sites meet the lowerer's honest fences
|
|
// instead (the SC1100 checked-cast family). Dirty there → the project-
|
|
// world errors are the ones reported: they are reproducible with the
|
|
// project's own tsc, which is what "fix type errors first" asks for.
|
|
// With JSX sources, the program over the original text answers the tsc
|
|
// gate; its positions move into the rewritten text the program holds.
|
|
// Errors only the rewritten text has come from the calls the JSX lowers
|
|
// to (a factory whose signature does not accept them).
|
|
const tscWorld = load.tscWorld;
|
|
const tscErrors =
|
|
tscWorld === undefined ? errorsOf(program) : keptErrors(tscWorld.program, tscWorld.diagnostics);
|
|
if (tscErrors.length > 0) {
|
|
const world =
|
|
tscWorld === undefined || tscWorld.declWorldFallback === true
|
|
? load.projectWorld()
|
|
: (tscWorld.projectWorld ?? tscWorld.program);
|
|
for (const d of errorsOf(world)) {
|
|
const diag = toPassthrough(d);
|
|
const lowered = tscWorld === undefined ? undefined : load.jsxSources?.get(diag.loc.file);
|
|
if (lowered !== undefined) {
|
|
diag.loc = {
|
|
...diag.loc,
|
|
start: lowered.rewrittenPosition(diag.loc.start, false),
|
|
end: lowered.rewrittenPosition(diag.loc.end, true),
|
|
};
|
|
}
|
|
diags.push(diag);
|
|
}
|
|
} else if (load.jsxSources !== undefined) {
|
|
for (const file of load.jsxSources.keys()) {
|
|
const sf = program.getSourceFile(file);
|
|
if (sf === undefined) continue;
|
|
const own = [...program.getSyntacticDiagnostics(sf), ...program.getSemanticDiagnostics(sf)];
|
|
for (const d of keptErrors(program, own)) {
|
|
const diag = toPassthrough(d);
|
|
diag.message = `${diag.message} (in the call this JSX lowers to)`;
|
|
diags.push(diag);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Preflight and lowering share this CheckerFacade. Claim executable
|
|
// source files after workspace discovery and the tsc gate but before
|
|
// structural preflight checker queries, so a miss cannot trigger the old
|
|
// whole-file sweep (and eagerly fetch every dead body's answers). The
|
|
// structure wave includes declaration headers and top-level code;
|
|
// reachable bodies are added by lowering's worklists.
|
|
const ambient = ambientDtsPath();
|
|
const programFiles = program
|
|
.getImplementationSourceFiles()
|
|
.filter(
|
|
(sf) =>
|
|
sf.fileName !== ambient &&
|
|
!sf.isDeclarationFile &&
|
|
!sf.fileName.endsWith(".json") &&
|
|
(!isNodeModulesPath(sf.fileName) ||
|
|
entryPackageFile(sf.fileName) ||
|
|
npmStaticPackageOfPath(sf.fileName) !== null) &&
|
|
!unselectedPackageJsFile(sf.fileName),
|
|
);
|
|
// Files reached only through `import type` / `export type … from` are
|
|
// checker input, never program modules: Node does not load them, so
|
|
// their imports and syntax must not fence the program.
|
|
const resolveRuntimeEdge = (
|
|
sf: ts.SourceFile,
|
|
spec: string,
|
|
resolutionKind?: "import" | "require",
|
|
): ts.SourceFile | null =>
|
|
resolveImport7(program, sf, spec, resolutionKind) ??
|
|
npmStaticDepSf7(program, sf, spec, resolutionKind);
|
|
const typeOnlyFiles = typeOnlyReachedFiles(
|
|
program,
|
|
entry,
|
|
programFiles,
|
|
() => [
|
|
...forkTargetPaths(program, program.getImplementationSourceFiles()),
|
|
...workerTargetPaths(program, program.getImplementationSourceFiles()),
|
|
],
|
|
resolveRuntimeEdge,
|
|
);
|
|
const runtimeFiles =
|
|
typeOnlyFiles.size === 0 ? programFiles : programFiles.filter((sf) => !typeOnlyFiles.has(sf));
|
|
const userFiles = npmStaticActive()
|
|
? planNpmStaticReexports(
|
|
program,
|
|
entry,
|
|
runtimeFiles,
|
|
[
|
|
...createRequireProgramRoots7(program),
|
|
...forkTargetPaths(program, program.getImplementationSourceFiles()),
|
|
...workerTargetPaths(program, program.getImplementationSourceFiles()),
|
|
],
|
|
resolveRuntimeEdge,
|
|
)
|
|
: runtimeFiles;
|
|
program.getTypeChecker().prefetchSourceFileStructures(userFiles);
|
|
|
|
// Unselected node_modules JS is not native program source even when
|
|
// maxNodeModuleJsDepth pulled it into the checker (nodeModulesJsSuppressed).
|
|
// Node stops at the nearest package.json even when it is malformed, and
|
|
// an explicit CommonJS scope (or .cjs/.cts extension) disables ambiguous-
|
|
// file syntax detection entirely. TypeScript's bundler checker models
|
|
// neither loader rule, so restore them before any module-edge analysis:
|
|
// one malformed-scope diagnostic per package file, and one pointed syntax
|
|
// diagnostic per CommonJS source file.
|
|
const invalidPackageConfigs = new Set<string>();
|
|
for (const sf of userFiles) {
|
|
const invalidConfig = nearestInvalidPackageJsonPath(sf.fileName);
|
|
if (invalidConfig !== null) {
|
|
if (!invalidPackageConfigs.has(invalidConfig)) {
|
|
invalidPackageConfigs.add(invalidConfig);
|
|
diags.push(
|
|
invalidPackageConfigDiag(invalidConfig, { file: sf.fileName, start: 0, end: 0 }),
|
|
);
|
|
}
|
|
continue;
|
|
}
|
|
if (!isNodeEsmFile7(sf, program)) {
|
|
const marker = nodeEsmSyntaxMarker7(sf);
|
|
if (marker !== null) diags.push(commonJsModuleSyntaxDiag(locOf7(marker)));
|
|
}
|
|
}
|
|
|
|
const ambientModules = new Set<string>(SUPPORTED_NODE_MODULES);
|
|
|
|
// Ambient `declare module "name"` declarations anywhere in the program —
|
|
// exact names and `*` patterns — so the import fence can say "type
|
|
// surface only, no runtime module" instead of the generic package story.
|
|
// Computed lazily: the scan only runs when a bare-import fence is about
|
|
// to fire.
|
|
let ambientModuleDecls: { names: Set<string>; patterns: string[] } | null = null;
|
|
const ambientDeclared = (spec: string): boolean => {
|
|
if (ambientModuleDecls === null) {
|
|
ambientModuleDecls = { names: new Set(), patterns: [] };
|
|
for (const f of program.getSourceFiles()) {
|
|
for (const s of f.statements.nodes) {
|
|
if (!ts.isModuleDeclaration(s) || !ts.isStringLiteral(s.name)) continue;
|
|
const name = s.name.text;
|
|
if (name.includes("*")) ambientModuleDecls.patterns.push(name);
|
|
else ambientModuleDecls.names.add(name);
|
|
}
|
|
}
|
|
}
|
|
if (ambientModuleDecls.names.has(spec)) return true;
|
|
return ambientModuleDecls.patterns.some((p) => {
|
|
const star = p.indexOf("*");
|
|
return (
|
|
spec.length >= p.length - 1 &&
|
|
spec.startsWith(p.slice(0, star)) &&
|
|
spec.endsWith(p.slice(star + 1))
|
|
);
|
|
});
|
|
};
|
|
|
|
// Module edges. An edge that closes a cycle is judged by the shared
|
|
// admission engine (makeCycleAdmission): admissible when nothing can
|
|
// OBSERVE the partially-initialized module through it — the importing
|
|
// statement binds nothing (side-effect import) or binds only NAMESPACE
|
|
// objects (initialized at link, never TDZ) whose every use is a bare
|
|
// expression statement (the no-op lowering; no member is ever read),
|
|
// or the cycle passes the declaration-only init window analysis (the
|
|
// benign-cycle admission block above). Node evaluates such cycles
|
|
// benignly (the revisited module is a cache hit, and no binding read
|
|
// can hit TDZ or a stale slot), so the guarded %init calls reproduce
|
|
// its order exactly. Cycles that fail both are judged by the exact
|
|
// initialization rule: admitted when every early read is modeled by an
|
|
// initialization check, fenced with SC1016 otherwise.
|
|
const edges = new Map<ts.SourceFile, CycleEdge[]>();
|
|
// Import edges Node's RESOLUTION refuses before any module evaluates —
|
|
// recorded in source order per file (interleaved with the resolved deps
|
|
// via `pos`) so the Node-order walk below can answer which refusal Node
|
|
// reports first. Each carries a fallback fence: a refusal in a module
|
|
// the entry never reaches contributes no startup crash (Node never
|
|
// resolves it), so it keeps today's compile-time diagnostic instead.
|
|
interface RefusalCandidate {
|
|
crash: StartupCrash;
|
|
fallback: ScrDiagnostic;
|
|
pos: number;
|
|
}
|
|
const refusals = new Map<ts.SourceFile, RefusalCandidate[]>();
|
|
const depPositions = new Map<ts.SourceFile, { dep: ts.SourceFile; pos: number }[]>();
|
|
for (const sf of userFiles) {
|
|
const deps: CycleEdge[] = [];
|
|
edges.set(sf, deps);
|
|
refusals.set(sf, []);
|
|
depPositions.set(sf, []);
|
|
for (const stmt of sf.statements.nodes) {
|
|
if (ts.isImportEqualsDeclaration(stmt)) {
|
|
// The ENTITY form (`import a = A`, `import x = N.y`) is namespace
|
|
// alias plumbing — the lowerer resolves references through it and
|
|
// fences what it cannot honor (lower-namespaces.ts). Only the
|
|
// require form is a module edge, and its RUNTIME form stays fenced
|
|
// (under the forced ESNext module world tsc itself rejects it, so
|
|
// only nested/namespace-internal sites can reach the lowering's
|
|
// fence); `import type x = require(...)` is pure type surface and
|
|
// lowers to nothing.
|
|
if (ts.isExternalModuleReference(stmt.moduleReference) && !stmt.isTypeOnly) {
|
|
const spec = stmt.moduleReference.expression;
|
|
if (
|
|
spec !== undefined &&
|
|
ts.isStringLiteralLike(spec) &&
|
|
load.externalTypes.has(spec.text)
|
|
) {
|
|
diags.push(externalHostModuleDiag7(spec.text, stmt));
|
|
} else {
|
|
diags.push(
|
|
unsupportedDiag("SC1013", locOf7(stmt), "import = require(...) assignments"),
|
|
);
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
if (ts.isExportAssignment(stmt)) {
|
|
if (stmt.isExportEquals) {
|
|
diags.push(unsupportedDiag("SC1012", locOf7(stmt), "export = assignments"));
|
|
}
|
|
continue;
|
|
}
|
|
if (ts.isExportDeclaration(stmt)) {
|
|
if (stmt.isTypeOnly || isPrunedNpmReexport(program, stmt)) continue;
|
|
if (!stmt.moduleSpecifier) continue;
|
|
const fromSpec = ts.isStringLiteral(stmt.moduleSpecifier) ? stmt.moduleSpecifier.text : "";
|
|
if (load.externalTypes.has(fromSpec)) {
|
|
diags.push(externalHostModuleDiag7(fromSpec, stmt));
|
|
continue;
|
|
}
|
|
const projectReexport = resolveImport7(program, sf, fromSpec);
|
|
if (!isRelativeSpecifier(fromSpec) && projectReexport === null) {
|
|
// NAMED re-exports from a SUPPORTED builtin pass (`export { ok }
|
|
// from "node:assert"` — a universal re-export facade facade): the
|
|
// statement binds nothing locally and evaluates nothing (builtins
|
|
// have no %init), and importers' references chase the alias to
|
|
// the builtin's own declarations exactly as if they imported the
|
|
// builtin directly — per-member support stays the lowering's
|
|
// per-site business. Star and namespace re-exports and package
|
|
// specifiers keep the fence.
|
|
if (
|
|
canonicalBuiltinModule(fromSpec) !== null &&
|
|
stmt.exportClause !== undefined &&
|
|
ts.isNamedExports(stmt.exportClause)
|
|
) {
|
|
continue;
|
|
}
|
|
// NAMED re-exports from an INSTALLED npm package (`export
|
|
// { isUrl } from "url-or-path"` — the pattern's universal facade,
|
|
// `export { visitorKeys as default } from "@glimmer/syntax"`):
|
|
// import-plus-export plumbing. The lowering owns the package
|
|
// value diagnostic when no compiled source can supply the binding.
|
|
if (
|
|
stmt.exportClause !== undefined &&
|
|
ts.isNamedExports(stmt.exportClause) &&
|
|
!fromSpec.startsWith("#") &&
|
|
resolveNpmImport7(sf.fileName, fromSpec) !== null
|
|
) {
|
|
continue;
|
|
}
|
|
diags.push(
|
|
unsupportedDiag("SC1014", locOf7(stmt), "re-exports from packages or builtin modules"),
|
|
);
|
|
continue;
|
|
}
|
|
// A re-export from a Node island package that tsconfig `paths` maps
|
|
// to its installed directory: its declarations are no program
|
|
// module (the lowering reports the island boundary).
|
|
if (
|
|
projectReexport !== null &&
|
|
projectReexport.isDeclarationFile &&
|
|
!isRelativeSpecifier(fromSpec) &&
|
|
isFrontendIsland(packageNameOfSpecifier(fromSpec))
|
|
)
|
|
continue;
|
|
const reDep = projectReexport;
|
|
// `export * as ns from "./m"` re-exports the module NAMESPACE
|
|
// object under a name: importers' `x.ns.member` reads resolve
|
|
// statically through the same alias machinery as `import * as ns`
|
|
// (moduleNsSymbolOf follows the chain to the module symbol), so a
|
|
// RESOLVED user module lowers; everything else keeps the fence —
|
|
// there is no namespace object to materialize.
|
|
if (stmt.exportClause && ts.isNamespaceExport(stmt.exportClause)) {
|
|
if (reDep === null || reDep.fileName.endsWith(".json") || isCjsJsFile7(reDep, program)) {
|
|
diags.push(
|
|
unsupportedDiag(
|
|
"SC1013",
|
|
locOf7(stmt),
|
|
"namespace re-exports (export * as ns) of this module form",
|
|
),
|
|
);
|
|
continue;
|
|
}
|
|
}
|
|
if (reDep && !reDep.fileName.endsWith(".json")) {
|
|
deps.push({ dep: reDep, stmt });
|
|
depPositions.get(sf)!.push({ dep: reDep, pos: ts.tokenStartOfNode(stmt) });
|
|
}
|
|
continue;
|
|
}
|
|
if (!ts.isImportDeclaration(stmt)) continue;
|
|
if (erasedTypeOnlyImport(stmt)) continue;
|
|
|
|
// Import-like nodes whose specifier the parse could not shape into a
|
|
// string literal — the jsdoc `/** @import x = require(...) */`
|
|
// materialization and `import defer type * as ns` both arrive here
|
|
// with no specifier text. Neither is a runtime module edge (both are
|
|
// type-only forms), and where the form is malformed TS the SC0001
|
|
// passthrough above already names it — never a crash here.
|
|
const specNode: ts.Expression | undefined = stmt.moduleSpecifier;
|
|
if (specNode === undefined || !ts.isStringLiteral(specNode)) continue;
|
|
const spec = specNode.text;
|
|
if (load.externalTypes.has(spec)) {
|
|
diags.push(externalHostModuleDiag7(spec, stmt));
|
|
continue;
|
|
}
|
|
const isRelative = isRelativeSpecifier(spec);
|
|
const isBare = !isRelative && !ambientModules.has(spec);
|
|
// Npm packages can use the same static createRequire forms as program
|
|
// code. Preserve fallback for bundler banners that pass the require
|
|
// value around or feed it computed specifiers during initialization.
|
|
if (canonicalBuiltinModule(spec) === "module") {
|
|
const pkg = npmStaticPackageOfPath(sf.fileName);
|
|
if (pkg !== null) {
|
|
const reason = npmStaticModuleImportReason7(program, stmt);
|
|
if (reason !== null) reportNpmStaticOffender(pkg, reason);
|
|
}
|
|
}
|
|
// An import edge Node's own resolution refuses BEFORE any module
|
|
// evaluates: recorded as a startup-crash candidate (the Node-order
|
|
// walk below picks the first one Node would report; the program
|
|
// compiles to exactly that crash — buildMain) with a compile-fence
|
|
// fallback for candidates the entry never reaches. The bindings of a
|
|
// refused edge never link, so every clause fence below is skipped —
|
|
// their uses meet the lowering's own fences if the code demands
|
|
// values from them.
|
|
const refuse = (
|
|
message: string,
|
|
className: StartupCrash["className"],
|
|
fallbackFeature: string,
|
|
code?: string,
|
|
): void => {
|
|
refusals.get(sf)!.push({
|
|
crash: { message, className, loc: locOf7(stmt), ...(code !== undefined ? { code } : {}) },
|
|
fallback: unsupportedDiag("SC1010", locOf7(stmt), fallbackFeature),
|
|
pos: ts.tokenStartOfNode(stmt),
|
|
});
|
|
};
|
|
// Project modules win before npm fallback: this is the source graph
|
|
// tsgo type-checked for tsconfig paths, package imports/self-references,
|
|
// and provenance entries. Ordinary packages still answer null here.
|
|
const projDep = isBare ? resolveImport7(program, sf, spec) : null;
|
|
// "#" specifiers can never name an npm package — they are the
|
|
// imports-field family, resolved by the project arm above.
|
|
const npm =
|
|
isBare && projDep === null && !spec.startsWith("#")
|
|
? resolveNpmImport7(sf.fileName, spec)
|
|
: null;
|
|
if (npm && isNodeTypesPath(npm.typesFile)) {
|
|
diags.push(unsupportedDiag("SC1010", locOf7(stmt), unsupportedModuleFeatureOf(spec)));
|
|
continue;
|
|
}
|
|
// --npm-static: an opted-in package's import is a PROGRAM-MODULE
|
|
// edge — its resolution (types stripped) answered the shipped JS,
|
|
// which allowJs + maxNodeModuleJsDepth pulled into the program, so
|
|
// the entry joins the module order like any user module and the
|
|
// clause fences below apply to it as to program JS. A miss (the
|
|
// resolution still answers declarations, or the file never joined
|
|
// the program) marks the package an OFFENDER: the frontend's
|
|
// selection loop rebuilds without it and reports the package refusal.
|
|
let npmStaticDep: ts.SourceFile | null = null;
|
|
if (npm !== null) {
|
|
if (isNpmStaticPackage(npm.packageName)) {
|
|
npmStaticDep = npmStaticProgramDep(program, npm.packageName, npm.typesFile);
|
|
if (npmStaticDep === null) continue;
|
|
} else {
|
|
// Types resolved, but Node's RUNTIME resolution can still refuse
|
|
// the edge (a types-only package whose exports target ships no JS,
|
|
// most commonly): that program is Node's startup crash, not an
|
|
|
|
const refusal = probeNodeImportRefusal(sf.fileName, spec);
|
|
if (refusal !== null) {
|
|
refuse(
|
|
refusal.message,
|
|
"%Error",
|
|
`the '${spec}' package (its runtime resolution fails: ${refusal.message})`,
|
|
refusal.code,
|
|
);
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
// PROJECT imports: tsconfig paths and package.json-mediated specifiers
|
|
// that resolve to the program's own sources. A source answer is an
|
|
// ordinary user-module edge, exactly like a relative import (the
|
|
// checker resolved the bindings the same way); a refused resolution is
|
|
// Node's startup crash with Node's exact message; what keeps a compile
|
|
// fence is unsupported builtin or types-only surface.
|
|
if (isBare && npmStaticDep === null) {
|
|
// A Node island package that tsconfig `paths` maps to its
|
|
// installed directory resolves to its declarations here: it loads
|
|
// in Node, typed by them, like an island package resolved through
|
|
// node_modules (scope-B NI-4).
|
|
if (
|
|
projDep !== null &&
|
|
projDep.isDeclarationFile &&
|
|
isFrontendIsland(packageNameOfSpecifier(spec))
|
|
)
|
|
continue;
|
|
if (projDep !== null && projDep.isDeclarationFile) {
|
|
diags.push(
|
|
unsupportedDiag(
|
|
"SC1010",
|
|
locOf7(stmt),
|
|
`the '${spec}' import (it resolves only to type declarations — there is no runtime implementation to compile)`,
|
|
),
|
|
);
|
|
continue;
|
|
}
|
|
if (projDep === null) {
|
|
// node:process's default export is the global process object. It
|
|
// contributes no module-order edge; the imported binding lowers
|
|
// through stdlibGlobalNameOf exactly like the bare global.
|
|
if (processModuleAliasImport7(spec, stmt)) continue;
|
|
const nodeBuiltin = spec.startsWith("node:") || nodeBuiltinNames.has(spec);
|
|
if (nodeBuiltin) {
|
|
diags.push(unsupportedDiag("SC1010", locOf7(stmt), unsupportedModuleFeatureOf(spec)));
|
|
continue;
|
|
}
|
|
if (spec === "#") {
|
|
refuse(
|
|
`Invalid module "#" is not a valid internal imports specifier name imported from ${sf.fileName}`,
|
|
"%TypeError",
|
|
`the '#' import (Node rejects the bare '#' specifier as invalid)`,
|
|
"ERR_INVALID_MODULE_SPECIFIER",
|
|
);
|
|
continue;
|
|
}
|
|
if (spec.startsWith("#")) {
|
|
const target = packageImportTarget(sf.fileName, spec);
|
|
if (target !== null && !target.startsWith("./") && !target.startsWith("../")) {
|
|
diags.push(
|
|
unsupportedDiag(
|
|
"SC1010",
|
|
locOf7(stmt),
|
|
`package.json imports entries that map to other packages (the '${spec}' import maps to '${target}')`,
|
|
),
|
|
);
|
|
continue;
|
|
}
|
|
const pkgJson = nearestPkgJsonPath(sf.fileName);
|
|
refuse(
|
|
`Package import specifier "${spec}" is not defined${pkgJson !== null ? ` in package ${pkgJson}` : ""} imported from ${sf.fileName}`,
|
|
"%TypeError",
|
|
`the '${spec}' import (no package.json imports-field entry maps it)`,
|
|
"ERR_PACKAGE_IMPORT_NOT_DEFINED",
|
|
);
|
|
continue;
|
|
}
|
|
const refusal = probeNodeImportRefusal(sf.fileName, spec);
|
|
if (refusal !== null) {
|
|
const ambientNote = ambientDeclared(spec)
|
|
? `the '${spec}' module (it exists only as an ambient 'declare module' type surface — there is no runtime module to import)`
|
|
: `the '${spec}' package (nothing installed resolves it)`;
|
|
refuse(refusal.message, "%Error", ambientNote, refusal.code);
|
|
continue;
|
|
}
|
|
// Runtime-resolvable (or a shape the probe stays conservative
|
|
// about) with no compilable types answer: scriptc's fence.
|
|
diags.push(
|
|
unsupportedDiag(
|
|
"SC1010",
|
|
locOf7(stmt),
|
|
ambientDeclared(spec)
|
|
? `the '${spec}' module (it exists only as an ambient 'declare module' type surface — there is no runtime module to import)`
|
|
: `the '${spec}' package (nothing installed resolves it)`,
|
|
),
|
|
);
|
|
continue;
|
|
}
|
|
}
|
|
const clause = stmt.importClause;
|
|
const dep = isRelative ? resolveImport7(program, sf, spec) : (npmStaticDep ?? projDep);
|
|
const isJson = dep !== null && dep.fileName.endsWith(".json");
|
|
// SELF-imports: the bindings alias this module's OWN declarations,
|
|
// so a TOP-LEVEL read lexically above the aliased declaration is
|
|
// Node's TDZ ReferenceError at runtime (exactly what tsc's TS2448
|
|
// refuses for the DIRECT spelling — imports are exempt from that
|
|
// check, so the fence restores the same bar). Hoisted function
|
|
// aliases initialize at link and read fine anywhere; reads inside
|
|
// function bodies keep the direct spelling's existing policy.
|
|
if (dep === sf && clause !== undefined) {
|
|
selfImportTdzFences7(program, sf, clause, diags);
|
|
}
|
|
if (clause) {
|
|
// Default imports of SUPPORTED builtins pass where the spelling is
|
|
// legal: Node's default export of a CJS builtin IS the module
|
|
// object, so the binding exposes exactly the namespace-import
|
|
// surface and the lowering keys the same tables
|
|
// (builtinNamespaceModuleOf's default-import twin). JS sources
|
|
// always (Node never asks for interop flags); TS sources when the
|
|
// project's explicit or implied synthetic-default permission accepts
|
|
// the spelling. A TS project without that permission keeps the
|
|
// SC1012 fence. The callable module objects (assert, events, test)
|
|
// stay allowed everywhere. The process default is the global process
|
|
// alias, including when combined with named builtin imports.
|
|
const syntheticDefaultsOn =
|
|
(program.getCompilerOptions() as { allowSyntheticDefaultImports?: boolean })
|
|
.allowSyntheticDefaultImports === true;
|
|
const defaultOk =
|
|
spec === "process" ||
|
|
spec === "node:process" ||
|
|
builtinDefaultImportModule(spec) !== null ||
|
|
((isJsSourceFileName(sf.fileName) || syntheticDefaultsOn) &&
|
|
canonicalBuiltinModule(spec) !== null);
|
|
if (clause.name && !isJson && dep === null && !defaultOk) {
|
|
diags.push(unsupportedDiag("SC1012", locOf7(clause.name)));
|
|
}
|
|
if (clause.namedBindings && ts.isNamedImports(clause.namedBindings) && isJson) {
|
|
diags.push(
|
|
unsupportedDiag(
|
|
"SC1012",
|
|
locOf7(clause.namedBindings),
|
|
"named imports from JSON modules",
|
|
),
|
|
);
|
|
}
|
|
if (clause.namedBindings && ts.isNamespaceImport(clause.namedBindings)) {
|
|
// `import * as ns from "./m"` of a RESOLVED user module lowers:
|
|
// ns.member accesses resolve statically through the checker to
|
|
// the exporter's own registrations (lower-namespaces.ts's
|
|
// module-namespace paths); the ns OBJECT as a first-class value
|
|
// fences per site. What keeps a fence here: JSON modules (their
|
|
// namespace object has no static story — default imports bake
|
|
// the data), CommonJS JS modules (Node builds their namespace
|
|
// from its lexer's static analysis, a different surface),
|
|
// and unresolved specifiers. Supported builtins pass through as
|
|
// before (their namespace members are the declared surface).
|
|
if (isJson) {
|
|
diags.push(
|
|
unsupportedDiag(
|
|
"SC1013",
|
|
locOf7(clause.namedBindings),
|
|
"namespace imports of JSON modules",
|
|
),
|
|
);
|
|
} else if (dep !== null && isCjsJsFile7(dep, program)) {
|
|
// A CJS namespace binding whose every use is a bare expression
|
|
// statement (`cjs;` — the corpus's "the import linked"
|
|
// assertion) carries no surface question: the statement lowers
|
|
// to Node's no-op, the module still evaluates through its
|
|
// ordinary init edge, and no member is ever read through the
|
|
// lexer facade (self-imports of a CJS module included — the
|
|
// partially-built exports are never observed). In JavaScript
|
|
// sources every use passes here: member reads lower through the
|
|
// names Node's lexer detects, and the other reads refuse at
|
|
// their site (lower-namespaces.ts, cjsNamespaceImportDep). Any
|
|
// OTHER use in TypeScript keeps the fence.
|
|
if (
|
|
!isJsSourceFileName(sf.fileName) &&
|
|
!nsBindingUsesAreBareStatements7(program, sf, clause.namedBindings.name)
|
|
) {
|
|
diags.push(
|
|
unsupportedDiag(
|
|
"SC1013",
|
|
locOf7(clause.namedBindings),
|
|
"namespace imports of CommonJS modules",
|
|
),
|
|
);
|
|
}
|
|
} else if (
|
|
dep === null &&
|
|
!(!isRelative && ambientModules.has(spec)) &&
|
|
// A Node island module's namespace (scope-B NI-4).
|
|
!(!isRelative && isFrontendIsland(packageNameOfSpecifier(spec)))
|
|
) {
|
|
diags.push(unsupportedDiag("SC1013", locOf7(clause.namedBindings)));
|
|
}
|
|
}
|
|
}
|
|
if (dep && !isJson) {
|
|
deps.push({ dep, stmt });
|
|
depPositions.get(sf)!.push({ dep, pos: ts.tokenStartOfNode(stmt) });
|
|
}
|
|
}
|
|
{
|
|
const jsRequireFile = isJsSourceFileName(sf.fileName);
|
|
const stmts = sf.statements.nodes;
|
|
// A binding that code running before the require can reach. Node
|
|
// initializes it AT the require: a const/let read earlier throws
|
|
// ReferenceError, which the lowering reproduces with an
|
|
// initialization check at every reference that can run early
|
|
// (require-initialization.ts). A var reads undefined there, which
|
|
// module aliases cannot represent; createRequire bindings have no
|
|
// checked form either.
|
|
const earlyRequireFence = (
|
|
req: RequireOccurrence7,
|
|
bindingKind: RequireCallBindingKind7,
|
|
loc: { file: string; start: number; end: number },
|
|
): boolean => {
|
|
const early = req.decl !== null ? requireEarlyReference7(program, req.decl) : null;
|
|
if (
|
|
early === null ||
|
|
(bindingKind !== "createRequire" &&
|
|
req.split === undefined &&
|
|
((req.decl!.parent?.flags ?? 0) & (ts.NodeFlags.Const | ts.NodeFlags.Let)) !== 0)
|
|
) {
|
|
return false;
|
|
}
|
|
diags.push(
|
|
unsupportedDiag(
|
|
"SC1013",
|
|
loc,
|
|
bindingKind === "createRequire"
|
|
? `createRequire() binding '${early.text}' below code that can reach it (Node throws ReferenceError on any earlier access, which is not modeled — move the require above the code that can run first)`
|
|
: req.split !== undefined
|
|
? `require() binding '${early.text}' assigned below code that can reach it (Node reads undefined before the assignment runs, which is not modeled — move the assignment above the code that can run first)`
|
|
: `var require() binding '${early.text}' below code that can reach it (Node reads undefined before the require runs, which is not modeled — declare it with const or let, or move the require above the code that can run first)`,
|
|
),
|
|
);
|
|
return true;
|
|
};
|
|
for (let k = 0; k < stmts.length; k++) {
|
|
const stmt = stmts[k]!;
|
|
for (const req of requiresOf7(stmt, program)) {
|
|
const loc = {
|
|
file: sf.fileName,
|
|
start: ts.tokenStartOfNode(req.node),
|
|
end: req.node.end,
|
|
};
|
|
const bindingKind = requireCallBindingKind7(program, req.node);
|
|
// TypeScript source files join this scan only through the
|
|
// canonical createRequire bridge. Their ordinary require-shaped
|
|
// calls retain the existing checker/lowering diagnostics; JS
|
|
// files keep the full CommonJS scan.
|
|
if (!jsRequireFile && bindingKind !== "createRequire") continue;
|
|
if (isNodeEsmFile7(sf, program)) {
|
|
if (bindingKind !== "createRequire") {
|
|
diags.push(unsupportedDiag("SC1013", loc, esmRequireFeature7(bindingKind)));
|
|
continue;
|
|
}
|
|
}
|
|
if (load.externalTypes.has(req.spec)) {
|
|
continue;
|
|
}
|
|
// A JSON document: the require answers its cached parsed object
|
|
// (json-require.ts), an ordinary value the binding stores, so
|
|
// every destructuring form lowers as usual. No module edge.
|
|
const jsonDep = resolveImport7(program, sf, req.spec, "require");
|
|
if (jsonDep !== null && jsonDep.fileName.endsWith(".json")) {
|
|
if (bindingKind === "createRequire") continue;
|
|
if (ts.isVariableStatement(stmt) && requiresOf7(stmt, program).length > 1) {
|
|
const others = requiresOf7(stmt, program).some((r) => {
|
|
const d = resolveImport7(program, sf, r.spec, "require");
|
|
return d === null || !d.fileName.endsWith(".json");
|
|
});
|
|
if (others) {
|
|
diags.push(
|
|
unsupportedDiag(
|
|
"SC1012",
|
|
loc,
|
|
"require() of a JSON module declared together with other require() bindings",
|
|
),
|
|
);
|
|
continue;
|
|
}
|
|
}
|
|
earlyRequireFence(req, bindingKind, loc);
|
|
continue;
|
|
}
|
|
if (req.decl && ts.isArrayBindingPattern(req.decl.name)) {
|
|
diags.push(unsupportedDiag("SC1012", loc, "array-destructuring require() bindings"));
|
|
continue;
|
|
}
|
|
if (req.decl && ts.isObjectBindingPattern(req.decl.name)) {
|
|
// A nested pattern of a builtin binds members of the sub-module
|
|
// its key names (`const { types: { isPromise } } =
|
|
// require("util")` — util.types IS util/types).
|
|
const builtin = canonicalBuiltinModule(req.spec);
|
|
const plainElements = (
|
|
pattern: ts.ObjectBindingPattern,
|
|
module: string | null,
|
|
): boolean =>
|
|
pattern.elements.nodes.every((e) => {
|
|
if (e.initializer !== undefined || e.dotDotDotToken !== undefined) return false;
|
|
if (e.name !== undefined && ts.isIdentifier(e.name)) return true;
|
|
// A nested pattern under a named export of a program module
|
|
// destructures that export's VALUE at the require
|
|
// (`const { codes: { ERR_X } } = require("./errors")`): the
|
|
// inner bindings are ordinary destructured storage.
|
|
if (
|
|
module === null &&
|
|
builtin === null &&
|
|
bindingKind !== "createRequire" &&
|
|
pattern === req.decl!.name &&
|
|
e.name !== undefined &&
|
|
ts.isObjectBindingPattern(e.name) &&
|
|
e.propertyName !== undefined &&
|
|
ts.isIdentifier(e.propertyName)
|
|
)
|
|
return true;
|
|
if (module === null || e.name === undefined || !ts.isObjectBindingPattern(e.name))
|
|
return false;
|
|
const key = e.propertyName;
|
|
if (key === undefined || !ts.isIdentifier(key)) return false;
|
|
const sub = canonicalBuiltinModule(`${module}/${key.text}`);
|
|
return sub !== null && plainElements(e.name, sub);
|
|
});
|
|
if (!plainElements(req.decl.name, builtin)) {
|
|
diags.push(
|
|
unsupportedDiag(
|
|
"SC1012",
|
|
loc,
|
|
builtin !== null
|
|
? "require() destructuring with defaults, rest, or nested patterns other than builtin sub-modules"
|
|
: "require() destructuring with defaults, rest, or nested patterns of program modules (JSON documents and builtin sub-modules destructure fully)",
|
|
),
|
|
);
|
|
continue;
|
|
}
|
|
}
|
|
// An npm package served as a builtin (graceful-fs as fs) is never
|
|
// a program-module edge, even when --npm-static names it.
|
|
if (npmBuiltinAliasOf(req.spec) !== null) continue;
|
|
const isRelative = isRelativeSpecifier(req.spec);
|
|
let dep = resolveImport7(program, sf, req.spec, "require");
|
|
if (dep === null && !isRelative) {
|
|
// --npm-static: a require() of an OPTED-IN package is a
|
|
// program-module edge exactly like the import-declaration
|
|
// form above (bundle dists require their workspace siblings —
|
|
// the same resolution, the same offender discipline on a
|
|
// miss).
|
|
const npmReq = !req.spec.startsWith("#")
|
|
? resolveNpmImport7(sf.fileName, req.spec, "require")
|
|
: null;
|
|
if (npmReq !== null && isNpmStaticPackage(npmReq.packageName)) {
|
|
dep = npmStaticProgramDep(program, npmReq.packageName, npmReq.typesFile);
|
|
if (dep === null) continue; // offender recorded — the fallback loop reloads
|
|
} else {
|
|
// createRequire's non-program targets belong to its own
|
|
// lowering, where missing packages become catchable
|
|
// MODULE_NOT_FOUND throws. Only resolved program targets are
|
|
// module-order edges in this preflight walk.
|
|
if (bindingKind === "createRequire") continue;
|
|
if (
|
|
canonicalBuiltinModule(req.spec) === null &&
|
|
!processModuleAliasRequire7(req.spec, req.decl)
|
|
) {
|
|
// A binding-less require of a package NOTHING resolves:
|
|
// Node throws MODULE_NOT_FOUND at the require site — the
|
|
// lowering compiles exactly that catchable throw (the
|
|
// optional-dependency try/require pattern), so no fence.
|
|
// Binding forms keep the fence: their downstream reads
|
|
// would need the module that never loads.
|
|
if (
|
|
(req.decl !== null || probeNodeRequireRefusal(sf.fileName, req.spec) === null) &&
|
|
// A Node island package: loaded in Node at the require (NI-4).
|
|
!isFrontendIsland(packageNameOfSpecifier(req.spec))
|
|
) {
|
|
diags.push(unsupportedDiag("SC1010", loc, unsupportedModuleFeatureOf(req.spec)));
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
if (earlyRequireFence(req, bindingKind, loc)) continue;
|
|
if (dep) deps.push({ dep });
|
|
}
|
|
}
|
|
const nestedBareRequires = nestedBareRequiresOf7(sf);
|
|
program
|
|
.getTypeChecker()
|
|
.prefetchSymbolNodesExact(
|
|
nestedBareRequires.flatMap((call) =>
|
|
ts.isIdentifier(call.expression) ? [call.expression] : [],
|
|
),
|
|
);
|
|
for (const call of nestedBareRequires) {
|
|
const spec = requireSpecOf7(call)!;
|
|
const loc = { file: sf.fileName, start: ts.tokenStartOfNode(call), end: call.end };
|
|
const bindingKind = requireCallBindingKind7(program, call);
|
|
if (!jsRequireFile && bindingKind !== "createRequire") continue;
|
|
if (isNodeEsmFile7(sf, program)) {
|
|
if (bindingKind !== "createRequire") {
|
|
diags.push(unsupportedDiag("SC1013", loc, esmRequireFeature7(bindingKind)));
|
|
continue;
|
|
}
|
|
}
|
|
if (load.externalTypes.has(spec)) {
|
|
continue;
|
|
}
|
|
if (npmBuiltinAliasOf(spec) !== null) continue;
|
|
const projectDep = resolveImport7(program, sf, spec, "require");
|
|
if (projectDep !== null) {
|
|
// A bare require of a JSON document only parses it: nothing to
|
|
// evaluate (json-require.ts creates the cached object).
|
|
if (projectDep.fileName.endsWith(".json")) continue;
|
|
deps.push({ dep: projectDep });
|
|
continue;
|
|
}
|
|
if (!isRelativeSpecifier(spec)) {
|
|
// --npm-static: opted-in packages ride the program-module edge
|
|
// (the statement-level require branch above).
|
|
const npmReq = !spec.startsWith("#")
|
|
? resolveNpmImport7(sf.fileName, spec, "require")
|
|
: null;
|
|
if (npmReq !== null && isNpmStaticPackage(npmReq.packageName)) {
|
|
const nDep = npmStaticProgramDep(program, npmReq.packageName, npmReq.typesFile);
|
|
if (nDep !== null) deps.push({ dep: nDep });
|
|
continue;
|
|
}
|
|
if (bindingKind === "createRequire") continue;
|
|
if (canonicalBuiltinModule(spec) === null && !processModuleAliasRequire7(spec, null)) {
|
|
// Binding-less by construction — same require-site throw
|
|
// channel as the statement-level form above.
|
|
if (
|
|
probeNodeRequireRefusal(sf.fileName, spec) === null &&
|
|
!isFrontendIsland(packageNameOfSpecifier(spec))
|
|
) {
|
|
diags.push(unsupportedDiag("SC1010", loc, unsupportedModuleFeatureOf(spec)));
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
// Inline requires of program modules are module edges like the bare
|
|
// statements above (cjs-inline-require.ts lowers each to the module's
|
|
// %init at its position). Only the CommonJS global in a CommonJS file
|
|
// counts, and targets outside the compiled graph add nothing here:
|
|
// their use sites keep their existing lowering fences.
|
|
if (jsRequireFile && !isNodeEsmFile7(sf, program)) {
|
|
// A split require binding's assignment is a statement-level require
|
|
// edge (requiresOf7 above), parenthesized or not.
|
|
const splitCalls = new Set(splitRequireBindings7(program, sf).values());
|
|
for (const call of inlineRequiresOf7(sf)) {
|
|
if (requireCallBindingKind7(program, call) !== "ambient") continue;
|
|
if (splitCalls.has(call)) continue;
|
|
const spec = requireSpecOf7(call)!;
|
|
if (load.externalTypes.has(spec) || canonicalBuiltinModule(spec) !== null) continue;
|
|
const lazy = inDeferredPosition7(call);
|
|
const edgeTo = (dep: ts.SourceFile): CycleEdge =>
|
|
lazy ? { dep, lazy: true, call } : { dep, call };
|
|
const projectDep = resolveImport7(program, sf, spec, "require");
|
|
if (projectDep !== null) {
|
|
if (!projectDep.fileName.endsWith(".json")) deps.push(edgeTo(projectDep));
|
|
continue;
|
|
}
|
|
if (isRelativeSpecifier(spec) || spec.startsWith("#")) continue;
|
|
const npmReq = resolveNpmImport7(sf.fileName, spec, "require");
|
|
if (npmReq !== null && isNpmStaticPackage(npmReq.packageName)) {
|
|
const nDep = npmStaticProgramDep(program, npmReq.packageName, npmReq.typesFile);
|
|
if (nDep !== null) deps.push(edgeTo(nDep));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Node's evaluation order: depth-first postorder from the entry, each
|
|
// module once, back-edges answered from the cache (a module already
|
|
// evaluating is skipped) — which the guarded %init calls reproduce
|
|
// exactly. Cycles are admitted where Node's partially-initialized
|
|
// semantics are UNOBSERVABLE or exactly modeled (JS's partial
|
|
// initialization otherwise misbehaves without a diagnostic, so SC1016
|
|
// stays for the rest):
|
|
// - a DIRECT SELF-IMPORT (the package self-name reference resolving to
|
|
// the importing module itself — Node's self-reference rule): the
|
|
// imported bindings alias the module's OWN top-level bindings with
|
|
// identical timing, evaluation happens once — always benign;
|
|
// - a back edge the shared admission engine (makeCycleAdmission)
|
|
// clears: the cheap per-edge rule (nothing binds, or only namespace
|
|
// objects bound to bare-statement uses — no read can hit TDZ or a
|
|
// stale slot) or the DECLARATION-ONLY INIT WINDOW analysis (the
|
|
// benign-cycle admission block: every module of the cycle's
|
|
// strongly-connected component has an inert top level and the
|
|
// closing edge's bindings are used only in deferred positions, so no
|
|
// read can execute before every member initialized);
|
|
// - the EXACT INITIALIZATION rule: an ES-module cluster whose early
|
|
// reads are all let/const bindings with module storage — the
|
|
// lowering checks each one (cycleEarlyBindings) and throws Node's
|
|
// ReferenceError on a read before the declaration ran.
|
|
const cycleAdmissionReason = makeCycleAdmission(program, (sf) => {
|
|
const all = edges.get(sf) ?? [];
|
|
return all.some((e) => e.call !== undefined) ? all.filter((e) => e.call === undefined) : all;
|
|
});
|
|
const order: ts.SourceFile[] = [];
|
|
const inlineRequireCycleCalls = new Set<ts.Node>();
|
|
const state = new Map<ts.SourceFile, "visiting" | "done">();
|
|
const stack: string[] = [];
|
|
const visit = (sf: ts.SourceFile): void => {
|
|
state.set(sf, "visiting");
|
|
stack.push(sf.fileName);
|
|
for (const e of edges.get(sf) ?? []) {
|
|
if (e.dep === sf) continue; // self-import: a Node cache hit, not a cycle
|
|
if (e.lazy === true) continue; // visited after this module completes
|
|
const s = state.get(e.dep);
|
|
if (s === "done") continue;
|
|
if (s === "visiting") {
|
|
if (e.call !== undefined) {
|
|
inlineRequireCycleCalls.add(e.call);
|
|
continue;
|
|
}
|
|
// A back-edge: Node answers it from the cache (the module is
|
|
// already evaluating) — admitted by the cheap per-edge rule, the
|
|
// declaration-only init window rule, or the exact initialization
|
|
// rule.
|
|
const reason = cycleAdmissionReason(sf, e);
|
|
if (reason !== null) {
|
|
const cycleStart = stack.indexOf(e.dep.fileName);
|
|
const cycle = [...stack.slice(cycleStart), e.dep.fileName].join(" → ");
|
|
diags.push(
|
|
unsupportedDiag(
|
|
"SC1016",
|
|
{ file: e.dep.fileName, start: 0, end: 0 },
|
|
`circular imports (${cycle}; ${reason})`,
|
|
),
|
|
);
|
|
}
|
|
continue;
|
|
}
|
|
visit(e.dep);
|
|
}
|
|
stack.pop();
|
|
state.set(sf, "done");
|
|
order.push(sf);
|
|
// Lazy inline requires run after this module's top level: their
|
|
// modules join the order afterwards (cache hits when already loaded).
|
|
for (const e of edges.get(sf) ?? []) {
|
|
if (e.lazy === true && state.get(e.dep) === undefined) visit(e.dep);
|
|
}
|
|
};
|
|
visit(entry);
|
|
|
|
// ECMAScript's sloppy-mode WRITE to an undeclared name, which the JS
|
|
// relaxation above admits for READS (plan D-2): refused by name, with the
|
|
// island route in the text (SC1091).
|
|
for (const d of jsUndeclaredWriteDiags(program, order)) diags.push(d);
|
|
|
|
// The startup crash Node's RESOLUTION phase reports: modules resolve
|
|
// their request specifiers in source order, children linking depth-first
|
|
// as each resolves (ModuleJob.syncLink) — a preorder walk over the
|
|
// resolved edges, statement-interleaved, so the first refusal found here
|
|
// is the one Node reports. Refusal candidates in modules the entry never
|
|
// reaches keep their compile-fence fallback instead (Node never resolves
|
|
// them). A found resolution crash wins over the CJS link check below:
|
|
// Node's fetch/resolve phase runs before instantiate.
|
|
let resolveCrash: StartupCrash | null = null;
|
|
const dynamicImportRejections = new Map<ts.SourceFile, StartupCrash>();
|
|
{
|
|
const seen = new Set<ts.SourceFile>();
|
|
const walk = (sf: ts.SourceFile): StartupCrash | null => {
|
|
if (seen.has(sf)) return null;
|
|
seen.add(sf);
|
|
const steps: { pos: number; dep?: ts.SourceFile; crash?: StartupCrash }[] = [
|
|
...(depPositions.get(sf) ?? []),
|
|
...(refusals.get(sf) ?? []).map((r) => ({ pos: r.pos, crash: r.crash })),
|
|
].sort((a, b) => a.pos - b.pos);
|
|
for (const step of steps) {
|
|
if (step.crash !== undefined) return step.crash;
|
|
if (step.dep !== undefined && step.dep !== sf) {
|
|
const hit = walk(step.dep);
|
|
if (hit !== null) return hit;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
resolveCrash = walk(entry);
|
|
// A module only a literal import() loads resolves its static graph when
|
|
// that import runs: Node rejects the import's promise with the first
|
|
// refusal it meets, before any module of that graph evaluates. Such a
|
|
// module never joins the compiled graph, and the refusals inside its
|
|
// graph are that rejection, not build failures.
|
|
const reach = (from: ts.SourceFile, into: Set<ts.SourceFile>): void => {
|
|
const stack = [from];
|
|
while (stack.length > 0) {
|
|
const sf = stack.pop()!;
|
|
if (into.has(sf)) continue;
|
|
into.add(sf);
|
|
for (const { dep } of depPositions.get(sf) ?? []) stack.push(dep);
|
|
}
|
|
};
|
|
const startup = new Set<ts.SourceFile>();
|
|
reach(entry, startup);
|
|
const covered = new Set<ts.SourceFile>();
|
|
for (const sf of userFiles) {
|
|
if (!sf.text.includes("import")) continue;
|
|
for (const call of moduleSourceCandidates(program, sf).calls) {
|
|
const arg = call.arguments.nodes[0];
|
|
if (call.expression.kind !== ts.SyntaxKind.ImportKeyword || arg === undefined) continue;
|
|
if (!ts.isStringLiteralLike(arg) || isUnloadedPackage(arg.text)) continue;
|
|
const root =
|
|
resolveImport7(program, sf, arg.text) ?? npmStaticDepSf7(program, sf, arg.text);
|
|
if (root === null || root.isDeclarationFile || startup.has(root)) continue;
|
|
// A module inside an already-rejected graph is `covered`, but a
|
|
// literal import() of it needs its OWN rejection entry: its graph
|
|
// rejects when that import runs, so it must not join the compiled
|
|
// graph (where lowering would fail) merely because another rejected
|
|
// import also reaches it. Walk each direct root independently.
|
|
if (dynamicImportRejections.has(root)) continue;
|
|
seen.clear();
|
|
const crash = walk(root);
|
|
if (crash === null) continue;
|
|
dynamicImportRejections.set(root, crash);
|
|
reach(root, covered);
|
|
}
|
|
}
|
|
// The crash channel engages only for programs that OTHERWISE pass
|
|
// preflight: when any other diagnostic already fails the build, every
|
|
// refusal keeps its compile-fence fallback instead — the report names
|
|
// each import's story, so edges whose crash will never compile still
|
|
// get a diagnostic. (No reachable refusal → same: fallbacks only.)
|
|
if (resolveCrash === null || diags.length > 0) {
|
|
resolveCrash = null;
|
|
for (const [sf, cands] of refusals) {
|
|
if (covered.has(sf) && !startup.has(sf)) continue;
|
|
for (const c of cands) diags.push(c.fallback);
|
|
}
|
|
}
|
|
}
|
|
|
|
const linkCrash =
|
|
resolveCrash !== null
|
|
? null
|
|
: earlierLinkCrash(
|
|
order,
|
|
cjsNamedImportLinkCheck(program, entry, order, diags),
|
|
esmNamedImportLinkCheck(program, entry, order, diags),
|
|
);
|
|
|
|
// ECMAScript early errors the TypeScript checker does not model (plan
|
|
// §7.7 D-4): a JavaScript source the spec makes a SyntaxError fails here,
|
|
// before any lowering, exactly where a tsc parse error fails.
|
|
diags.push(...earlyErrorDiagnostics7(order, (sf) => isNodeEsmFile7(sf, program)));
|
|
|
|
return {
|
|
diags,
|
|
moduleOrder: order,
|
|
startupCrash: resolveCrash ?? linkCrash,
|
|
inlineRequireCycleCalls,
|
|
moduleEdges: edges,
|
|
dynamicImportRejections,
|
|
};
|
|
}
|
|
|
|
/* ── the CJS named-import link check ─────────────────────────────────────
|
|
* Node builds a CommonJS module's ESM facade from its LEXER (cjs-lexer.ts
|
|
* mirrors it), never from execution — so a named import the checker binds
|
|
* happily can still fail Node's instantiate phase with a SyntaxError,
|
|
* thrown before ANY module in the graph evaluates. The checker's word is
|
|
* NOT Node's word exactly here, and shipping the checker's answer would
|
|
* diverge from Node without a diagnostic (`module.exports = { a: 7 }` served `a` where
|
|
* Node refuses the program). This walk asks Node's question at compile
|
|
* time, in Node's exact ORDER, and answers with Node's exact message. */
|
|
|
|
/** The CommonJS program file a re-export (`module.exports = require(spec)`)
|
|
* of `from` resolves to, or null: reexport targets union in only when they
|
|
* resolve to CommonJS program files (Node's cjsPreparseModuleExports rule). */
|
|
function cjsReexportDep7(program: ts.Program) {
|
|
return (from: ts.SourceFile, spec: string): ts.SourceFile | null => {
|
|
const dep =
|
|
resolveImport7(program, from, spec, "require") ??
|
|
npmStaticDepSf7(program, from, spec, "require");
|
|
return dep !== null && isCjsJsFile7(dep, program) ? dep : null;
|
|
};
|
|
}
|
|
|
|
/** Node's lexer analysis of a CommonJS file. A bundler rewrite spells some
|
|
* lexer-visible names as accessors. */
|
|
function lexWithAccessorNames(sf: ts.SourceFile): ReturnType<typeof cjsLexedExportsOfFile> {
|
|
const lexed = cjsLexedExportsOfFile(sf);
|
|
for (const name of npmStaticLexerVisibleNames(sf.fileName)) lexed.exports.add(name);
|
|
return lexed;
|
|
}
|
|
|
|
/** The property names of Node's namespace object for the CommonJS module
|
|
* `dep`: the names its lexer detects, CommonJS re-exports included (the
|
|
* rule of the named-import link check), plus `default` and
|
|
* `module.exports`. */
|
|
/** `dep` and every CommonJS module it re-exports, transitively (the modules
|
|
* whose exports its namespace object shows). */
|
|
export function cjsReexportedModules(program: ts.Program, dep: ts.SourceFile): ts.SourceFile[] {
|
|
const resolveDep = cjsReexportDep7(program);
|
|
const seen = new Set<ts.SourceFile>([dep]);
|
|
const order = [dep];
|
|
for (let i = 0; i < order.length; i++) {
|
|
const mod = order[i]!;
|
|
for (const spec of lexWithAccessorNames(mod).reexports) {
|
|
const next = resolveDep(mod, spec);
|
|
if (next !== null && !seen.has(next)) {
|
|
seen.add(next);
|
|
order.push(next);
|
|
}
|
|
}
|
|
}
|
|
return order;
|
|
}
|
|
|
|
export function cjsNamespaceExportNames(program: ts.Program, dep: ts.SourceFile): Set<string> {
|
|
const names = cjsVisibleNames(dep, lexWithAccessorNames, cjsReexportDep7(program), new Map());
|
|
return new Set([...names, "default", "module.exports"]);
|
|
}
|
|
|
|
/** The export names a CommonJS module assigns inside functions
|
|
* (`exports.current = value` in a function body), or "all" when such code
|
|
* writes a computed name, replaces module.exports or redefines exports
|
|
* through Object.defineProperty. Node's namespace object of the module
|
|
* holds the values the exports had when the module finished evaluating,
|
|
* so it does not see those writes. */
|
|
export function cjsExportsAssignedInFunctions(sf: ts.SourceFile): ReadonlySet<string> | "all" {
|
|
const names = new Set<string>();
|
|
let all = false;
|
|
const exportsObject = (e: ts.Expression): boolean =>
|
|
(ts.isIdentifier(e) && e.text === "exports") || isModuleExportsAccess(e);
|
|
const target = (e: ts.Expression): void => {
|
|
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
|
if (isModuleExportsAccess(e)) all = true;
|
|
else if (ts.isPropertyAccessExpression(e) && exportsObject(e.expression))
|
|
names.add(e.name.text);
|
|
else if (ts.isElementAccessExpression(e) && exportsObject(e.expression)) {
|
|
if (ts.isStringLiteralLike(e.argumentExpression)) names.add(e.argumentExpression.text);
|
|
else all = true;
|
|
}
|
|
};
|
|
const visit = (node: ts.Node, inFunction: boolean): void => {
|
|
if (all) return;
|
|
const nested = inFunction || ts.isFunctionLike(node);
|
|
if (nested) {
|
|
if (
|
|
ts.isBinaryExpression(node) &&
|
|
node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment &&
|
|
node.operatorToken.kind <= ts.SyntaxKind.LastAssignment
|
|
)
|
|
target(node.left);
|
|
else if (
|
|
(ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) &&
|
|
(node.operator === ts.SyntaxKind.PlusPlusToken ||
|
|
node.operator === ts.SyntaxKind.MinusMinusToken)
|
|
)
|
|
target(node.operand);
|
|
else if (ts.isDeleteExpression(node)) target(node.expression);
|
|
else if (
|
|
ts.isCallExpression(node) &&
|
|
ts.isPropertyAccessExpression(node.expression) &&
|
|
ts.isIdentifier(node.expression.expression) &&
|
|
node.expression.expression.text === "Object" &&
|
|
node.arguments.nodes[0] !== undefined &&
|
|
exportsObject(node.arguments.nodes[0])
|
|
)
|
|
all = true;
|
|
}
|
|
ts.forEachChild(node, (child) => visit(child, nested));
|
|
};
|
|
visit(sf, false);
|
|
return all ? "all" : names;
|
|
}
|
|
|
|
interface BadCjsImport {
|
|
/** The export name requested from the CJS module. */
|
|
exportName: string;
|
|
/** The import specifier as written. */
|
|
spec: string;
|
|
/** The failing name token (`a` in `import { a }` / `export { a } from`,
|
|
* the PROPERTY name in the aliased forms). */
|
|
nameNode: ts.Node;
|
|
sf: ts.SourceFile;
|
|
}
|
|
|
|
/** Walks the entry's ESM instantiate graph exactly as V8 does — children
|
|
* in request order, each module's own imports checked AFTER its children
|
|
* (postorder), regular imports sorted by LOCAL name (code units), then
|
|
* `export ... from` re-exports in source order — and answers the first
|
|
* named import of a lexer-INVISIBLE CommonJS export as Node's link-time
|
|
* SyntaxError (the caller compiles the program to that startup crash).
|
|
* ESM modules OUTSIDE the instantiate graph (reached only through
|
|
* require() of an ES module from CommonJS) instantiate mid-evaluation in
|
|
* Node, where the SyntaxError interleaves with output already produced —
|
|
* not modeled: those get a pointed fence instead. */
|
|
function cjsNamedImportLinkCheck(
|
|
program: ts.Program,
|
|
entry: ts.SourceFile,
|
|
moduleOrder: ts.SourceFile[],
|
|
diags: ScrDiagnostic[],
|
|
): StartupCrash | null {
|
|
const lexMemo = new Map<ts.SourceFile, Set<string>>();
|
|
// Project specifiers resolve as ever; aliases and package.json-mediated
|
|
// edges (#alias/self-name) join relative imports so
|
|
// a named import THROUGH one of a CommonJS module keeps Node's lexer
|
|
// check. Builtin/npm targets contribute nothing here — except opted-in
|
|
// --npm-static packages, whose CJS entries face Node's lexer exactly
|
|
// like program CJS files (their JS IS the program now).
|
|
const resolveEdge = (from: ts.SourceFile, spec: string): ts.SourceFile | null => {
|
|
const project = resolveImport7(program, from, spec);
|
|
if (project !== null) return project;
|
|
const npmStatic = npmStaticDepSf7(program, from, spec);
|
|
return npmStatic;
|
|
};
|
|
const resolveCjsDep = cjsReexportDep7(program);
|
|
const visible = (dep: ts.SourceFile, name: string): boolean =>
|
|
// `default` and `module.exports` are the module.exports binding itself
|
|
// — always provided. The program already owns native source ASTs;
|
|
// reuse them without starting another syntax session or reparsing
|
|
// their source text.
|
|
name === "default" ||
|
|
name === "module.exports" ||
|
|
cjsVisibleNames(dep, lexWithAccessorNames, resolveCjsDep, lexMemo).has(name);
|
|
|
|
/** The statement's resolved LOCAL CommonJS dependency, when it is an
|
|
* import/re-export from one. */
|
|
const cjsDepOf = (sf: ts.SourceFile, spec: string): ts.SourceFile | null => {
|
|
const dep = resolveEdge(sf, spec);
|
|
return dep !== null && isCjsJsFile7(dep, program) ? dep : null;
|
|
};
|
|
|
|
/** The first lexer-invisible CJS name request of ONE module, in V8's
|
|
* check order: all regular named imports sorted by local binding name,
|
|
* then `export { x } from` elements in source order. */
|
|
const firstInvisibleOf = (sf: ts.SourceFile): BadCjsImport | null => {
|
|
const entries: {
|
|
local: string;
|
|
exportName: string;
|
|
nameNode: ts.Node;
|
|
spec: string;
|
|
dep: ts.SourceFile;
|
|
}[] = [];
|
|
for (const stmt of sf.statements.nodes) {
|
|
if (
|
|
!ts.isImportDeclaration(stmt) ||
|
|
stmt.moduleSpecifier === undefined ||
|
|
!ts.isStringLiteral(stmt.moduleSpecifier)
|
|
)
|
|
continue;
|
|
const clause = stmt.importClause;
|
|
if (!clause || clause.phaseModifier === ts.SyntaxKind.TypeKeyword) continue;
|
|
if (!clause.namedBindings || !ts.isNamedImports(clause.namedBindings)) continue;
|
|
const spec = stmt.moduleSpecifier.text;
|
|
const dep = cjsDepOf(sf, spec);
|
|
if (dep === null) continue;
|
|
for (const e of clause.namedBindings.elements.nodes) {
|
|
if (e.isTypeOnly) continue;
|
|
const nameNode = e.propertyName ?? e.name;
|
|
entries.push({ local: e.name.text, exportName: nameNode.text, nameNode, spec, dep });
|
|
}
|
|
}
|
|
entries.sort((a, b) => (a.local < b.local ? -1 : a.local > b.local ? 1 : 0));
|
|
for (const e of entries) {
|
|
if (!visible(e.dep, e.exportName))
|
|
return { exportName: e.exportName, spec: e.spec, nameNode: e.nameNode, sf };
|
|
}
|
|
for (const stmt of sf.statements.nodes) {
|
|
if (!ts.isExportDeclaration(stmt) || stmt.isTypeOnly) continue;
|
|
if (stmt.moduleSpecifier === undefined || !ts.isStringLiteral(stmt.moduleSpecifier)) continue;
|
|
if (!stmt.exportClause || !ts.isNamedExports(stmt.exportClause)) continue;
|
|
const spec = stmt.moduleSpecifier.text;
|
|
const dep = cjsDepOf(sf, spec);
|
|
if (dep === null) continue;
|
|
for (const e of stmt.exportClause.elements.nodes) {
|
|
if (e.isTypeOnly) continue;
|
|
const nameNode = e.propertyName ?? e.name;
|
|
if (!visible(dep, nameNode.text)) return { exportName: nameNode.text, spec, nameNode, sf };
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
// The instantiate graph: from an ESM entry, every import/re-export
|
|
// request in source order, recursing into ESM dependencies only (CJS
|
|
// and JSON modules are leaves — their own requires never join V8's
|
|
// linking). A CommonJS ENTRY has no instantiate phase at all.
|
|
const visited = new Set<ts.SourceFile>();
|
|
const dfs = (sf: ts.SourceFile): BadCjsImport | null => {
|
|
if (visited.has(sf)) return null;
|
|
visited.add(sf);
|
|
for (const stmt of sf.statements.nodes) {
|
|
if (!ts.isImportDeclaration(stmt) && !(ts.isExportDeclaration(stmt) && !stmt.isTypeOnly))
|
|
continue;
|
|
if (ts.isExportDeclaration(stmt) && isPrunedNpmReexport(program, stmt)) continue;
|
|
if (
|
|
ts.isImportDeclaration(stmt) &&
|
|
stmt.importClause?.phaseModifier === ts.SyntaxKind.TypeKeyword
|
|
)
|
|
continue;
|
|
const specNode = stmt.moduleSpecifier;
|
|
if (specNode === undefined || !ts.isStringLiteral(specNode)) continue;
|
|
const spec = specNode.text;
|
|
const dep = resolveEdge(sf, spec);
|
|
if (dep === null || dep.fileName.endsWith(".json") || !isNodeEsmFile7(dep, program)) continue;
|
|
const bad = dfs(dep);
|
|
if (bad !== null) return bad;
|
|
}
|
|
return firstInvisibleOf(sf);
|
|
};
|
|
const bad = isNodeEsmFile7(entry, program) ? dfs(entry) : null;
|
|
|
|
// ESM modules only require() reaches: same invisibility, but Node's
|
|
// SyntaxError fires mid-evaluation at the require — a pointed fence
|
|
// instead of a mismodeled crash position.
|
|
for (const sf of moduleOrder) {
|
|
if (!isNodeEsmFile7(sf, program) || visited.has(sf)) continue;
|
|
const b = firstInvisibleOf(sf);
|
|
if (b !== null) {
|
|
diags.push(
|
|
unsupportedDiag(
|
|
"SC1013",
|
|
locOf7(b.nameNode),
|
|
`a named import of a CommonJS export Node's lexer cannot detect, reached through require() of an ES module (Node throws its SyntaxError mid-evaluation at that require; import the CommonJS module's default and read members off it instead)`,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
if (bad === null) return null;
|
|
|
|
// Node's message: V8's generic missing-export SyntaxError, rewritten to
|
|
// the CommonJS hint form only when the failing SPECIFIER STRING is also
|
|
// one of the ENTRY module's own requests and the entry's resolution of
|
|
// it is CommonJS (module_job.js consults the ROOT job's moduleRequests/
|
|
// commonJsDeps — a deep module's failure keeps the generic wording).
|
|
let flavored = false;
|
|
for (const stmt of entry.statements.nodes) {
|
|
if (!ts.isImportDeclaration(stmt) && !(ts.isExportDeclaration(stmt) && !stmt.isTypeOnly))
|
|
continue;
|
|
if (
|
|
ts.isImportDeclaration(stmt) &&
|
|
stmt.importClause?.phaseModifier === ts.SyntaxKind.TypeKeyword
|
|
)
|
|
continue;
|
|
const specNode = stmt.moduleSpecifier;
|
|
if (specNode === undefined || !ts.isStringLiteral(specNode) || specNode.text !== bad.spec)
|
|
continue;
|
|
if (cjsDepOf(entry, bad.spec) !== null) {
|
|
flavored = true;
|
|
break;
|
|
}
|
|
}
|
|
const loc = locOf7(bad.nameNode);
|
|
if (!flavored) {
|
|
return {
|
|
message: `The requested module '${bad.spec}' does not provide an export named '${bad.exportName}'`,
|
|
className: "%SyntaxError",
|
|
loc,
|
|
};
|
|
}
|
|
// The destructuring hint: Node regexes the failing name's SOURCE LINE
|
|
// for its brace group (greedy, first `{` to last `}` on the line) and
|
|
// swaps ` as ` for `: ` — reproduced exactly, quirks included
|
|
// (multi-line import statements simply lose the hint).
|
|
const text = bad.sf.text;
|
|
const pos = ts.tokenStartOfNode(bad.nameNode);
|
|
const lineStart = text.lastIndexOf("\n", pos - 1) + 1;
|
|
const lineEnd = text.indexOf("\n", pos);
|
|
let line = text.slice(lineStart, lineEnd < 0 ? text.length : lineEnd);
|
|
if (line.endsWith("\r")) line = line.slice(0, -1);
|
|
const braces = /{.*}/.exec(line);
|
|
const destructuring = braces === null ? null : braces[0].replace(/\s+as\s+/g, ": ");
|
|
return {
|
|
message:
|
|
`Named export '${bad.exportName}' not found. The requested module '${bad.spec}' is a CommonJS module, which may not support all module.exports as named exports.\n` +
|
|
"CommonJS modules can always be imported via the default export, for example using:\n\n" +
|
|
`import pkg from '${bad.spec}';\n` +
|
|
(destructuring !== null ? `const ${destructuring} = pkg;\n` : ""),
|
|
className: "%SyntaxError",
|
|
loc,
|
|
};
|
|
}
|
|
|
|
/* ── the native ESM named-import link check ─────────────────────────────
|
|
* TypeScript accepts `import { Shape } from "./types.ts"` when Shape is an
|
|
* interface or type alias: the checker is answering a TYPE question. Node's
|
|
* ESM linker asks a VALUE question instead. Because Node 24's strip-only
|
|
* execution leaves that import request in the module graph, the request
|
|
* fails before any module evaluates unless the source uses `import type`.
|
|
*
|
|
* Keep this separate from cjsNamedImportLinkCheck. CommonJS exports are
|
|
* determined by Node's source lexer and have their own interop message and
|
|
* ordering rules; native ESM exports come from the resolved TypeScript
|
|
* module symbol and use Node's generic missing-export SyntaxError. */
|
|
interface BadEsmImport {
|
|
exportName: string;
|
|
spec: string;
|
|
nameNode: ts.Node;
|
|
sf: ts.SourceFile;
|
|
}
|
|
|
|
interface EsmNamedImportLinkAnalysis {
|
|
crash: StartupCrash | null;
|
|
visited: Set<ts.SourceFile>;
|
|
firstMissingOf: (sf: ts.SourceFile) => BadEsmImport | null;
|
|
}
|
|
|
|
/** Node's strip-only loader exposes an empty `default` export for a default
|
|
* interface only when the `.ts` file is in an ambiguous (typeless) package
|
|
* scope. Explicitly ESM files (`.mts` and `.mjs`) and `.ts` files in a
|
|
* `"type": "module"` package do not get that placeholder. */
|
|
function hasNodeTsDefaultInterfacePlaceholder7(sf: ts.SourceFile): boolean {
|
|
return sf.fileName.endsWith(".ts") && nearestPackageType(sf.fileName) === null;
|
|
}
|
|
|
|
/** Finds the first native-ESM link failure in the static module graph rooted
|
|
* at `entry`. This is used both for startup linking of the entry graph and
|
|
* for dynamic imports: a dynamically loaded graph rejects its import promise
|
|
* at this same link point instead of running any module body. */
|
|
function analyzeEsmNamedImportLinks(
|
|
program: ts.Program,
|
|
entry: ts.SourceFile,
|
|
): EsmNamedImportLinkAnalysis {
|
|
const checker = program.getTypeChecker();
|
|
const resolveEdge = (from: ts.SourceFile, spec: string): ts.SourceFile | null => {
|
|
const project = resolveImport7(program, from, spec);
|
|
if (project !== null) return project;
|
|
return npmStaticDepSf7(program, from, spec);
|
|
};
|
|
|
|
const runtimeExport = (
|
|
dep: ts.SourceFile,
|
|
name: string,
|
|
seenModules: ReadonlySet<ts.SourceFile> = new Set(),
|
|
): ts.Symbol | undefined => {
|
|
if (seenModules.has(dep)) return undefined;
|
|
const nextSeen = new Set(seenModules);
|
|
nextSeen.add(dep);
|
|
const module = checker.getSymbolAtLocation(dep);
|
|
const exported = module?.getExports().get(ts.escapedSymbolName(name));
|
|
if (exported !== undefined) {
|
|
let resolved = exported;
|
|
const seenSymbols = new Set<ts.Symbol>();
|
|
while ((resolved.flags & ts.SymbolFlags.Alias) !== 0 && !seenSymbols.has(resolved)) {
|
|
seenSymbols.add(resolved);
|
|
resolved = checker.getAliasedSymbol(resolved);
|
|
}
|
|
if (resolved.flags & ts.SymbolFlags.Value) return resolved;
|
|
// Node's strip-only TypeScript loader materializes a direct `export
|
|
// default interface X {}` as an empty default export in an ambiguous
|
|
// `.ts` file. The checker correctly classifies the declaration as
|
|
// type-only, but the native ESM linker sees the runtime placeholder.
|
|
if (
|
|
name === "default" &&
|
|
hasNodeTsDefaultInterfacePlaceholder7(dep) &&
|
|
resolved.declarations.some(
|
|
(declaration) =>
|
|
ts.isInterfaceDeclaration(declaration) &&
|
|
ts
|
|
.getModifiers(declaration)
|
|
?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword) === true &&
|
|
ts
|
|
.getModifiers(declaration)
|
|
?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) === true,
|
|
)
|
|
) {
|
|
return resolved;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
// TypeScript 7's client-side module symbol exposes the module's own
|
|
// export table but not names contributed by `export *`. Follow those
|
|
// source edges explicitly, applying the ESM ambiguity rule: the same
|
|
// resolved symbol may arrive through multiple stars, while distinct
|
|
// symbols make the name unavailable. `default` never star-reexports.
|
|
if (name === "default") return undefined;
|
|
let starExport: ts.Symbol | undefined;
|
|
for (const stmt of dep.statements.nodes) {
|
|
if (!ts.isExportDeclaration(stmt) || stmt.isTypeOnly || stmt.exportClause !== undefined)
|
|
continue;
|
|
if (stmt.moduleSpecifier === undefined || !ts.isStringLiteral(stmt.moduleSpecifier)) continue;
|
|
const target = resolveEdge(dep, stmt.moduleSpecifier.text);
|
|
if (target === null || target.fileName.endsWith(".json") || !isNodeEsmFile7(target, program))
|
|
continue;
|
|
const candidate = runtimeExport(target, name, nextSeen);
|
|
if (candidate === undefined) continue;
|
|
if (starExport !== undefined && starExport !== candidate) return undefined;
|
|
starExport = candidate;
|
|
}
|
|
return starExport;
|
|
};
|
|
|
|
const firstMissingOf = (sf: ts.SourceFile): BadEsmImport | null => {
|
|
const imports: {
|
|
local: string;
|
|
exportName: string;
|
|
spec: string;
|
|
nameNode: ts.Node;
|
|
dep: ts.SourceFile;
|
|
}[] = [];
|
|
for (const stmt of sf.statements.nodes) {
|
|
if (!ts.isImportDeclaration(stmt) || !ts.isStringLiteral(stmt.moduleSpecifier)) continue;
|
|
const clause = stmt.importClause;
|
|
if (clause === undefined || clause.phaseModifier === ts.SyntaxKind.TypeKeyword) continue;
|
|
const spec = stmt.moduleSpecifier.text;
|
|
const dep = resolveEdge(sf, spec);
|
|
if (dep === null || dep.fileName.endsWith(".json") || !isNodeEsmFile7(dep, program)) continue;
|
|
if (clause.name !== undefined) {
|
|
imports.push({
|
|
local: clause.name.text,
|
|
exportName: "default",
|
|
spec,
|
|
nameNode: clause.name,
|
|
dep,
|
|
});
|
|
}
|
|
if (clause.namedBindings === undefined || !ts.isNamedImports(clause.namedBindings)) continue;
|
|
for (const element of clause.namedBindings.elements.nodes) {
|
|
if (element.isTypeOnly) continue;
|
|
const nameNode = element.propertyName ?? element.name;
|
|
imports.push({
|
|
local: element.name.text,
|
|
exportName: nameNode.text,
|
|
spec,
|
|
nameNode,
|
|
dep,
|
|
});
|
|
}
|
|
}
|
|
// Node checks regular named requests in local-binding order. Keeping the
|
|
// same order as the CommonJS linker matters when one statement asks for
|
|
// more than one erased TypeScript export.
|
|
imports.sort((a, b) => (a.local < b.local ? -1 : a.local > b.local ? 1 : 0));
|
|
for (const request of imports) {
|
|
if (runtimeExport(request.dep, request.exportName) === undefined) {
|
|
return {
|
|
exportName: request.exportName,
|
|
spec: request.spec,
|
|
nameNode: request.nameNode,
|
|
sf,
|
|
};
|
|
}
|
|
}
|
|
|
|
for (const stmt of sf.statements.nodes) {
|
|
if (!ts.isExportDeclaration(stmt) || stmt.isTypeOnly) continue;
|
|
const moduleSpecifier = stmt.moduleSpecifier;
|
|
const exportClause = stmt.exportClause;
|
|
if (moduleSpecifier === undefined || exportClause === undefined) continue;
|
|
if (!ts.isStringLiteral(moduleSpecifier) || !ts.isNamedExports(exportClause)) continue;
|
|
const spec = moduleSpecifier.text;
|
|
const dep = resolveEdge(sf, spec);
|
|
if (dep === null || dep.fileName.endsWith(".json") || !isNodeEsmFile7(dep, program)) continue;
|
|
for (const element of exportClause.elements.nodes) {
|
|
if (element.isTypeOnly) continue;
|
|
const nameNode = element.propertyName ?? element.name;
|
|
if (runtimeExport(dep, nameNode.text) === undefined) {
|
|
return { exportName: nameNode.text, spec, nameNode, sf };
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
// The ESM instantiate graph is the same depth-first graph used by the CJS
|
|
// linker: dependencies are linked before the importing module, and a
|
|
// CommonJS/JSON target is a leaf for this native-export check.
|
|
const visited = new Set<ts.SourceFile>();
|
|
const dfs = (sf: ts.SourceFile): BadEsmImport | null => {
|
|
if (visited.has(sf)) return null;
|
|
visited.add(sf);
|
|
for (const stmt of sf.statements.nodes) {
|
|
if (!ts.isImportDeclaration(stmt) && !(ts.isExportDeclaration(stmt) && !stmt.isTypeOnly))
|
|
continue;
|
|
if (ts.isExportDeclaration(stmt) && isPrunedNpmReexport(program, stmt)) continue;
|
|
if (
|
|
ts.isImportDeclaration(stmt) &&
|
|
stmt.importClause?.phaseModifier === ts.SyntaxKind.TypeKeyword
|
|
)
|
|
continue;
|
|
const moduleSpecifier = stmt.moduleSpecifier;
|
|
if (moduleSpecifier === undefined || !ts.isStringLiteral(moduleSpecifier)) continue;
|
|
const dep = resolveEdge(sf, moduleSpecifier.text);
|
|
if (dep === null || dep.fileName.endsWith(".json") || !isNodeEsmFile7(dep, program)) continue;
|
|
const bad = dfs(dep);
|
|
if (bad !== null) return bad;
|
|
}
|
|
return firstMissingOf(sf);
|
|
};
|
|
|
|
// Native ESM linking itself starts only from an ESM entry. The caller
|
|
// supplies dynamic-only roots here too; each such root gets its own
|
|
// promise rejection rather than a startup crash.
|
|
const bad = isNodeEsmFile7(entry, program) ? dfs(entry) : null;
|
|
if (bad !== null) {
|
|
return {
|
|
crash: {
|
|
message: `The requested module '${bad.spec}' does not provide an export named '${bad.exportName}'`,
|
|
className: "%SyntaxError",
|
|
loc: locOf7(bad.nameNode),
|
|
},
|
|
visited,
|
|
firstMissingOf,
|
|
};
|
|
}
|
|
|
|
return { crash: null, visited, firstMissingOf };
|
|
}
|
|
|
|
export function esmNamedImportLinkCrash(
|
|
program: ts.Program,
|
|
entry: ts.SourceFile,
|
|
): StartupCrash | null {
|
|
return analyzeEsmNamedImportLinks(program, entry).crash;
|
|
}
|
|
|
|
function esmNamedImportLinkCheck(
|
|
program: ts.Program,
|
|
entry: ts.SourceFile,
|
|
moduleOrder: ts.SourceFile[],
|
|
diags: ScrDiagnostic[],
|
|
): StartupCrash | null {
|
|
const analysis = analyzeEsmNamedImportLinks(program, entry);
|
|
if (analysis.crash !== null) return analysis.crash;
|
|
|
|
// A CommonJS entry can synchronously require an ESM graph. Node links that
|
|
// graph at the require site, after the CommonJS module has already begun
|
|
// evaluating, so this cannot use startupCrash (which would move the error
|
|
// before earlier output). Keep the compile-time fence used by the CJS link
|
|
// checker for the analogous mid-evaluation failure instead.
|
|
for (const sf of moduleOrder) {
|
|
if (!isNodeEsmFile7(sf, program) || analysis.visited.has(sf)) continue;
|
|
const childFailure = analysis.firstMissingOf(sf);
|
|
if (childFailure !== null) {
|
|
diags.push(
|
|
unsupportedDiag(
|
|
"SC1013",
|
|
locOf7(childFailure.nameNode),
|
|
"a named import of an unavailable export in an ES module reached through require() (Node throws its SyntaxError mid-evaluation at that require; make the import match the module's runtime exports)",
|
|
),
|
|
);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** Both named-export link checks run independently because their export
|
|
* questions are different (Node's CJS lexer versus TypeScript's value
|
|
* symbols). Their first failures still share one Node instantiate order: the
|
|
* module postorder established by preflight, then regular named imports by
|
|
* local binding name, then source-order re-exports. Pick the earlier result
|
|
* so a native ESM failure in a child cannot be hidden by a CJS failure in a
|
|
* later parent. */
|
|
function earlierLinkCrash(
|
|
moduleOrder: ts.SourceFile[],
|
|
first: StartupCrash | null,
|
|
second: StartupCrash | null,
|
|
): StartupCrash | null {
|
|
if (first === null) return second;
|
|
if (second === null) return first;
|
|
const moduleIndex = new Map(moduleOrder.map((sf, index) => [sf.fileName, index]));
|
|
const firstIndex = moduleIndex.get(first.loc.file) ?? Number.MAX_SAFE_INTEGER;
|
|
const secondIndex = moduleIndex.get(second.loc.file) ?? Number.MAX_SAFE_INTEGER;
|
|
if (firstIndex !== secondIndex) return firstIndex < secondIndex ? first : second;
|
|
if (first.loc.start !== second.loc.start) {
|
|
const sf = moduleOrder[firstIndex];
|
|
if (sf === undefined) return first.loc.start < second.loc.start ? first : second;
|
|
const rank = (crash: StartupCrash): [number, number] => {
|
|
const entries: { local: string; start: number; kind: 0 | 1 }[] = [];
|
|
for (const stmt of sf.statements.nodes) {
|
|
if (ts.isImportDeclaration(stmt) && ts.isStringLiteral(stmt.moduleSpecifier)) {
|
|
const clause = stmt.importClause;
|
|
if (clause?.phaseModifier === ts.SyntaxKind.TypeKeyword) continue;
|
|
if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings)) {
|
|
for (const element of clause.namedBindings.elements.nodes) {
|
|
if (!element.isTypeOnly) {
|
|
entries.push({
|
|
local: element.name.text,
|
|
start: ts.tokenStartOfNode(element.propertyName ?? element.name),
|
|
kind: 0,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
entries.sort((a, b) => (a.local < b.local ? -1 : a.local > b.local ? 1 : 0));
|
|
const direct = entries.findIndex((entry) => entry.start === crash.loc.start);
|
|
if (direct >= 0) return [0, direct];
|
|
let reexport = 0;
|
|
for (const stmt of sf.statements.nodes) {
|
|
const exportClause = ts.isExportDeclaration(stmt) ? stmt.exportClause : undefined;
|
|
if (
|
|
!ts.isExportDeclaration(stmt) ||
|
|
stmt.isTypeOnly ||
|
|
exportClause === undefined ||
|
|
!ts.isNamedExports(exportClause)
|
|
)
|
|
continue;
|
|
for (const element of exportClause.elements.nodes) {
|
|
if (
|
|
!element.isTypeOnly &&
|
|
ts.tokenStartOfNode(element.propertyName ?? element.name) === crash.loc.start
|
|
) {
|
|
return [1, reexport];
|
|
}
|
|
if (!element.isTypeOnly) reexport++;
|
|
}
|
|
}
|
|
return [2, crash.loc.start];
|
|
};
|
|
const firstRank = rank(first);
|
|
const secondRank = rank(second);
|
|
if (firstRank[0] !== secondRank[0]) return firstRank[0] < secondRank[0] ? first : second;
|
|
return firstRank[1] <= secondRank[1] ? first : second;
|
|
}
|
|
return first;
|
|
}
|
|
|
|
/* node's builtin-module name list, for the SC1010 wording decision ("the
|
|
* 'fs' module" vs the generic package message) — shared verbatim with the
|
|
* retired 5.9.3 lane's builtinModules use. */
|
|
const nodeBuiltinNames: ReadonlySet<string> = new Set(builtinModules);
|
|
|
|
/* ── the lowering-facing surface ─────────────────────────────────────────
|
|
* A family of world-typed helpers the LOWERING imports (require
|
|
* recognizers, the CJS export-identity analysis, ordered imports,
|
|
* resolution, locations), exported under the names the 5.9.3 program.ts
|
|
* established (the phase-2 port kept every spelling); the world-neutral
|
|
* helpers re-export from their focused modules. */
|
|
|
|
export {
|
|
ambientDtsPath,
|
|
fallbackDtsPath,
|
|
isFallbackDtsPath,
|
|
isNodeTypesPath,
|
|
overridesDtsPath,
|
|
} from "./dts-paths.js";
|
|
|
|
export {
|
|
builtinDefaultImportModule,
|
|
canonicalBuiltinModule,
|
|
npmBuiltinAliasOf,
|
|
SUPPORTED_BUILTIN_MODULES,
|
|
} from "./builtin-modules.js";
|
|
|
|
export { npmPackageNameOf, workspacePackageOfPath } from "./workspace-registry.js";
|
|
|
|
export {
|
|
requireSpecOf7 as requireSpecOf,
|
|
isRequireStatement7 as isRequireStatement,
|
|
requiresOf7 as requiresOf,
|
|
splitRequireCallOf7 as splitRequireCallOf,
|
|
isNodeEsmFile7 as isNodeEsmFile,
|
|
resolveImport7 as resolveImport,
|
|
resolveNpmImport7 as resolveNpmImport,
|
|
locOf7 as locOf,
|
|
requireEarlyReference7 as requireEarlyReference,
|
|
inDeferredPosition7 as inDeferredPosition,
|
|
};
|
|
|
|
/** True for JavaScript source files (.js/.mjs/.cjs) — the files whose
|
|
* types come entirely from inference (checkJs + JSDoc) and whose classes
|
|
* declare fields by constructor assignment. */
|
|
export function isJsSourceFile(sf: ts.SourceFile): boolean {
|
|
return isJsSourceFileName(sf.fileName);
|
|
}
|
|
|
|
/** True for a JavaScript file of the program that TypeScript type-checks:
|
|
* one with a `// @ts-check` comment before its first statement, or any
|
|
* file of a project whose tsconfig sets `checkJs` unless the file has a
|
|
* `// @ts-nocheck` comment. Files under node_modules are never checked. */
|
|
export function isCheckedJsSourceFile(sf: ts.SourceFile, projectCheckJs: boolean): boolean {
|
|
if (!isJsSourceFile(sf) || isNodeModulesPath(sf.fileName)) return false;
|
|
let checked = projectCheckJs;
|
|
const head = sf.text.slice(
|
|
0,
|
|
(sf.statements.nodes.length === 0 ? undefined : ts.tokenStartOfNode(sf.statements.nodes[0]!)) ??
|
|
sf.text.length,
|
|
);
|
|
for (const line of head.split("\n")) {
|
|
const directive = /^\s*\/\/\s*@ts-(no)?check\b/.exec(line);
|
|
if (directive) checked = directive[1] === undefined;
|
|
}
|
|
return checked;
|
|
}
|
|
|
|
export function orderedImportsOf(
|
|
program: ts.Program,
|
|
sf: ts.SourceFile,
|
|
): { stmt: ts.Statement; dep: ts.SourceFile | null }[] {
|
|
const out: { stmt: ts.Statement; dep: ts.SourceFile | null }[] = [];
|
|
for (const stmt of sf.statements.nodes) {
|
|
if (ts.isExportDeclaration(stmt) && isPrunedNpmReexport(program, stmt)) continue;
|
|
if (ts.isExportDeclaration(stmt) && (stmt.isTypeOnly || !stmt.moduleSpecifier)) continue;
|
|
if (!ts.isImportDeclaration(stmt) && !ts.isExportDeclaration(stmt)) continue;
|
|
if (ts.isImportDeclaration(stmt) && erasedTypeOnlyImport(stmt)) continue;
|
|
if (!stmt.moduleSpecifier || !ts.isStringLiteral(stmt.moduleSpecifier)) continue;
|
|
const spec = stmt.moduleSpecifier.text;
|
|
// Every project edge (relative, tsconfig alias, #alias, or self-name)
|
|
// resolves to the same dep so the importer's %init header calls theirs.
|
|
// Builtin and npm bare specifiers stay null — EXCEPT opted-in --npm-static
|
|
// packages, whose entries are program modules the header must init.
|
|
const dep = resolveImport7(program, sf, spec) ?? npmStaticDepSf7(program, sf, spec);
|
|
const isJson = dep !== null && dep.fileName.endsWith(".json");
|
|
out.push({ stmt, dep: isJson ? null : dep });
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** The files a compiled program can load: the module order closed over
|
|
* every literal require() and import() call it contains (deferred and
|
|
* nested ones included) and, for opted-in package JavaScript, over every
|
|
* import and export declaration as well. A package file outside this set
|
|
* is reachable only through types, so whatever it spells cannot affect
|
|
* the compiled program. Null when a reachable file loads a computed
|
|
* specifier: then any file may load. */
|
|
export function npmStaticLoadableFiles(
|
|
program: ts.Program,
|
|
moduleOrder: readonly ts.SourceFile[],
|
|
): Set<string> | null {
|
|
const seen = new Set<ts.SourceFile>();
|
|
const work: ts.SourceFile[] = [];
|
|
const add = (sf: ts.SourceFile | null | undefined): void => {
|
|
if (sf === null || sf === undefined || seen.has(sf) || sf.isDeclarationFile) return;
|
|
seen.add(sf);
|
|
work.push(sf);
|
|
};
|
|
for (const sf of moduleOrder) add(sf);
|
|
let computed = false;
|
|
while (work.length > 0 && !computed) {
|
|
const sf = work.pop()!;
|
|
const packageJs =
|
|
npmStaticPackageOfPath(sf.fileName) !== null && isJsSourceFileName(sf.fileName);
|
|
const follow = (spec: string, kind: "import" | "require"): void => {
|
|
if (canonicalBuiltinModule(spec) !== null) return;
|
|
add(resolveImport7(program, sf, spec, kind) ?? npmStaticDepSf7(program, sf, spec, kind));
|
|
};
|
|
ts.walkPreorder(sf, (n) => {
|
|
if (computed) return "stop";
|
|
if (ts.isCallExpression(n)) {
|
|
const isImport = n.expression.kind === ts.SyntaxKind.ImportKeyword;
|
|
const isRequire = ts.isIdentifier(n.expression) && n.expression.text === "require";
|
|
if (!isImport && !isRequire) return undefined;
|
|
const arg = n.arguments.nodes[0];
|
|
if (arg !== undefined && ts.isStringLiteralLike(arg))
|
|
follow(arg.text, isImport ? "import" : "require");
|
|
else if (packageJs || isImport) computed = true;
|
|
} else if (
|
|
packageJs &&
|
|
(ts.isImportDeclaration(n) || ts.isExportDeclaration(n)) &&
|
|
n.moduleSpecifier !== undefined &&
|
|
ts.isStringLiteral(n.moduleSpecifier)
|
|
) {
|
|
follow(n.moduleSpecifier.text, "import");
|
|
}
|
|
return undefined;
|
|
});
|
|
}
|
|
return computed ? null : new Set([...seen].map((sf) => sf.fileName.split("\\").join("/")));
|
|
}
|
|
|
|
/** The program source file a bare specifier reaches when it names an
|
|
* opted-in --npm-static package (the shipped-JS entry preflight admitted
|
|
* as a module edge), else null. No offender reporting here — preflight
|
|
* already classified the import; this is the lookup the module-order and
|
|
* lowering paths share. */
|
|
export function npmStaticDepSf7(
|
|
program: ts.Program,
|
|
sf: ts.SourceFile,
|
|
spec: string,
|
|
resolutionKind: "import" | "require" = "import",
|
|
): ts.SourceFile | null {
|
|
if (!npmStaticActive() || isRelativeSpecifier(spec)) return null;
|
|
if (spec.startsWith("node:") || spec.startsWith("#")) return null;
|
|
const npm = resolveNpmImport7(sf.fileName, spec, resolutionKind);
|
|
if (npm === null || !isNpmStaticPackage(npm.packageName)) return null;
|
|
if (!isJsSourceFileName(npm.typesFile)) return null;
|
|
return program.getSourceFile(npm.typesFile) ?? null;
|
|
}
|
|
|
|
/** True when `expr` is the `module.exports` property access. */
|
|
export function isModuleExportsAccess(expr: ts.Expression): expr is ts.PropertyAccessExpression {
|
|
return (
|
|
ts.isPropertyAccessExpression(expr) &&
|
|
!expr.questionDotToken &&
|
|
ts.isIdentifier(expr.expression) &&
|
|
expr.expression.text === "module" &&
|
|
expr.name.text === "exports"
|
|
);
|
|
}
|
|
|
|
/** Classifies a statement as a CommonJS EXPORT assignment:
|
|
* - table: `module.exports = { ... }` — the whole export table at once
|
|
* (obj is null when the RHS is not an object literal — the
|
|
* recognized-but-unsupported form, fenced by the caller);
|
|
* - member: `exports.f = <expr>` / `module.exports.f = <expr>`.
|
|
* Null for everything else. Callers gate on top-level position in a JS
|
|
* module file — these forms anywhere else keep their generic fences. */
|
|
export function cjsExportAssignmentOf(stmt: ts.Statement):
|
|
| {
|
|
kind: "table";
|
|
obj: ts.ObjectLiteralExpression | null;
|
|
expr: ts.BinaryExpression;
|
|
/** `exports = module.exports = value`: the `exports` binding names
|
|
* the new value too, so later `exports.f =` members attach to it. */
|
|
rebindsExports?: true;
|
|
}
|
|
| { kind: "member"; name: ts.MemberName; value: ts.Expression; expr: ts.BinaryExpression }
|
|
| null {
|
|
if (!ts.isExpressionStatement(stmt)) return null;
|
|
const e = stmt.expression;
|
|
if (!ts.isBinaryExpression(e) || e.operatorToken.kind !== ts.SyntaxKind.EqualsToken) return null;
|
|
if (isModuleExportsAccess(e.left)) {
|
|
return { kind: "table", obj: ts.isObjectLiteralExpression(e.right) ? e.right : null, expr: e };
|
|
}
|
|
if (ts.isIdentifier(e.left) && e.left.text === "exports") {
|
|
let inner: ts.Expression = e.right;
|
|
while (ts.isParenthesizedExpression(inner)) inner = inner.expression;
|
|
if (
|
|
ts.isBinaryExpression(inner) &&
|
|
inner.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
isModuleExportsAccess(inner.left) &&
|
|
// `exports = module.exports = {}` is the fresh-table reset, which the
|
|
// export walk handles on its own.
|
|
!ts.isObjectLiteralExpression(inner.right)
|
|
) {
|
|
return { kind: "table", obj: null, expr: inner, rebindsExports: true };
|
|
}
|
|
}
|
|
if (ts.isPropertyAccessExpression(e.left) && !e.left.questionDotToken) {
|
|
const recv = e.left.expression;
|
|
const isExports = ts.isIdentifier(recv) && recv.text === "exports";
|
|
if (isExports || isModuleExportsAccess(recv)) {
|
|
return { kind: "member", name: e.left.name, value: e.right, expr: e };
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** The tsc VOID-INIT PREAMBLE (`exports.a = exports.b = void 0;` with the
|
|
* real `exports.a = a;` further down; every tsc-emitted dist file opens its
|
|
* exports this way): when every name in the chain has a later real
|
|
* assignment, the export takes its value there and the preamble lowers to
|
|
* nothing. Returns those later assignments by name, or null when this is
|
|
* not such a preamble (a preamble with no later real assignment keeps the
|
|
* ordinary path: the export IS undefined then). */
|
|
export function cjsVoidInitPreambleReals(
|
|
cjs: Extract<ReturnType<typeof cjsExportAssignmentOf>, { kind: "member" }>,
|
|
): Map<string, Extract<ReturnType<typeof cjsExportAssignmentOf>, { kind: "member" }>> | null {
|
|
if (!ts.isIdentifier(cjs.name)) return null;
|
|
const isExportsMember = (e: ts.Expression): e is ts.PropertyAccessExpression =>
|
|
ts.isPropertyAccessExpression(e) &&
|
|
ts.isIdentifier(e.name) &&
|
|
((ts.isIdentifier(e.expression) && e.expression.text === "exports") ||
|
|
(ts.isPropertyAccessExpression(e.expression) &&
|
|
ts.isIdentifier(e.expression.expression) &&
|
|
e.expression.expression.text === "module" &&
|
|
ts.isIdentifier(e.expression.name) &&
|
|
e.expression.name.text === "exports"));
|
|
const isVoid = (e: ts.Expression): boolean => {
|
|
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
|
return (
|
|
e.kind === ts.SyntaxKind.VoidExpression || (ts.isIdentifier(e) && e.text === "undefined")
|
|
);
|
|
};
|
|
// chains: `exports.a = exports.b = void 0;` collects every name
|
|
const names: string[] = [cjs.name.text];
|
|
let tail: ts.Expression = cjs.value;
|
|
while (
|
|
ts.isBinaryExpression(tail) &&
|
|
tail.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
|
|
isExportsMember(tail.left)
|
|
) {
|
|
names.push((tail.left.name as ts.Identifier).text);
|
|
tail = tail.right;
|
|
}
|
|
if (!isVoid(tail)) return null;
|
|
const sf = ts.getSourceFileOfNode(cjs.expr);
|
|
const myPos = ts.tokenStartOfNode(cjs.expr);
|
|
const reals = new Map<
|
|
string,
|
|
Extract<ReturnType<typeof cjsExportAssignmentOf>, { kind: "member" }>
|
|
>();
|
|
for (const name of names) {
|
|
const real = sf.statements.nodes
|
|
.map((s) => cjsExportAssignmentOf(s))
|
|
.find(
|
|
(other) =>
|
|
other?.kind === "member" &&
|
|
ts.isIdentifier(other.name) &&
|
|
other.name.text === name &&
|
|
ts.tokenStartOfNode(other.expr) > myPos &&
|
|
!isVoid(other.value),
|
|
);
|
|
if (real?.kind !== "member") return null;
|
|
reals.set(name, real);
|
|
}
|
|
return reals;
|
|
}
|
|
|
|
/** The members and shared value of a CHAINED member export
|
|
* `exports.a = exports.b = module.exports.c = value` (outermost name
|
|
* first), or null when the assignment is not a chain of at least two
|
|
* identifier-named export members or its value is undefined. */
|
|
export function cjsExportChainOf(
|
|
e: ts.BinaryExpression,
|
|
): { names: ts.Identifier[]; value: ts.Expression } | null {
|
|
const names: ts.Identifier[] = [];
|
|
let cur: ts.Expression = e;
|
|
for (;;) {
|
|
while (ts.isParenthesizedExpression(cur)) cur = cur.expression;
|
|
if (!ts.isBinaryExpression(cur) || cur.operatorToken.kind !== ts.SyntaxKind.EqualsToken) break;
|
|
const left = cur.left;
|
|
if (
|
|
!ts.isPropertyAccessExpression(left) ||
|
|
left.questionDotToken ||
|
|
!ts.isIdentifier(left.name)
|
|
)
|
|
break;
|
|
const recv = left.expression;
|
|
if (!((ts.isIdentifier(recv) && recv.text === "exports") || isModuleExportsAccess(recv))) break;
|
|
names.push(left.name);
|
|
cur = cur.right;
|
|
}
|
|
// tsc's `exports.a = exports.b = void 0;` preamble is not a value chain
|
|
// (the export-statement lowering owns it).
|
|
const isVoid =
|
|
cur.kind === ts.SyntaxKind.VoidExpression || (ts.isIdentifier(cur) && cur.text === "undefined");
|
|
return names.length >= 2 && !isVoid ? { names, value: cur } : null;
|
|
}
|
|
|
|
/** Why Node would DISCARD this CommonJS export statement, or null when it
|
|
* survives (Node's export-object identity rules — checker-free, pure
|
|
* AST): `module.exports = {...}` REPLACES the export object, so members
|
|
* attached before it (either spelling), any earlier table, and
|
|
* `exports.f =` after it (exports still references the replaced object)
|
|
* are never seen by importers. Lowering fences these instead of shipping
|
|
* the checker's everything-is-live answer — the checker/runtime split
|
|
* would otherwise diverge from Node without a diagnostic. */
|
|
export function cjsExportDiscardReason(stmt: ts.Statement): string | null {
|
|
const sf = stmt.parent;
|
|
if (!ts.isSourceFile(sf)) return null;
|
|
const mine = cjsExportAssignmentOf(stmt);
|
|
if (!mine) return null;
|
|
const stmts = sf.statements.nodes;
|
|
let lastTable = -1;
|
|
for (let i = 0; i < stmts.length; i++) {
|
|
if (cjsExportAssignmentOf(stmts[i]!)?.kind === "table") lastTable = i;
|
|
}
|
|
if (lastTable < 0) return null;
|
|
const myIdx = stmts.indexOf(stmt as never);
|
|
if (mine.kind === "table") {
|
|
return myIdx < lastTable
|
|
? "multiple 'module.exports =' assignments (Node keeps only the last one — this table is discarded)"
|
|
: null;
|
|
}
|
|
if (myIdx < lastTable) {
|
|
return "export members attached before a 'module.exports =' assignment (Node replaces the export object — importers never see this member; attach it to module.exports AFTER the table)";
|
|
}
|
|
const left = mine.expr.left as ts.PropertyAccessExpression;
|
|
const last = cjsExportAssignmentOf(stmts[lastTable]!);
|
|
if (last?.kind === "table" && last.rebindsExports === true) return null;
|
|
return ts.isIdentifier(left.expression) && left.expression.text === "exports"
|
|
? "'exports.f =' after a 'module.exports =' assignment ('exports' still references the replaced object — Node discards this member; write module.exports.f instead)"
|
|
: null;
|
|
}
|
|
|
|
/** The object literal a `module.exports = <expr>` statement's RHS
|
|
* RESOLVES to (syntactically): the literal itself; `new Proxy(target,
|
|
* handler)` — Node programs export identity-forwarding proxies over a
|
|
* table, and the TARGET is the table (traps cannot run in a compiled
|
|
* program: every member access is statically resolved against the
|
|
* target's own type); or an identifier naming a top-level const whose
|
|
* initializer is the literal. Null for everything else. */
|
|
export function cjsExportTargetLiteral(
|
|
e: ts.Expression,
|
|
sf: ts.SourceFile,
|
|
): ts.ObjectLiteralExpression | null {
|
|
while (ts.isParenthesizedExpression(e)) e = e.expression;
|
|
if (ts.isObjectLiteralExpression(e)) return e;
|
|
if (
|
|
ts.isNewExpression(e) &&
|
|
ts.isIdentifier(e.expression) &&
|
|
e.expression.text === "Proxy" &&
|
|
(e.arguments?.nodes.length ?? 0) >= 1
|
|
) {
|
|
return cjsExportTargetLiteral(e.arguments!.nodes[0]!, sf);
|
|
}
|
|
if (ts.isIdentifier(e)) {
|
|
for (const stmt of sf.statements.nodes) {
|
|
if (!ts.isVariableStatement(stmt)) continue;
|
|
if ((stmt.declarationList.flags & ts.NodeFlags.Const) === 0) continue;
|
|
for (const d of stmt.declarationList.declarations.nodes) {
|
|
if (
|
|
d.name !== undefined &&
|
|
ts.isIdentifier(d.name) &&
|
|
d.name.text === e.text &&
|
|
d.initializer
|
|
) {
|
|
let init: ts.Expression = d.initializer;
|
|
while (ts.isParenthesizedExpression(init)) init = init.expression;
|
|
return ts.isObjectLiteralExpression(init) ? init : null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** The class expression a JS module's WHOLE export is (`module.exports =
|
|
* class …{}`): the kept (last) top-level `module.exports =` statement's
|
|
* paren-unwrapped RHS when it is a class expression, together with the
|
|
* statement it sits in (in-file `module.exports` reads gate on source
|
|
* order against it). Null when the kept export is anything else. */
|
|
export function cjsClassExprWholeExportOf(
|
|
sf: ts.SourceFile,
|
|
): { classExpr: ts.ClassExpression; stmt: ts.Statement } | null {
|
|
let last: { classExpr: ts.ClassExpression; stmt: ts.Statement } | null = null;
|
|
for (const stmt of sf.statements.nodes) {
|
|
const cjs = cjsExportAssignmentOf(stmt);
|
|
if (!cjs || cjs.kind !== "table") continue;
|
|
let rhs: ts.Expression = cjs.expr.right;
|
|
while (ts.isParenthesizedExpression(rhs)) rhs = rhs.expression;
|
|
// Any LATER table replaces the export object — a non-class one takes
|
|
// the class answer back off the table.
|
|
last = ts.isClassExpression(rhs) ? { classExpr: rhs, stmt } : null;
|
|
}
|
|
return last;
|
|
}
|
|
|
|
/** True when `obj` is the effective export-table literal of a JS module:
|
|
* the object literal of a top-level `module.exports = { ... }` statement
|
|
* directly, OR the RESOLVED TARGET of the module's kept `module.exports =`
|
|
* statement (the identifier/Proxy forms — see cjsExportTargetLiteral).
|
|
* The gate for the CJS export-property alias re-resolution
|
|
* (resolveValueSymbol) and the accessor-read lift. */
|
|
export function isCjsExportTableLiteral(obj: ts.ObjectLiteralExpression): boolean {
|
|
const assign = obj.parent;
|
|
if (
|
|
ts.isBinaryExpression(assign) &&
|
|
assign.right === obj &&
|
|
isModuleExportsAccess(assign.left) &&
|
|
ts.isExpressionStatement(assign.parent) &&
|
|
ts.isSourceFile(assign.parent.parent)
|
|
) {
|
|
return isJsSourceFile(assign.parent.parent);
|
|
}
|
|
const decl = obj.parent;
|
|
if (!ts.isVariableDeclaration(decl) || decl.initializer !== obj) return false;
|
|
const sf = ts.getSourceFileOfNode(obj);
|
|
if (!isJsSourceFile(sf)) return false;
|
|
for (const stmt of sf.statements.nodes) {
|
|
const cjs = cjsExportAssignmentOf(stmt);
|
|
if (cjs?.kind === "table" && cjsExportDiscardReason(stmt) === null) {
|
|
return cjsExportTargetLiteral(cjs.expr.right, sf) === obj;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** Names other runtimes and the DOM declare that scriptc's type world
|
|
* (the ES2025 library plus Node types) does not. */
|
|
const FOREIGN_RUNTIME_GLOBALS = new Set(["Deno", "Bun"]);
|
|
const DOM_GLOBALS = new Set([
|
|
"window",
|
|
"document",
|
|
"navigator",
|
|
"location",
|
|
"localStorage",
|
|
"sessionStorage",
|
|
"HTMLElement",
|
|
"Element",
|
|
"Document",
|
|
"Window",
|
|
"BodyInit",
|
|
"HeadersInit",
|
|
"RequestInfo",
|
|
"XMLHttpRequest",
|
|
"requestAnimationFrame",
|
|
]);
|
|
/** Root identifiers whose members the bundled Node fallback declares. */
|
|
const NODE_GLOBAL_ROOTS = new Set(["process", "Buffer", "console", "globalThis"]);
|
|
|
|
/** A hint when a TypeScript error comes from scriptc's own type world rather
|
|
* than from the project: a library member newer than ES2025, a global of
|
|
* another runtime or of the DOM, a Node member the bundled fallback types
|
|
* leave out, or the strictNullChecks scriptc enforces. Null otherwise. */
|
|
function typeWorldHint(d: ts.Diagnostic, message: string, load: LoadResult): string | null {
|
|
const world = load.typeWorld;
|
|
if (world === undefined) return null;
|
|
const quoted = /'([^']+)'/.exec(message)?.[1];
|
|
// TS2304/TS2552: Cannot find name; TS2580/TS2591/TS2592: missing type
|
|
// packages; TS2583: needs a newer library.
|
|
if (d.code === 2304 || d.code === 2552 || d.code === 2583) {
|
|
if (quoted !== undefined && FOREIGN_RUNTIME_GLOBALS.has(quoted)) {
|
|
return `'${quoted}' is another runtime's global; scriptc's type world declares the ES2025 library and Node only — feature-test it through globalThis instead, e.g. '(globalThis as { ${quoted}?: unknown }).${quoted}' (the native program runs as Node would, where it is undefined)`;
|
|
}
|
|
if (quoted !== undefined && DOM_GLOBALS.has(quoted)) {
|
|
return `'${quoted}' comes from the DOM library, which scriptc's type world does not include (it checks with the ES2025 library and Node types regardless of the project's "lib") — declare the shape you need locally, or move browser-only code out of the program`;
|
|
}
|
|
if (d.code === 2583) {
|
|
return `scriptc checks with the ES2025 library regardless of the project's "lib" setting, and '${quoted ?? "this name"}' comes from a newer library that does not compile yet`;
|
|
}
|
|
}
|
|
// TS2339/TS2550/TS2551: property does not exist (2550: on this lib).
|
|
if (d.code === 2339 || d.code === 2550 || d.code === 2551) {
|
|
const typeText = /on type '([^']+)'/.exec(message)?.[1] ?? "";
|
|
const newerLib =
|
|
world.projectLib !== null &&
|
|
world.projectLib.some((lib) => /esnext|es202[6-9]|es20[3-9]\d/i.test(lib));
|
|
if (d.code === 2550 || (newerLib && /^[A-Z][A-Za-z0-9]*(?:Constructor)?$/.test(typeText))) {
|
|
return `scriptc checks with the ES2025 library regardless of the project's "lib" setting, and '${quoted ?? "this member"}' comes from a newer library that does not compile yet — use an ES2025 equivalent here`;
|
|
}
|
|
if (world.nodeFallback && d.fileName !== undefined) {
|
|
const root = receiverRoot(load.program.getSourceFile(d.fileName)?.text ?? "", d.pos);
|
|
if (
|
|
root !== null &&
|
|
(NODE_GLOBAL_ROOTS.has(root) || importsNodeBuiltin(load, d.fileName, root))
|
|
) {
|
|
return `the project has no @types/node, so scriptc types Node with its bundled declarations, which list only the members that compile natively — '${quoted ?? "this member"}' does not compile natively yet (https://scriptc.dev/limitations); installing @types/node turns this into a located refusal with a hint`;
|
|
}
|
|
}
|
|
}
|
|
if (world.forcedStrictNullChecks && /\b(?:null|undefined)\b/.test(message)) {
|
|
return `the project disables strictNullChecks, which scriptc always enables — this error appears only under strict null checks; narrow the value (or enable "strictNullChecks" in the project to see the same errors from its own tsc)`;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** The root identifier of the property access whose member name starts at
|
|
* `pos` (`process.version` → `process`), or null. */
|
|
function receiverRoot(text: string, pos: number): string | null {
|
|
let i = pos - 1;
|
|
while (i >= 0 && /\s/.test(text[i]!)) i--;
|
|
if (text[i] !== ".") return null;
|
|
i--;
|
|
if (text[i] === "?") i--;
|
|
let root: string | null = null;
|
|
for (;;) {
|
|
while (i >= 0 && /\s/.test(text[i]!)) i--;
|
|
let j = i;
|
|
while (j >= 0 && /[\w$]/.test(text[j]!)) j--;
|
|
if (j === i) return root;
|
|
root = text.slice(j + 1, i + 1);
|
|
i = j;
|
|
while (i >= 0 && /\s/.test(text[i]!)) i--;
|
|
if (text[i] !== ".") return root;
|
|
i--;
|
|
if (text[i] === "?") i--;
|
|
}
|
|
}
|
|
|
|
/** Whether `name` is bound in `fileName` by an import of a Node builtin. */
|
|
function importsNodeBuiltin(load: LoadResult, fileName: string, name: string): boolean {
|
|
const sf = load.program.getSourceFile(fileName);
|
|
for (const stmt of sf?.statements?.nodes ?? []) {
|
|
if (!ts.isImportDeclaration(stmt) || !ts.isStringLiteral(stmt.moduleSpecifier)) continue;
|
|
if (canonicalBuiltinModule(stmt.moduleSpecifier.text) === null) continue;
|
|
const clause = stmt.importClause;
|
|
if (clause?.name?.text === name) return true;
|
|
const bindings = clause?.namedBindings;
|
|
if (bindings && ts.isNamespaceImport(bindings) && bindings.name.text === name) return true;
|
|
if (
|
|
bindings &&
|
|
ts.isNamedImports(bindings) &&
|
|
bindings.elements.nodes.some((e) => e.name.text === name)
|
|
)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|