2026-09-29 19:07:05 +08:00
|
|
|
import * as t from "@babel/types";
|
2026-10-09 00:20:48 +08:00
|
|
|
import type { JavaScriptSemanticPrimitive } from "./javascriptSemanticValueTypes.js";
|
|
|
|
|
|
2026-10-10 01:39:28 +08:00
|
|
|
/**
|
|
|
|
|
* Remove wrappers that affect TypeScript or Flow checking but not runtime
|
|
|
|
|
* expression identity. The returned depth lets bounded evaluators account for
|
|
|
|
|
* syntax that normalization would otherwise skip.
|
|
|
|
|
*/
|
|
|
|
|
export const unwrapJavaScriptExpression = (
|
|
|
|
|
node: t.Node,
|
|
|
|
|
): { readonly node: t.Node; readonly depth: number } => {
|
|
|
|
|
let current = node;
|
|
|
|
|
let depth = 0;
|
|
|
|
|
while (
|
|
|
|
|
t.isParenthesizedExpression(current) ||
|
|
|
|
|
t.isTSAsExpression(current) ||
|
|
|
|
|
t.isTSTypeAssertion(current) ||
|
|
|
|
|
t.isTSNonNullExpression(current) ||
|
|
|
|
|
t.isTSSatisfiesExpression(current) ||
|
|
|
|
|
t.isTSInstantiationExpression(current) ||
|
|
|
|
|
t.isTypeCastExpression(current)
|
|
|
|
|
) {
|
|
|
|
|
current = current.expression;
|
|
|
|
|
depth += 1;
|
|
|
|
|
}
|
|
|
|
|
return { node: current, depth };
|
|
|
|
|
};
|
|
|
|
|
|
2026-10-09 00:20:48 +08:00
|
|
|
/** Read one syntax-level primitive only when its value is exact. */
|
|
|
|
|
export const readExactJavaScriptLiteral = (
|
|
|
|
|
node: t.Node,
|
|
|
|
|
):
|
|
|
|
|
| { readonly found: true; readonly value: JavaScriptSemanticPrimitive }
|
|
|
|
|
| { readonly found: false } => {
|
2026-10-10 01:39:28 +08:00
|
|
|
node = unwrapJavaScriptExpression(node).node;
|
2026-10-09 00:20:48 +08:00
|
|
|
if (t.isStringLiteral(node)) return { found: true, value: node.value };
|
|
|
|
|
if (t.isNumericLiteral(node) && Number.isFinite(node.value))
|
|
|
|
|
return { found: true, value: node.value };
|
|
|
|
|
if (t.isBooleanLiteral(node)) return { found: true, value: node.value };
|
|
|
|
|
if (t.isNullLiteral(node)) return { found: true, value: null };
|
|
|
|
|
if (
|
|
|
|
|
t.isTemplateLiteral(node) &&
|
|
|
|
|
node.expressions.length === 0 &&
|
|
|
|
|
node.quasis.length === 1
|
|
|
|
|
) {
|
|
|
|
|
const cooked = node.quasis[0]?.value.cooked;
|
|
|
|
|
return cooked === null || cooked === undefined
|
|
|
|
|
? { found: false }
|
|
|
|
|
: { found: true, value: cooked };
|
|
|
|
|
}
|
|
|
|
|
return { found: false };
|
|
|
|
|
};
|
2026-09-29 19:07:05 +08:00
|
|
|
|
|
|
|
|
/** Read an identifier or literal property name without evaluating syntax. */
|
|
|
|
|
export const propertyName = (node: t.Node): string => {
|
|
|
|
|
if (t.isIdentifier(node)) return node.name;
|
|
|
|
|
if (t.isStringLiteral(node) || t.isNumericLiteral(node))
|
|
|
|
|
return String(node.value);
|
|
|
|
|
return "";
|
|
|
|
|
};
|
2026-10-06 00:50:38 +08:00
|
|
|
|
|
|
|
|
/** Read a property name only when its syntax commits to one exact key. */
|
|
|
|
|
export const semanticStaticPropertyName = (
|
|
|
|
|
property: t.Node,
|
|
|
|
|
computed: boolean,
|
2026-10-06 21:23:43 -05:00
|
|
|
): string => semanticStaticPropertyKey(property, computed) ?? "";
|
|
|
|
|
|
|
|
|
|
/** Read an exact property key, preserving the legal empty-string key. */
|
|
|
|
|
export const semanticStaticPropertyKey = (
|
|
|
|
|
property: t.Node,
|
|
|
|
|
computed: boolean,
|
|
|
|
|
): string | null => {
|
2026-10-10 01:39:28 +08:00
|
|
|
property = unwrapJavaScriptExpression(property).node;
|
2026-10-06 21:23:43 -05:00
|
|
|
if (t.isStringLiteral(property) || t.isNumericLiteral(property))
|
|
|
|
|
return String(property.value);
|
|
|
|
|
return !computed && t.isIdentifier(property) ? property.name : null;
|
|
|
|
|
};
|
2026-10-06 20:43:08 -07:00
|
|
|
|
2026-10-10 13:47:15 +08:00
|
|
|
/** Read exact consumed object-pattern keys, leaving dynamic keys unknown. */
|
|
|
|
|
export const semanticObjectPatternKeys = (
|
|
|
|
|
pattern: t.ObjectPattern,
|
|
|
|
|
): readonly string[] =>
|
|
|
|
|
pattern.properties.flatMap((property) => {
|
|
|
|
|
if (t.isRestElement(property)) return [];
|
|
|
|
|
const key = semanticStaticPropertyKey(property.key, property.computed);
|
|
|
|
|
return key === null ? [] : [key];
|
|
|
|
|
});
|
|
|
|
|
|
2026-10-11 09:25:32 +08:00
|
|
|
/** Visit member write targets in direct and destructuring assignment forms. */
|
|
|
|
|
export function* semanticMemberWriteTargets(
|
|
|
|
|
root: t.Node,
|
|
|
|
|
): Generator<t.MemberExpression | t.OptionalMemberExpression> {
|
|
|
|
|
const pending = [root];
|
|
|
|
|
while (pending.length > 0) {
|
|
|
|
|
const next = pending.pop();
|
|
|
|
|
if (next === undefined) break;
|
|
|
|
|
const node = unwrapJavaScriptExpression(next).node;
|
|
|
|
|
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node))
|
|
|
|
|
yield node;
|
|
|
|
|
else if (t.isRestElement(node)) pending.push(node.argument);
|
|
|
|
|
else if (t.isAssignmentPattern(node)) pending.push(node.left);
|
|
|
|
|
else if (t.isArrayPattern(node)) {
|
|
|
|
|
for (const element of node.elements.toReversed())
|
|
|
|
|
if (element !== null) pending.push(element);
|
|
|
|
|
} else if (t.isObjectPattern(node)) {
|
|
|
|
|
for (const property of node.properties.toReversed())
|
|
|
|
|
pending.push(
|
|
|
|
|
t.isRestElement(property) ? property.argument : property.value,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-06 20:43:08 -07:00
|
|
|
/**
|
|
|
|
|
* Display an exact JavaScript string as nonempty label text. Graph labels and
|
|
|
|
|
* artifact-local keys are nonempty, so the legal empty value is shown as `""`;
|
|
|
|
|
* producers keep the exact value in their properties.
|
|
|
|
|
*/
|
|
|
|
|
export const javascriptDisplayText = (value: string): string =>
|
|
|
|
|
value === "" ? '""' : value;
|