canonicalizeSite
tcmts.core.canonicalize.canonicalizeSite · #kb5xxcbl · term · runs anywhere
import {
declKey, IDENT_NAME, isAssignmentTarget, isCalled, isDeferred, isJsxTag, isList, isNamingPosition,
isSkippedModifier, isStructuralName, isTaggedTemplatePart, isTypePosition, jsxText, kindName, meaningAt, REJECTED,
slots,
} from "#codebase/tcmts/core/canonicalize";
import type { CanonContext, RawSpan, Resolved, SiteResult } from "#codebase/tcmts/core/canonicalize";
import { isAnonymousFunction, isInstantiated, TcmError } from "#codebase/tcmts/core/extract";
import type { DefSite } from "#codebase/tcmts/core/extract";
import type { IR } from "#codebase/tcmts/core/hash";
import { getModifiers, isOptionalChain } from "#codebase/tcmts/core/ts";
import type { ExtRef, RefPos } from "#codebase/tcmts/core/types";
import {
isConstructorDeclaration as ts_isConstructorDeclaration, isHeritageClause as ts_isHeritageClause,
isIdentifier as ts_isIdentifier, isImportSpecifier as ts_isImportSpecifier, isJsxText as ts_isJsxText,
isMetaProperty as ts_isMetaProperty, isModuleBlock as ts_isModuleBlock,
isModuleDeclaration as ts_isModuleDeclaration, isNamespaceImport as ts_isNamespaceImport,
isNoSubstitutionTemplateLiteral as ts_isNoSubstitutionTemplateLiteral,
isObjectBindingPattern as ts_isObjectBindingPattern, isParameterDeclaration as ts_isParameter,
isPostfixUnaryExpression as ts_isPostfixUnaryExpression, isPrefixUnaryExpression as ts_isPrefixUnaryExpression,
isPropertyAccessExpression as ts_isPropertyAccessExpression, isQualifiedName as ts_isQualifiedName,
isSourceFile as ts_isSourceFile, isStringLiteral as ts_isStringLiteral, isTypeOperatorNode as ts_isTypeOperatorNode,
isVariableDeclarationList as ts_isVariableDeclarationList, NodeFlags as ts_NodeFlags, SyntaxKind as ts_SyntaxKind,
tokenToString as ts_tokenToString,
} from "typescript/ast";
import type {
BigIntLiteral as ts_BigIntLiteral, BindingElement as ts_BindingElement, CallExpression as ts_CallExpression,
ElementAccessExpression as ts_ElementAccessExpression, Identifier as ts_Identifier, ImportClause as ts_ImportClause,
ImportDeclaration as ts_ImportDeclaration, ImportSpecifier as ts_ImportSpecifier, JsxText as ts_JsxText,
ModuleDeclaration as ts_ModuleDeclaration, NamespaceImport as ts_NamespaceImport, Node as ts_Node,
NoSubstitutionTemplateLiteral as ts_NoSubstitutionTemplateLiteral, NumericLiteral as ts_NumericLiteral,
ParenthesizedExpression as ts_ParenthesizedExpression, ParenthesizedTypeNode as ts_ParenthesizedTypeNode,
PrivateIdentifier as ts_PrivateIdentifier, QualifiedName as ts_QualifiedName,
RegularExpressionLiteral as ts_RegularExpressionLiteral,
ShorthandPropertyAssignment as ts_ShorthandPropertyAssignment, StringLiteral as ts_StringLiteral,
TemplateLiteralLikeNode as ts_TemplateLiteralLikeNode,
} from "typescript/ast";
import { SymbolFlags as ts_SymbolFlags } from "typescript/sync";
import type { NodeHandle as ts_NodeHandle, Symbol as ts_Symbol } from "typescript/sync";
export function canonicalizeSite(site: DefSite, ctx: CanonContext): SiteResult {
const { checker } = ctx;
const sf = site.sf;
const locals = new Map<ts_Symbol, number>();
const spans: RawSpan[] = [];
const reserved = new Set<string>();
const edges = new Map<number, boolean>();
const writes = new Set<number>();
let transform = false;
let jsx: string | undefined;
const globals = new Set<string>();
/** a module augmentation's specifier, and the name it declares: the definition they augment (see `augmenting`) */
const augmentSpecs = new Map<ts_Node, Resolved>();
const augmentNames = new Map<ts_Node, Resolved>();
const ACCESSIBILITY = new Set([
ts_SyntaxKind.PublicKeyword,
ts_SyntaxKind.PrivateKeyword,
ts_SyntaxKind.ProtectedKeyword,
ts_SyntaxKind.ReadonlyKeyword,
ts_SyntaxKind.OverrideKeyword,
]);
/**
* If `left` is a namespace import of a project module, what `left.right` names: one of its definitions, or what
* it re-exports from a package.
*/
const namespaceMember = (left: ts_Identifier, right: ts_Identifier): Resolved | undefined => {
const sym = checker.getSymbolAtLocation(left);
if (!sym || !(sym.flags & ts_SymbolFlags.Alias)) return undefined;
const target = checker.getAliasedSymbol(sym);
const decl = target.declarations?.[0];
if (!decl || decl.kind !== ts_SyntaxKind.SourceFile || !ctx.projectFiles.has(decl.path))
return undefined;
let m = checker.getSymbolAtLocation(right);
if (m && m.flags & ts_SymbolFlags.Alias) m = checker.getAliasedSymbol(m);
const d = m?.declarations?.length ? pickDef(m.declarations, right) : undefined;
if (d !== undefined) return { kind: "def", def: d };
const ext = reexport(decl, right.text, right);
if (ext) return { kind: "ext", ext };
throw new TcmError(
`'${left.text}.${right.text}' is not a top-level definition of ${ctx.fileName(decl.path)}`,
right,
);
};
/** What project module `file` (a source file's declaration) re-exports from a package as `name`. */
const reexport = (file: ts_NodeHandle, name: string, at: ts_Node): ExtRef | undefined => {
const sf = ctx.nodeOf(file);
return sf && ts_isSourceFile(sf) ? ctx.classifyReexport(sf, name, at) : undefined;
};
/** A package's export, imported by `imp` through a project module that re-exports it. */
const importedReexport = (
imp: ts_ImportSpecifier | ts_ImportClause | ts_NamespaceImport,
at: ts_Node,
): ExtRef | undefined => {
if (ts_isNamespaceImport(imp)) return undefined;
const decl = (
ts_isImportSpecifier(imp) ? imp.parent.parent.parent : imp.parent
) as ts_ImportDeclaration;
const mod = checker.getSymbolAtLocation(decl.moduleSpecifier)?.declarations?.[0];
if (mod?.kind !== ts_SyntaxKind.SourceFile || !ctx.projectFiles.has(mod.path)) return undefined;
return reexport(
mod,
ts_isImportSpecifier(imp) ? (imp.propertyName ?? imp.name).text : "default",
at,
);
};
/**
* The package namespace `e` is, through a project module's `export * as ns from "pkg"`: `ns` imported from the
* module, or `m.ns` through `import * as m` of it (in a type, too).
*/
const reexportedNamespace = (e: ts_Node): ExtRef | undefined => {
let ext: ExtRef | undefined;
if (ts_isIdentifier(e)) {
const d0 = checker.getSymbolAtLocation(e)?.declarations?.[0];
const imp = d0?.kind === ts_SyntaxKind.ImportSpecifier ? ctx.nodeOf(d0) : undefined;
if (imp && ts_isImportSpecifier(imp)) ext = importedReexport(imp, e);
} else {
const [l, r] = ts_isPropertyAccessExpression(e)
? [e.expression, e.name]
: e.kind === ts_SyntaxKind.QualifiedName
? [(e as ts_QualifiedName).left, (e as ts_QualifiedName).right]
: [];
const m =
l && r && ts_isIdentifier(l) && ts_isIdentifier(r) ? namespaceMember(l, r) : undefined;
if (m?.kind === "ext") ext = m.ext;
}
return ext?.export === "*" ? ext : undefined;
};
/** The definition a symbol denotes here. A type and a value sharing a name are one merged symbol. */
const pickDef = (decls: readonly ts_NodeHandle[], at: ts_Node): number | undefined => {
const defs = [
...new Set(decls.map((d) => ctx.declToDef.get(declKey(d))).filter((d) => d !== undefined)),
];
if (defs.length <= 1) return defs[0];
const wantType = meaningAt(at) === "type";
return defs.find((d) => (ctx.kindOf(d) === "type") === wantType) ?? defs[0];
};
/**
* Whether one of a symbol's declarations is in this definition: in a module augmentation, one merging with the
* augmented definition's (`interface Augment`) is declared first there, in its file or its generated module, and here
* too.
*/
const declaredHere = (decls: readonly ts_NodeHandle[]) =>
decls.some((d) => {
const node = d.path === sf.path ? ctx.nodeOf(d) : undefined;
return !!node && site.nodes.some((n) => node.pos >= n.pos && node.end <= n.end);
});
const classifySymbol = (sym: ts_Symbol | undefined, at: ts_Node, text: string): Resolved => {
if (!sym) return { kind: "raw", name: text };
const decls = sym.declarations ?? [];
const d0 = decls[0];
if (!d0) return { kind: "global", name: text };
if (
d0.kind === ts_SyntaxKind.ImportSpecifier ||
d0.kind === ts_SyntaxKind.ImportClause ||
d0.kind === ts_SyntaxKind.NamespaceImport
) {
// a name both imported and declared here (`import type { Stream }` beside `const Stream`) is what's declared
// here, for the meaning that has
const own = decls.filter(
(d) =>
d.kind !== ts_SyntaxKind.ImportSpecifier &&
d.kind !== ts_SyntaxKind.ImportClause &&
d.kind !== ts_SyntaxKind.NamespaceImport,
);
const ownDef = own.length ? pickDef(own, at) : undefined;
if (ownDef !== undefined) {
const k = ctx.kindOf(ownDef);
if (k === "class" || (k === "type") === (meaningAt(at) === "type"))
return { kind: "def", def: ownDef };
}
// imports between project files (directly or through re-exports) name a definition in the graph
const target = checker.getAliasedSymbol(sym);
const td = target.declarations?.[0];
if (td && ctx.projectFiles.has(td.path)) {
const def = pickDef(target.declarations!, at);
if (def !== undefined) return { kind: "def", def };
// a namespace import has no type: in a type, the name is the file's own type of that name (`interface Scope`
// beside `import type * as Scope`), or else the global one (`Iterable<T>` beside `import * as Iterable`)
if (
td.kind === ts_SyntaxKind.SourceFile &&
meaningAt(at) === "type" &&
!(at.parent && ts_isQualifiedName(at.parent) && at.parent.left === at)
) {
const own = decls.filter(
(d) =>
d.kind !== ts_SyntaxKind.ImportSpecifier &&
d.kind !== ts_SyntaxKind.ImportClause &&
d.kind !== ts_SyntaxKind.NamespaceImport,
);
if (!own.length) return { kind: "global", name: text };
const ownDef = pickDef(own, at);
if (ownDef !== undefined) return { kind: "def", def: ownDef };
}
throw new TcmError(
td.kind === ts_SyntaxKind.SourceFile
? `'${text}' is a namespace import of a project module; import the definitions you use by name`
: `'${text}' is not a top-level definition of ${ctx.fileName(td.path)}`,
at,
);
}
const imp = ctx.nodeOf(d0) as ts_ImportSpecifier | ts_ImportClause | ts_NamespaceImport;
const ext = importedReexport(imp, at);
if (ext) return { kind: "ext", ext };
return ctx.classifyImport(imp, meaningAt(at));
}
if (ctx.projectFiles.has(d0.path)) {
const def = pickDef(decls, at);
if (def !== undefined) return { kind: "def", def };
if (declaredHere(decls)) return { kind: "local", symbol: sym };
throw new TcmError(`cannot resolve '${text}'`, at);
}
if (/\.d\.[cm]?ts$/.test(d0.path)) return { kind: "global", name: text };
if (declaredHere(decls)) return { kind: "local", symbol: sym };
throw new TcmError(
`'${text}' refers to a declaration in ${d0.path}; import codebase definitions from '#codebase/...' instead of local files`,
at,
);
};
const refIR = (r: Resolved, at: ts_Node, text: string, shorthand?: string): IR => {
switch (r.kind) {
case "local": {
let n = locals.get(r.symbol);
if (n === undefined) {
n = locals.size;
locals.set(r.symbol, n);
}
reserved.add(text);
return ["L", String(n)];
}
case "global":
reserved.add(r.name);
globals.add(r.name);
return ["G", r.name];
case "raw":
reserved.add(r.name);
globals.add(r.name);
return ["U", r.name];
}
const pos: RefPos = site.kind === "type" || isTypePosition(at) ? "type" : "value";
const strict = pos === "value" && !isDeferred(at, site.nodes);
const base = {
start: at.getStart(sf),
end: at.end,
pos,
strict,
...(pos === "value" && isCalled(at) ? { called: true } : {}),
shorthand,
...(isJsxTag(at) ? { element: true } : {}),
...(augmentSpecs.has(at) ? { literal: true } : {}),
...(augmentNames.has(at) ? { augmented: true } : {}),
};
switch (r.kind) {
case "def":
spans.push({ ...base, target: { def: r.def } });
if (r.def !== site.id) edges.set(r.def, (edges.get(r.def) ?? false) || strict);
if (r.def !== site.id && ctx.isLet(r.def) && isAssignmentTarget(at)) writes.add(r.def);
return ["@def", String(r.def)];
case "hash":
spans.push({ ...base, target: { hash: r.hash } });
return ["Ref", r.hash];
case "ext":
spans.push({ ...base, target: { ext: r.ext } });
return ["Ext", r.ext.module, r.ext.subpath, r.ext.export];
}
};
const atRoot = (node: ts_Node) => site.nodes.includes(node);
const children = (node: ts_Node, out: IR[]) => {
for (const [slot, c] of slots(node)) {
if (!isList(c)) {
if (!(atRoot(node) && isSkippedModifier(c))) out.push([slot, visit(c)]);
continue;
}
// only the definition's own `export` is dropped: inside a namespace, `export` changes what the code means
// and indentation between JSX tags, which React drops: `<a><b /></a>` hashes like its multi-line layout
const kids = [...c].filter(
(k) =>
!(atRoot(node) && isSkippedModifier(k)) && !(ts_isJsxText(k) && jsxText(k.text) === ""),
);
// `export function f` and `function f` must hash the same: drop a modifier list emptied by stripping
if (kids.length === 0 && slot === "modifiers") continue;
out.push([slot, ["[", ...kids.map(visit)]]);
}
};
/**
* What a module augmentation (`declare module "./Schema" { namespace Annotations … }`) augments: the definition its
* one declaration names, by the checker (in imported files, which merge them) or by its codebase path (in a scratch
* file, `#codebase/effect/Schema`, whose re-export keeps it from merging). Its specifier and that name are references
* to it, so neither hashes as written.
*/
const augmenting = (md: ts_ModuleDeclaration) => {
const spec = (md.name as ts_StringLiteral).text;
const decls = md.body && ts_isModuleBlock(md.body) ? md.body.statements : [];
const named = decls[0] as (ts_Node & { name?: ts_Node }) | undefined;
if (decls.length !== 1 || !named?.name || !ts_isIdentifier(named.name))
throw new TcmError("a `declare module` augments one definition: write one for each", md);
const id = named.name;
// a generated module's (`declare module "./<module>.ts" { namespace h_<hash> … }`, as tcm renders it): the hash
const generated = /^\.\/[a-z2-7]+\.tsx?$/.test(spec) ? /^h_([a-z2-7]+)$/.exec(id.text) : null;
let r: Resolved = generated
? { kind: "hash", hash: generated[1]! }
: classifySymbol(checker.getSymbolAtLocation(id), id, id.text);
if (r.kind !== "def" && r.kind !== "hash" && spec.startsWith("#codebase")) {
const ns = spec.slice("#codebase".length).replace(/^\//, "").replaceAll("/", ".");
const hash = ctx.resolvePath?.(ns ? `${ns}.${id.text}` : id.text);
if (hash) r = { kind: "hash", hash };
}
if (r.kind !== "def" && r.kind !== "hash")
throw new TcmError(
`can't tell which definition \`declare module "${spec}"\` augments: '${id.text}' isn't one of that module's`,
id,
);
augmentNames.set(id, r);
augmentSpecs.set(md.name, r);
};
const visit = (node: ts_Node): IR => {
const k = node.kind;
if (
k === ts_SyntaxKind.ModuleDeclaration &&
site.tag === "Augment" &&
ts_isStringLiteral((node as ts_ModuleDeclaration).name)
)
augmenting(node as ts_ModuleDeclaration);
const transformable =
k === ts_SyntaxKind.EnumDeclaration || k === ts_SyntaxKind.ModuleDeclaration;
// a namespace holding only types is erasable, in a scratch file too
const rejected =
k === ts_SyntaxKind.ModuleDeclaration && !isInstantiated(node as ts_ModuleDeclaration)
? undefined
: REJECTED[k];
if (rejected && !(transformable && ctx.allowTransformed)) throw new TcmError(rejected, node);
if (transformable) transform = true;
if (
ts_isParameter(node) &&
ts_isConstructorDeclaration(node.parent) &&
(getModifiers(node) ?? []).some((m) => ACCESSIBILITY.has(m.kind))
)
transform = true;
switch (k) {
case ts_SyntaxKind.Identifier: {
const id = node as ts_Identifier;
const augmented = augmentNames.get(id);
if (augmented) return refIR(augmented, id, id.text);
if (isStructuralName(id)) return ["N", id.text];
// a namespace's own name is this definition's, though the symbol it shares with a value of that name (`function
// Struct` beside `interface Struct` and `namespace Struct`) reads as the value where a declaration names it
const own =
!!id.parent &&
ts_isModuleDeclaration(id.parent) &&
id.parent.name === id &&
site.declarations.includes(id.parent);
const ref = refIR(
own
? { kind: "def", def: site.id }
: classifySymbol(checker.getSymbolAtLocation(id), id, id.text),
id,
id.text,
);
return isNamingPosition(id) ? ["Named", id.text, ref] : ref;
}
case ts_SyntaxKind.JsxText:
return ["JsxText", jsxText((node as ts_JsxText).text)];
case ts_SyntaxKind.JsxElement:
case ts_SyntaxKind.JsxSelfClosingElement:
case ts_SyntaxKind.JsxFragment:
// elements compile to calls into a runtime package: pinned (and installed) like an import of it
if (jsx === undefined) {
const e = ctx.jsxRuntime(node);
jsx = e.module;
}
break;
case ts_SyntaxKind.PrivateIdentifier:
return ["PN", (node as ts_PrivateIdentifier).text];
case ts_SyntaxKind.StringLiteral: {
const augmented = augmentSpecs.get(node);
if (augmented)
return ["AugmentOf", refIR(augmented, node, (node as ts_StringLiteral).text)];
return ["Str", (node as ts_StringLiteral).text];
}
case ts_SyntaxKind.NoSubstitutionTemplateLiteral: {
const t = node as ts_NoSubstitutionTemplateLiteral;
return isTaggedTemplatePart(t) ? ["TStr", t.text, t.getText(sf)] : ["Str", t.text];
}
case ts_SyntaxKind.TemplateHead:
case ts_SyntaxKind.TemplateMiddle:
case ts_SyntaxKind.TemplateTail: {
const t = node as ts_TemplateLiteralLikeNode & ts_Node;
return isTaggedTemplatePart(t)
? [kindName[k]!, t.text, t.getText(sf)]
: [kindName[k]!, t.text];
}
case ts_SyntaxKind.NumericLiteral:
return ["Num", String(Number((node as ts_NumericLiteral).text.replaceAll("_", "")))];
case ts_SyntaxKind.BigIntLiteral:
return [
"BigInt",
BigInt((node as ts_BigIntLiteral).text.slice(0, -1).replaceAll("_", "")).toString(),
];
case ts_SyntaxKind.RegularExpressionLiteral:
return ["Re", (node as ts_RegularExpressionLiteral).text];
case ts_SyntaxKind.ParenthesizedExpression: {
// `(a?.b).c` differs from `a?.b.c`: parentheses around an optional chain count where the chain goes on after
// them (format 2; `tcmts.core.migrate.dropUncontinuedParens`); every other paren is just grouping already
// captured by the tree
const inner = (node as ts_ParenthesizedExpression).expression;
const parent = node.parent as
| (ts_Node & { expression?: ts_Node; tag?: ts_Node })
| undefined;
const continued =
!!parent &&
(parent.expression === node || parent.tag === node) &&
[
ts_SyntaxKind.PropertyAccessExpression,
ts_SyntaxKind.ElementAccessExpression,
ts_SyntaxKind.CallExpression,
ts_SyntaxKind.TaggedTemplateExpression,
ts_SyntaxKind.NonNullExpression,
ts_SyntaxKind.NewExpression,
].includes(parent.kind);
if (!isOptionalChain(inner) || !continued) return visit(inner);
break;
}
case ts_SyntaxKind.ParenthesizedType:
return visit((node as ts_ParenthesizedTypeNode).type);
case ts_SyntaxKind.ShorthandPropertyAssignment: {
// `{ x }` hashes exactly like `{ x: x }` (and `{ x = d }` like `{ x: x = d }`): renaming what `x` refers to
// expands the shorthand, and the expanded text must hash the same
const s = node as ts_ShorthandPropertyAssignment;
// a shorthand's name is always an identifier
const name = s.name as ts_Identifier;
const sym = checker.getShorthandAssignmentValueSymbol(s);
const ref = refIR(classifySymbol(sym, name, name.text), name, name.text, name.text);
const init = s.objectAssignmentInitializer;
const value: IR = init
? [
"BinaryExpression",
["left", isAnonymousFunction(init) ? ["Named", name.text, ref] : ref],
["operatorToken", ["EqualsToken"]],
["right", visit(init)],
]
: ref;
return ["PropertyAssignment", ["name", ["N", name.text]], ["initializer", value]];
}
case ts_SyntaxKind.BindingElement: {
const b = node as ts_BindingElement;
// `const { a } = o` binds a local *and* reads property `a`: keep the property name structural
if (
ts_isObjectBindingPattern(b.parent) &&
!b.propertyName &&
!b.dotDotDotToken &&
b.name &&
ts_isIdentifier(b.name)
) {
// `{ a }` hashes exactly like `{ a: a }`; a top-level binding can be renamed, and the span remembers the
// property so the splice writes `{ a: renamed }`
const ref = refIR(
classifySymbol(checker.getSymbolAtLocation(b.name), b.name, b.name.text),
b.name,
b.name.text,
b.name.text,
);
const out: IR[] = [
"BindingElement",
["propertyName", ["N", b.name.text]],
["name", isNamingPosition(b.name) ? ["Named", b.name.text, ref] : ref],
];
if (b.initializer) out.push(["initializer", visit(b.initializer)]);
return out;
}
break;
}
case ts_SyntaxKind.ElementAccessExpression: {
const e = node as ts_ElementAccessExpression;
const arg = e.argumentExpression;
if (
(ts_isStringLiteral(arg) || ts_isNoSubstitutionTemplateLiteral(arg)) &&
IDENT_NAME.test(arg.text)
) {
// same IR as the PropertyAccessExpression `a.b` would produce
const out: IR[] = ["PropertyAccessExpression", ["expression", visit(e.expression)]];
if (e.questionDotToken) out.push(["questionDotToken", ["QuestionDotToken"]]);
out.push(["name", ["N", arg.text]]);
return out;
}
break;
}
case ts_SyntaxKind.PropertyAccessExpression:
case ts_SyntaxKind.QualifiedName: {
// `ns.member` through `import * as ns from "./mod"` (or a re-exported `export * as ns`) is a reference to
// that member: it hashes, and is spliced, exactly as if `member` had been imported by name
const left = ts_isPropertyAccessExpression(node)
? node.expression
: (node as ts_QualifiedName).left;
const right = ts_isPropertyAccessExpression(node)
? node.name
: (node as ts_QualifiedName).right;
const member =
ts_isIdentifier(left) && ts_isIdentifier(right)
? namespaceMember(left, right)
: undefined;
if (member) return refIR(member, node, right.text);
// `ns.x` through a re-exported `export * as ns from "pkg"` is a reference to `pkg`'s `x`
const ns = ts_isIdentifier(right) ? reexportedNamespace(left) : undefined;
if (ns) return refIR({ kind: "ext", ext: { ...ns, export: right.text } }, node, right.text);
break;
}
case ts_SyntaxKind.CallExpression: {
const call = node as ts_CallExpression;
if (call.expression.kind === ts_SyntaxKind.ImportKeyword) {
const arg = call.arguments[0];
if (
call.arguments.length !== 1 ||
!arg ||
!(ts_isStringLiteral(arg) || ts_isNoSubstitutionTemplateLiteral(arg))
) {
// a computed specifier loads code the codebase doesn't track (like `import.meta`, it depends on where
// the module runs): allowed, hashed as written, and flagged
ctx.warn(
"`import()` with a computed specifier loads code the codebase doesn't track",
node,
);
break;
}
// pinned like a static import; the generated code imports the pinned version
const ext = ctx.classifyDynamicImport(arg.text, arg);
spans.push({
start: arg.getStart(sf),
end: arg.end,
pos: "value",
strict: false,
target: { ext },
literal: true,
});
return ["DynImport", ext.module, ext.subpath];
}
break;
}
}
const out: IR[] = [kindName[k] ?? `Kind${k}`];
if (ts_isPrefixUnaryExpression(node) || ts_isPostfixUnaryExpression(node))
out.push(ts_tokenToString(node.operator)!);
else if (ts_isTypeOperatorNode(node)) out.push(ts_tokenToString(node.operator)!);
else if (ts_isHeritageClause(node)) out.push(ts_tokenToString(node.token)!);
else if (ts_isMetaProperty(node)) {
out.push(ts_tokenToString(node.keywordToken)!);
if (node.keywordToken === ts_SyntaxKind.ImportKeyword) {
ctx.warn(
`'${node.getText(sf)}' depends on where the module lives; in the codebase it is evaluated inside .tcm/cache`,
node,
);
}
} else if (ts_isVariableDeclarationList(node)) {
const scope = node.flags & ts_NodeFlags.BlockScoped;
out.push(
scope === ts_NodeFlags.AwaitUsing
? "await using"
: scope === ts_NodeFlags.Using
? "using"
: scope === ts_NodeFlags.Const
? "const"
: scope === ts_NodeFlags.Let
? "let"
: "var",
);
}
children(node, out);
return out;
};
// the salt gives separately declared definitions separate identities, even when their code is identical
const head: IR[] =
site.salt === undefined ? ["Def", site.tag] : ["Def", site.tag, ["Salt", site.salt]];
const body = site.nodes.map(visit);
const ir: IR = [...head, ...(jsx ? [["Jsx", jsx] as IR] : []), ...body];
if (site.tag === "Enum" || site.tag === "Namespace") transform = true;
return { ir, spans, reserved, edges, transform, globals, writes, ...(jsx ? { jsx } : {}) };
}
uses
used by