Files
orca/config/scripts/mobile-localization-source-bindings.mjs
T

128 lines
3.7 KiB
JavaScript

// TypeScript 7 is a native CLI; AST consumers still need the legacy JavaScript API.
import ts from 'typescript-api'
function bindingDeclarations(name, declaration, entries = []) {
if (!name) {
return entries
}
if (ts.isIdentifier(name)) {
entries.push([name.text, declaration])
} else {
for (const element of name.elements) {
bindingDeclarations(element.name, element, entries)
}
}
return entries
}
function isLexicalScope(node) {
return (
ts.isSourceFile(node) ||
ts.isFunctionLike(node) ||
ts.isModuleBlock(node) ||
ts.isBlock(node) ||
ts.isCaseBlock(node) ||
ts.isCatchClause(node) ||
ts.isForStatement(node) ||
ts.isForInStatement(node) ||
ts.isForOfStatement(node)
)
}
function nearestScope(node, sourceFile, predicate = isLexicalScope) {
let current = node.parent
while (current && current !== sourceFile) {
if (predicate(current)) {
return current
}
current = current.parent
}
return sourceFile
}
function varScope(node, sourceFile) {
return nearestScope(
node,
sourceFile,
(scope) => ts.isFunctionLike(scope) || ts.isModuleBlock(scope)
)
}
function collectDeclarations(sourceFile) {
const declarations = new Map()
function add(scope, name, declaration) {
const byName = declarations.get(scope) ?? new Map()
const matches = byName.get(name) ?? []
matches.push(declaration)
byName.set(name, matches)
declarations.set(scope, byName)
}
function visit(node) {
if (ts.isImportDeclaration(node)) {
const clause = node.importClause
if (clause?.name) {
add(sourceFile, clause.name.text, clause.name)
}
if (clause?.namedBindings && ts.isNamespaceImport(clause.namedBindings)) {
add(sourceFile, clause.namedBindings.name.text, clause.namedBindings)
}
if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings)) {
for (const element of clause.namedBindings.elements) {
add(sourceFile, element.name.text, element)
}
}
} else if (ts.isParameter(node)) {
for (const [name, declaration] of bindingDeclarations(node.name, node)) {
add(node.parent, name, declaration)
}
} else if (ts.isVariableDeclaration(node)) {
const declarationList = node.parent
const scope =
ts.isVariableDeclarationList(declarationList) &&
(declarationList.flags & ts.NodeFlags.BlockScoped) === 0
? varScope(node, sourceFile)
: nearestScope(node, sourceFile)
for (const [name, declaration] of bindingDeclarations(node.name, node)) {
add(scope, name, declaration)
}
} else if ((ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node)) && node.name) {
add(nearestScope(node, sourceFile), node.name.text, node)
} else if ((ts.isFunctionExpression(node) || ts.isClassExpression(node)) && node.name) {
add(node, node.name.text, node)
} else if (ts.isCatchClause(node) && node.variableDeclaration) {
for (const [name, declaration] of bindingDeclarations(
node.variableDeclaration.name,
node.variableDeclaration
)) {
add(node, name, declaration)
}
}
ts.forEachChild(node, visit)
}
visit(sourceFile)
return declarations
}
export function collectSourceBindings(sourceFile) {
const declarations = collectDeclarations(sourceFile)
return {
resolveBinding(identifier) {
let current = identifier.parent
while (current) {
const matches = declarations.get(current)?.get(identifier.text)
if (matches?.length) {
return matches[0]
}
if (current === sourceFile) {
break
}
current = current.parent
}
return undefined
}
}
}