import * as t from "@babel/types"; import type { JavaScriptSemanticPrimitive } from "./javascriptSemanticValueTypes.js"; /** TypeScript receiver annotations occupy no positional runtime parameter slot. */ export const runtimeFunctionParameters = ( node: t.Function, ): t.Function["params"] => t.isIdentifier(node.params[0], { name: "this" }) ? node.params.slice(1) : node.params; /** * 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 }; }; /** 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 } => { node = unwrapJavaScriptExpression(node).node; 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 }; }; /** 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 ""; }; /** Read a property name only when its syntax commits to one exact key. */ export const semanticStaticPropertyName = ( property: t.Node, computed: boolean, ): 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 => { property = unwrapJavaScriptExpression(property).node; if (t.isStringLiteral(property) || t.isNumericLiteral(property)) return String(property.value); return !computed && t.isIdentifier(property) ? property.name : null; }; /** 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]; }); /** Return the syntactic node whose target this construct writes. */ export const semanticWriteSiteTarget = (node: t.Node): t.Node | null => { if (t.isAssignmentExpression(node)) return node.left; if (t.isUpdateExpression(node)) return node.argument; if (t.isUnaryExpression(node, { operator: "delete" })) return node.argument; if (t.isForOfStatement(node) || t.isForInStatement(node)) return node.left; return null; }; /** Whether a write site reads the previous target value before writing. */ export const semanticWriteSiteReadsFirst = (site: t.Node): boolean => (t.isAssignmentExpression(site) && site.operator !== "=") || t.isUpdateExpression(site); /** Find the owning write site through inert TypeScript and parenthesis wrappers. */ export const semanticWrappedWriteSite = ( node: t.Node, parent: t.Node, ancestors: readonly t.Node[], ): t.Node | null => { const chain = ancestors.at(-1) === parent ? ancestors : [...ancestors, parent]; for (let index = chain.length - 1; index >= 0; index -= 1) { const candidate = chain[index]; if (candidate === undefined) return null; const target = semanticWriteSiteTarget(candidate); if (target !== null) return unwrapJavaScriptExpression(target).node === node ? candidate : null; if (unwrapJavaScriptExpression(candidate).depth === 0) return null; } return null; }; /** Visit binding and member leaves in direct and destructuring write targets. */ export function* semanticWriteTargets( root: t.Node, ): Generator { const pending = [root]; while (pending.length > 0) { const next = pending.pop(); if (next === undefined) break; const node = unwrapJavaScriptExpression(next).node; if ( t.isIdentifier(node) || 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, ); } } } /** * 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;