mirror of
https://github.com/stablyai/orca.git
synced 2026-10-21 00:03:46 +00:00
639 lines
20 KiB
JavaScript
639 lines
20 KiB
JavaScript
// TypeScript 7 is a native CLI; AST consumers still need the legacy JavaScript API.
|
|
import ts from 'typescript-api'
|
|
|
|
import { destructuredLocalizationTargets } from './mobile-localization-destructuring-targets.mjs'
|
|
|
|
const ASSIGNMENT_KINDS = new Set([
|
|
ts.SyntaxKind.EqualsToken,
|
|
ts.SyntaxKind.AmpersandAmpersandEqualsToken,
|
|
ts.SyntaxKind.BarBarEqualsToken,
|
|
ts.SyntaxKind.QuestionQuestionEqualsToken
|
|
])
|
|
const LOGICAL_ASSIGNMENT_KINDS = new Set([
|
|
ts.SyntaxKind.AmpersandAmpersandEqualsToken,
|
|
ts.SyntaxKind.BarBarEqualsToken,
|
|
ts.SyntaxKind.QuestionQuestionEqualsToken
|
|
])
|
|
|
|
function unwrapExpression(node) {
|
|
if (
|
|
ts.isParenthesizedExpression(node) ||
|
|
ts.isAsExpression(node) ||
|
|
ts.isSatisfiesExpression(node) ||
|
|
ts.isNonNullExpression(node)
|
|
) {
|
|
return unwrapExpression(node.expression)
|
|
}
|
|
return node
|
|
}
|
|
|
|
function propertyNameText(name) {
|
|
if (ts.isIdentifier(name) || ts.isStringLiteralLike(name) || ts.isNumericLiteral(name)) {
|
|
return name.text
|
|
}
|
|
if (ts.isComputedPropertyName(name) && ts.isStringLiteralLike(name.expression)) {
|
|
return name.expression.text
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
function memberParts(node) {
|
|
const expression = unwrapExpression(node)
|
|
if (ts.isPropertyAccessExpression(expression)) {
|
|
return { object: expression.expression, propertyName: expression.name.text }
|
|
}
|
|
if (
|
|
ts.isElementAccessExpression(expression) &&
|
|
expression.argumentExpression &&
|
|
ts.isStringLiteralLike(expression.argumentExpression)
|
|
) {
|
|
return { object: expression.expression, propertyName: expression.argumentExpression.text }
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
function statementContainer(node) {
|
|
let current = node
|
|
while (current.parent) {
|
|
if (
|
|
ts.isSourceFile(current.parent) ||
|
|
ts.isBlock(current.parent) ||
|
|
ts.isModuleBlock(current.parent) ||
|
|
ts.isCaseBlock(current.parent) ||
|
|
ts.isClassStaticBlockDeclaration(current.parent)
|
|
) {
|
|
return { container: current.parent, statement: current }
|
|
}
|
|
current = current.parent
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
function isConstBinding(binding) {
|
|
const declaration = ts.isBindingElement(binding) ? binding.parent.parent : binding
|
|
const declarationList = ts.isVariableDeclaration(declaration) ? declaration.parent : undefined
|
|
return Boolean(
|
|
declarationList &&
|
|
ts.isVariableDeclarationList(declarationList) &&
|
|
(declarationList.flags & ts.NodeFlags.Const) !== 0
|
|
)
|
|
}
|
|
|
|
function isOrderedBefore(write, use) {
|
|
const writeStatement = statementContainer(write.node)
|
|
const useStatement = statementContainer(use)
|
|
return Boolean(
|
|
writeStatement &&
|
|
useStatement &&
|
|
writeStatement.container === useStatement.container &&
|
|
writeStatement.statement.end <= useStatement.statement.pos
|
|
)
|
|
}
|
|
|
|
function isDefiniteBefore(write, use) {
|
|
return !write.conditional && isOrderedBefore(write, use)
|
|
}
|
|
|
|
function functionOwner(node) {
|
|
let current = node.parent
|
|
while (current && !ts.isSourceFile(current)) {
|
|
if (ts.isFunctionLike(current)) {
|
|
return current
|
|
}
|
|
current = current.parent
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
function loopOwner(node) {
|
|
let current = node.parent
|
|
while (current && !ts.isFunctionLike(current) && !ts.isSourceFile(current)) {
|
|
if (
|
|
ts.isForStatement(current) ||
|
|
ts.isForInStatement(current) ||
|
|
ts.isForOfStatement(current) ||
|
|
ts.isWhileStatement(current) ||
|
|
ts.isDoStatement(current)
|
|
) {
|
|
return current
|
|
}
|
|
current = current.parent
|
|
}
|
|
return undefined
|
|
}
|
|
|
|
function canReachUse(write, use, sourceFile, scopeOwners = new Set()) {
|
|
if (write.position < use.getStart(sourceFile)) {
|
|
return true
|
|
}
|
|
const useOwner = functionOwner(use)
|
|
if (!useOwner || functionOwner(write.node) !== useOwner) {
|
|
return Boolean(useOwner)
|
|
}
|
|
if (loopOwner(write.node) && loopOwner(write.node) === loopOwner(use)) {
|
|
return true
|
|
}
|
|
return [...scopeOwners].some((owner) => owner !== useOwner)
|
|
}
|
|
|
|
export function createMobileLocalizationValueFlow(sourceFile, bindings) {
|
|
const writes = new Map()
|
|
const propertyWrites = new Map()
|
|
const pendingPropertyWrites = []
|
|
|
|
function addWrite(identifier, expression, node, propertyPath, operator, defaultExpression) {
|
|
const binding = bindings.resolveBinding(identifier)
|
|
if (!binding) {
|
|
return
|
|
}
|
|
const entries = writes.get(binding) ?? []
|
|
entries.push({
|
|
conditional: LOGICAL_ASSIGNMENT_KINDS.has(operator),
|
|
expression,
|
|
node,
|
|
operator,
|
|
position: node.getStart(sourceFile),
|
|
propertyPath,
|
|
defaultExpression
|
|
})
|
|
writes.set(binding, entries)
|
|
}
|
|
|
|
function addTargetWrites(name, expression, node, operator) {
|
|
const target = unwrapExpression(name)
|
|
if (ts.isIdentifier(target)) {
|
|
addWrite(target, expression, node, undefined, operator)
|
|
return
|
|
}
|
|
for (const entry of destructuredLocalizationTargets(target)) {
|
|
addWrite(
|
|
entry.identifier,
|
|
expression,
|
|
node,
|
|
entry.propertyPath,
|
|
operator,
|
|
entry.defaultExpression
|
|
)
|
|
}
|
|
}
|
|
|
|
function memberWriteTarget(node) {
|
|
const member = memberParts(node)
|
|
return member ? { ...member, target: unwrapExpression(node) } : undefined
|
|
}
|
|
|
|
function visit(node) {
|
|
if (ts.isVariableDeclaration(node) && node.initializer) {
|
|
addTargetWrites(node.name, node.initializer, node)
|
|
} else if (ts.isBinaryExpression(node) && ASSIGNMENT_KINDS.has(node.operatorToken.kind)) {
|
|
const memberTarget = memberWriteTarget(node.left)
|
|
if (memberTarget) {
|
|
pendingPropertyWrites.push({
|
|
...memberTarget,
|
|
conditional: LOGICAL_ASSIGNMENT_KINDS.has(node.operatorToken.kind),
|
|
expression: node.right,
|
|
node,
|
|
operator: node.operatorToken.kind,
|
|
position: node.getStart(sourceFile)
|
|
})
|
|
} else {
|
|
addTargetWrites(node.left, node.right, node, node.operatorToken.kind)
|
|
}
|
|
}
|
|
ts.forEachChild(node, visit)
|
|
}
|
|
|
|
visit(sourceFile)
|
|
for (const entries of writes.values()) {
|
|
entries.sort((left, right) => left.position - right.position)
|
|
}
|
|
function staticValueState(node) {
|
|
const expression = unwrapExpression(node)
|
|
if (
|
|
expression.kind === ts.SyntaxKind.NullKeyword ||
|
|
ts.isVoidExpression(expression) ||
|
|
(ts.isIdentifier(expression) &&
|
|
expression.text === 'undefined' &&
|
|
!bindings.resolveBinding(expression))
|
|
) {
|
|
return { nullish: true, truthy: false }
|
|
}
|
|
if (expression.kind === ts.SyntaxKind.TrueKeyword) {
|
|
return { nullish: false, truthy: true }
|
|
}
|
|
if (expression.kind === ts.SyntaxKind.FalseKeyword) {
|
|
return { nullish: false, truthy: false }
|
|
}
|
|
if (ts.isStringLiteralLike(expression) || ts.isNoSubstitutionTemplateLiteral(expression)) {
|
|
return { nullish: false, truthy: expression.text.length > 0 }
|
|
}
|
|
if (ts.isNumericLiteral(expression)) {
|
|
return { nullish: false, truthy: Number(expression.text) !== 0 }
|
|
}
|
|
if (ts.isBigIntLiteral(expression)) {
|
|
return { nullish: false, truthy: expression.text !== '0n' }
|
|
}
|
|
if (
|
|
ts.isArrowFunction(expression) ||
|
|
ts.isFunctionExpression(expression) ||
|
|
ts.isClassExpression(expression) ||
|
|
ts.isObjectLiteralExpression(expression) ||
|
|
ts.isArrayLiteralExpression(expression) ||
|
|
ts.isNewExpression(expression)
|
|
) {
|
|
return { nullish: false, truthy: true }
|
|
}
|
|
if (ts.isIdentifier(expression)) {
|
|
const binding = bindings.resolveBinding(expression)
|
|
if (binding && bindings.rootDescriptors?.has(binding)) {
|
|
return { nullish: false, truthy: true }
|
|
}
|
|
}
|
|
return { nullish: undefined, truthy: undefined }
|
|
}
|
|
|
|
function logicalWriteAction(operator, expressions, knownEmpty = false) {
|
|
const values = expressions.length > 0 ? expressions : knownEmpty ? [undefined] : []
|
|
const actions = new Set()
|
|
for (const expression of values) {
|
|
const state =
|
|
expression === undefined ? { nullish: true, truthy: false } : staticValueState(expression)
|
|
const applies =
|
|
operator === ts.SyntaxKind.QuestionQuestionEqualsToken
|
|
? state.nullish
|
|
: operator === ts.SyntaxKind.BarBarEqualsToken
|
|
? state.truthy === undefined
|
|
? undefined
|
|
: !state.truthy
|
|
: state.truthy
|
|
actions.add(applies === true ? 'assign' : applies === false ? 'keep' : 'unknown')
|
|
}
|
|
return actions.size === 1 ? [...actions][0] : 'unknown'
|
|
}
|
|
|
|
function isUninitializedBinding(binding) {
|
|
return ts.isVariableDeclaration(binding) && binding.initializer === undefined
|
|
}
|
|
|
|
function selectedWrites(identifier, use, all) {
|
|
const binding = bindings.resolveBinding(identifier)
|
|
const entries = binding ? (writes.get(binding) ?? []) : []
|
|
if (all || (binding && isConstBinding(binding))) {
|
|
return entries
|
|
}
|
|
const available = entries.filter((entry) =>
|
|
canReachUse(entry, use, sourceFile, new Set([functionOwner(binding)]))
|
|
)
|
|
let selected = []
|
|
for (const entry of available) {
|
|
const action = entry.conditional
|
|
? logicalWriteAction(
|
|
entry.operator,
|
|
selected.flatMap((write) => (write.propertyPath ? [] : [write.expression])),
|
|
selected.length === 0 && isUninitializedBinding(binding)
|
|
)
|
|
: 'assign'
|
|
if (!isOrderedBefore(entry, use)) {
|
|
if (action !== 'keep') {
|
|
selected.push(entry)
|
|
}
|
|
continue
|
|
}
|
|
if (!entry.conditional) {
|
|
selected = [entry]
|
|
continue
|
|
}
|
|
if (action === 'assign') {
|
|
selected = [entry]
|
|
} else if (action === 'unknown') {
|
|
selected.push(entry)
|
|
}
|
|
}
|
|
return selected
|
|
}
|
|
|
|
function locationKey(location) {
|
|
return `${location.binding.pos}:${location.path.join('\0')}`
|
|
}
|
|
|
|
function objectLocations(node, use = node, seen = new Set(), all = false) {
|
|
const expression = unwrapExpression(node)
|
|
if (ts.isIdentifier(expression)) {
|
|
const binding = bindings.resolveBinding(expression)
|
|
if (!binding) {
|
|
return []
|
|
}
|
|
const direct = [{ binding, path: [] }]
|
|
if (seen.has(binding)) {
|
|
return direct
|
|
}
|
|
const nextSeen = new Set(seen).add(binding)
|
|
const sources = selectedWrites(expression, use, all)
|
|
const resolved = sources.flatMap((source) => {
|
|
const locations = objectLocations(source.expression, source.node, nextSeen, all)
|
|
return source.propertyPath
|
|
? locations.map((location) => ({
|
|
binding: location.binding,
|
|
path: [...location.path, ...source.propertyPath]
|
|
}))
|
|
: locations
|
|
})
|
|
const locations = resolved.length > 0 ? resolved : direct
|
|
return [...new Map(locations.map((location) => [locationKey(location), location])).values()]
|
|
}
|
|
if (ts.isObjectLiteralExpression(expression) || ts.isArrayLiteralExpression(expression)) {
|
|
return [{ binding: expression, path: [] }]
|
|
}
|
|
const member = memberParts(expression)
|
|
if (!member) {
|
|
return []
|
|
}
|
|
return objectLocations(member.object, use, seen, all).map((location) => ({
|
|
binding: location.binding,
|
|
path: [...location.path, member.propertyName]
|
|
}))
|
|
}
|
|
|
|
function indexedPropertyWrites(location) {
|
|
const byPath = propertyWrites.get(location.binding) ?? new Map()
|
|
propertyWrites.set(location.binding, byPath)
|
|
const key = location.path.join('\0')
|
|
const entries = byPath.get(key) ?? []
|
|
byPath.set(key, entries)
|
|
return entries
|
|
}
|
|
|
|
for (const entry of pendingPropertyWrites) {
|
|
const targetLocations = objectLocations(entry.object, entry.node).map((location) => ({
|
|
binding: location.binding,
|
|
path: [...location.path, entry.propertyName]
|
|
}))
|
|
const rightMember = memberParts(entry.expression)
|
|
const rightLocations = rightMember
|
|
? objectLocations(rightMember.object, entry.node).map((location) => ({
|
|
binding: location.binding,
|
|
path: [...location.path, rightMember.propertyName]
|
|
}))
|
|
: []
|
|
const rightKeys = new Set(rightLocations.map(locationKey))
|
|
const selfAssignment =
|
|
targetLocations.length === 1 &&
|
|
rightLocations.length === 1 &&
|
|
rightKeys.has(locationKey(targetLocations[0]))
|
|
const indexed = {
|
|
...entry,
|
|
scopeOwners: new Set(targetLocations.map((location) => functionOwner(location.binding))),
|
|
selfAssignment
|
|
}
|
|
for (const location of targetLocations) {
|
|
indexedPropertyWrites(location).push(indexed)
|
|
}
|
|
}
|
|
for (const byPath of propertyWrites.values()) {
|
|
for (const entries of byPath.values()) {
|
|
entries.sort((left, right) => left.position - right.position)
|
|
}
|
|
}
|
|
|
|
function propertyWriteEntries(node, propertyName, use, all) {
|
|
const entries = objectLocations(node, use, new Set(), all).flatMap((location) => {
|
|
const key = [...location.path, propertyName].join('\0')
|
|
return propertyWrites.get(location.binding)?.get(key) ?? []
|
|
})
|
|
return [
|
|
...new Set(
|
|
entries.filter(
|
|
(entry) =>
|
|
!entry.selfAssignment && (all || canReachUse(entry, use, sourceFile, entry.scopeOwners))
|
|
)
|
|
)
|
|
].sort((left, right) => left.position - right.position)
|
|
}
|
|
|
|
function applyPropertyWrites(resolved, known, node, propertyName, use, all) {
|
|
for (const entry of propertyWriteEntries(node, propertyName, use, all)) {
|
|
if (all) {
|
|
resolved.push(entry.expression)
|
|
continue
|
|
}
|
|
const action = entry.conditional
|
|
? logicalWriteAction(entry.operator, resolved, known && resolved.length === 0)
|
|
: 'assign'
|
|
if (!isOrderedBefore(entry, use)) {
|
|
if (action !== 'keep') {
|
|
resolved.push(entry.expression)
|
|
}
|
|
continue
|
|
}
|
|
if (action === 'assign') {
|
|
resolved.splice(0, resolved.length, entry.expression)
|
|
known = true
|
|
} else if (action === 'unknown') {
|
|
resolved.push(entry.expression)
|
|
}
|
|
}
|
|
return known
|
|
}
|
|
|
|
function propertyPathValues(node, propertyPath, use = node, seen = new Set(), all = false) {
|
|
let sources = [{ expression: node, use }]
|
|
let known = true
|
|
for (const propertyName of propertyPath) {
|
|
const next = []
|
|
for (const source of sources) {
|
|
const values = propertyValues(
|
|
source.expression,
|
|
propertyName,
|
|
source.use,
|
|
new Set(seen),
|
|
all
|
|
)
|
|
if (values === undefined) {
|
|
known = false
|
|
continue
|
|
}
|
|
next.push(...values.map((expression) => ({ expression, use: expression })))
|
|
}
|
|
sources = next
|
|
}
|
|
const values = sources.map((source) => source.expression)
|
|
return known || (all && values.length > 0) ? values : undefined
|
|
}
|
|
|
|
function propertyValues(node, propertyName, use = node, seen = new Set(), all = false) {
|
|
const expression = unwrapExpression(node)
|
|
if (ts.isIdentifier(expression)) {
|
|
const binding = bindings.resolveBinding(expression)
|
|
if (!binding || seen.has(binding)) {
|
|
return undefined
|
|
}
|
|
const nextSeen = new Set(seen).add(binding)
|
|
const sources = valueSources(expression, use, all)
|
|
const resolved = []
|
|
let known = true
|
|
for (const source of sources) {
|
|
const values = source.propertyPath
|
|
? propertyPathValues(source.expression, source.propertyPath, source.node, nextSeen, all)
|
|
: [source.expression]
|
|
if (values === undefined) {
|
|
known = false
|
|
continue
|
|
}
|
|
for (const value of values) {
|
|
const matches = propertyValues(value, propertyName, source.node, nextSeen, all)
|
|
if (matches === undefined) {
|
|
known = false
|
|
} else {
|
|
resolved.push(...matches)
|
|
}
|
|
}
|
|
}
|
|
known = applyPropertyWrites(resolved, known, expression, propertyName, use, all)
|
|
return known || (all && resolved.length > 0) ? resolved : undefined
|
|
}
|
|
const member = memberParts(expression)
|
|
if (member) {
|
|
const objectValues = propertyValues(
|
|
member.object,
|
|
member.propertyName,
|
|
use,
|
|
new Set(seen),
|
|
all
|
|
)
|
|
const resolved = []
|
|
let known = objectValues !== undefined
|
|
for (const value of objectValues ?? []) {
|
|
const matches = propertyValues(value, propertyName, value, new Set(seen), all)
|
|
if (matches === undefined) {
|
|
known = false
|
|
} else {
|
|
resolved.push(...matches)
|
|
}
|
|
}
|
|
known = applyPropertyWrites(resolved, known, expression, propertyName, use, all)
|
|
return known || (all && resolved.length > 0) ? resolved : undefined
|
|
}
|
|
if (ts.isArrayLiteralExpression(expression)) {
|
|
const index = Number(propertyName)
|
|
if (!Number.isInteger(index) || index < 0 || index >= expression.elements.length) {
|
|
return []
|
|
}
|
|
const element = expression.elements[index]
|
|
return ts.isOmittedExpression(element) || ts.isSpreadElement(element) ? [] : [element]
|
|
}
|
|
if (!ts.isObjectLiteralExpression(expression)) {
|
|
return undefined
|
|
}
|
|
for (const property of expression.properties.toReversed()) {
|
|
if (ts.isSpreadAssignment(property)) {
|
|
const spreadValues = propertyValues(
|
|
property.expression,
|
|
propertyName,
|
|
property.expression,
|
|
new Set(seen),
|
|
all
|
|
)
|
|
if (spreadValues === undefined || spreadValues.length > 0) {
|
|
return spreadValues
|
|
}
|
|
continue
|
|
}
|
|
const name = propertyNameText(property.name)
|
|
if (name === undefined) {
|
|
return undefined
|
|
}
|
|
if (name !== propertyName) {
|
|
continue
|
|
}
|
|
if (ts.isPropertyAssignment(property)) {
|
|
return [property.initializer]
|
|
}
|
|
if (ts.isShorthandPropertyAssignment(property)) {
|
|
return [property.name]
|
|
}
|
|
if (ts.isMethodDeclaration(property) || ts.isGetAccessorDeclaration(property)) {
|
|
return [property]
|
|
}
|
|
return undefined
|
|
}
|
|
return []
|
|
}
|
|
|
|
function materializeWrites(entries, use, seen) {
|
|
const values = []
|
|
for (const entry of entries) {
|
|
if (!entry.propertyPath) {
|
|
values.push(entry.expression)
|
|
continue
|
|
}
|
|
const matches = propertyPathValues(
|
|
entry.expression,
|
|
entry.propertyPath,
|
|
entry.node,
|
|
new Set(seen)
|
|
)
|
|
if (matches === undefined) {
|
|
return undefined
|
|
}
|
|
values.push(
|
|
...(matches.length === 0 && entry.defaultExpression ? [entry.defaultExpression] : matches)
|
|
)
|
|
}
|
|
return values
|
|
}
|
|
|
|
function valueExpressions(identifier, use = identifier, seen = new Set()) {
|
|
return materializeWrites(selectedWrites(identifier, use, false), use, seen)
|
|
}
|
|
|
|
function allValueExpressions(identifier, use = identifier, seen = new Set()) {
|
|
return materializeWrites(selectedWrites(identifier, use, true), use, seen)
|
|
}
|
|
|
|
function valueSources(identifier, use = identifier, all = false) {
|
|
return selectedWrites(identifier, use, all)
|
|
}
|
|
|
|
function stableBindingWrite(binding) {
|
|
let source
|
|
for (const entry of writes.get(binding) ?? []) {
|
|
if (!entry.conditional) {
|
|
if (source) {
|
|
return undefined
|
|
}
|
|
source = entry
|
|
continue
|
|
}
|
|
if (
|
|
!source ||
|
|
source.propertyPath ||
|
|
logicalWriteAction(entry.operator, [source.expression]) !== 'keep'
|
|
) {
|
|
return undefined
|
|
}
|
|
}
|
|
return source
|
|
}
|
|
|
|
function bindingWrites() {
|
|
return writes.entries()
|
|
}
|
|
|
|
function writeReachesUse(write, use) {
|
|
return isDefiniteBefore(write, use)
|
|
}
|
|
|
|
return {
|
|
allValueExpressions,
|
|
bindingWrites,
|
|
isImmutableBinding: isConstBinding,
|
|
propertyPathValues,
|
|
propertyValues,
|
|
stableBindingWrite,
|
|
unwrapExpression,
|
|
valueExpressions,
|
|
valueSources,
|
|
writeReachesUse
|
|
}
|
|
}
|