* Copyright (c) 2026 Huawei Device Co., Ltd.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as ts from 'typescript';
import { extractModuleSpecifier, extractModuleSpecifierFromExport, isExternalModuleSpecifier, getLocalNameOfDeclaration } from './declaration_merger_utils';
export interface SysApiContext {
checker: ts.TypeChecker;
printer: ts.Printer;
entrySourceFile: ts.SourceFile | undefined;
systemModules: string[] | undefined;
}
export function resolveToActualDeclaration(checker: ts.TypeChecker, symbol: ts.Symbol): ts.Symbol | null {
const visited: Set<ts.Symbol> = new Set();
let current: ts.Symbol | undefined = symbol;
while (current && (current.flags & ts.SymbolFlags.Alias)) {
if (visited.has(current)) {
break;
}
visited.add(current);
const aliased: ts.Symbol = checker.getAliasedSymbol(current);
if (!aliased || aliased === current) {
break;
}
current = aliased;
}
if (!current || !current.declarations || current.declarations.length === 0) {
return null;
}
return current;
}
export function extractStatementText(printer: ts.Printer, decl: ts.Declaration): string | null {
let current: ts.Node | undefined = decl;
while (current) {
if (ts.isImportDeclaration(current) || ts.isExportDeclaration(current)) {
return printer.printNode(ts.EmitHint.Unspecified, current, current.getSourceFile()).trim();
}
if (ts.isSourceFile(current)) {
break;
}
current = current.parent;
}
return null;
}
export function specifierMatchesSystemModule(systemModules: string[] | undefined, specifier: string): boolean {
if (!systemModules) {
return false;
}
for (const mod of systemModules) {
if (mod.replace(/\.d\.(ets|ts)$/, '') === specifier) {
return true;
}
}
return false;
}
export function collectExportDeclarationsFromModule(
checker: ts.TypeChecker, moduleSymbol: ts.Symbol, exportName: string
): ts.Declaration[] {
const results: ts.Declaration[] = [];
for (const exp of checker.getExportsOfModule(moduleSymbol)) {
if (exp.name !== exportName) {
continue;
}
for (const decl of exp.declarations ?? []) {
if (ts.isExportSpecifier(decl)) {
results.push(decl);
}
}
}
return results;
}
export function importClauseMatchesExport(
checker: ts.TypeChecker, importClause: ts.ImportClause,
exportName: string, resolvedSymbol: ts.Symbol
): boolean {
if (importClause.namedBindings && ts.isNamedImports(importClause.namedBindings)) {
for (const spec of importClause.namedBindings.elements) {
if (spec.name.text !== exportName) {
continue;
}
const specSymbol = checker.getSymbolAtLocation(spec.name);
if (specSymbol && resolveToActualDeclaration(checker, specSymbol) === resolvedSymbol) {
return true;
}
}
}
if (importClause.name && importClause.name.text === exportName) {
const nameSymbol = checker.getSymbolAtLocation(importClause.name);
if (nameSymbol && resolveToActualDeclaration(checker, nameSymbol) === resolvedSymbol) {
return true;
}
}
if (importClause.namedBindings && ts.isNamespaceImport(importClause.namedBindings) &&
importClause.namedBindings.name.text === exportName) {
const nsSymbol = checker.getSymbolAtLocation(importClause.namedBindings.name);
if (nsSymbol && resolveToActualDeclaration(checker, nsSymbol) === resolvedSymbol) {
return true;
}
}
return false;
}
export function isSystemApiDeclaration(
ctx: SysApiContext, declaration: ts.Node, aliasSymbol?: ts.Symbol
): { moduleName: string; name: string; statementText: string } | null {
if (!ctx.systemModules || ctx.systemModules.length === 0) {
return null;
}
if (!aliasSymbol) {
return null;
}
let found = findSystemApiModuleName(ctx, aliasSymbol);
if (!found) {
found = findSystemApiModuleNameViaReexportChain(ctx, aliasSymbol);
}
if (!found) {
return null;
}
return { moduleName: found.moduleName, name: getLocalNameOfDeclaration(declaration), statementText: found.statementText };
}
export function findSystemApiModuleName(
ctx: SysApiContext, symbol: ts.Symbol
): { moduleName: string; statementText: string } | null {
const visited: Set<ts.Symbol> = new Set();
let current: ts.Symbol | undefined = symbol;
while (current && !visited.has(current)) {
visited.add(current);
for (const decl of current.declarations ?? []) {
const specifier = extractModuleSpecifier(decl);
if (specifier && specifierMatchesSystemModule(ctx.systemModules, specifier)) {
const stmt = extractStatementText(ctx.printer, decl);
return { moduleName: specifier, statementText: stmt ?? '' };
}
}
if (!(current.flags & ts.SymbolFlags.Alias)) {
break;
}
const aliased = ctx.checker.getAliasedSymbol(current);
if (!aliased || aliased === current) {
break;
}
current = aliased;
}
return null;
}
export function findSystemApiModuleNameViaReexportChain(
ctx: SysApiContext, symbol: ts.Symbol
): { moduleName: string; statementText: string } | null {
const visited: Set<string> = new Set();
const queue: Array<{ declarations: readonly ts.Declaration[]; exportName: string }> = [];
const seedResult = seedReexportBfsQueue(ctx, symbol, queue, visited);
if (seedResult) {
return seedResult;
}
while (queue.length > 0) {
const item = queue.shift()!;
for (const expDecl of item.declarations) {
const expSpec = extractModuleSpecifier(expDecl);
if (!expSpec) {
continue;
}
if (specifierMatchesSystemModule(ctx.systemModules, expSpec)) {
const stmt = extractStatementText(ctx.printer, expDecl);
return { moduleName: expSpec, statementText: stmt ?? '' };
}
const namedExports: ts.Node = (expDecl as ts.ExportSpecifier).parent;
if (!ts.isNamedExports(namedExports)) {
continue;
}
const exportDecl: ts.Node = namedExports.parent;
if (!ts.isExportDeclaration(exportDecl) || !exportDecl.moduleSpecifier ||
!ts.isStringLiteral(exportDecl.moduleSpecifier)) {
continue;
}
const nextKey = expSpec + ':' + item.exportName;
if (visited.has(nextKey)) {
continue;
}
visited.add(nextKey);
const moduleSym = ctx.checker.getSymbolAtLocation(exportDecl.moduleSpecifier);
if (!moduleSym) {
continue;
}
const nextDecls = collectExportDeclarationsFromModule(ctx.checker, moduleSym, item.exportName);
if (nextDecls.length > 0) {
queue.push({ declarations: nextDecls, exportName: item.exportName });
}
}
}
return null;
}
function seedReexportBfsQueue(
ctx: SysApiContext, symbol: ts.Symbol,
queue: Array<{ declarations: readonly ts.Declaration[]; exportName: string }>,
visited: Set<string>
): { moduleName: string; statementText: string } | null {
for (const decl of symbol.declarations ?? []) {
if (!ts.isExportSpecifier(decl)) {
continue;
}
const namedExports: ts.Node = decl.parent;
if (!ts.isNamedExports(namedExports)) {
continue;
}
const exportDecl: ts.Node = namedExports.parent;
if (!ts.isExportDeclaration(exportDecl) || !exportDecl.moduleSpecifier ||
!ts.isStringLiteral(exportDecl.moduleSpecifier)) {
continue;
}
const specifier: string = exportDecl.moduleSpecifier.text;
const exportName: string = (decl.propertyName ?? decl.name).text;
if (specifierMatchesSystemModule(ctx.systemModules, specifier)) {
const stmt = extractStatementText(ctx.printer, decl);
return { moduleName: specifier, statementText: stmt ?? '' };
}
const moduleSym = ctx.checker.getSymbolAtLocation(exportDecl.moduleSpecifier);
if (!moduleSym) {
continue;
}
const nextDecls = collectExportDeclarationsFromModule(ctx.checker, moduleSym, exportName);
if (nextDecls.length > 0) {
const key = specifier + ':' + exportName;
if (!visited.has(key)) {
visited.add(key); queue.push({ declarations: nextDecls, exportName });
}
}
}
return null;
}
export function searchImportInSourceFile(
ctx: SysApiContext, sourceFile: ts.SourceFile, exportName: string, resolvedSymbol: ts.Symbol
): { moduleName: string; statementText: string } | null {
let result: { moduleName: string; statementText: string } | null = null;
ts.forEachChild(sourceFile, (node: ts.Node): void => {
if (result || !ts.isImportDeclaration(node)) {
return;
}
if (!node.moduleSpecifier || !ts.isStringLiteral(node.moduleSpecifier) || !node.importClause) {
return;
}
if (importClauseMatchesExport(ctx.checker, node.importClause, exportName, resolvedSymbol)) {
result = {
moduleName: node.moduleSpecifier.text,
statementText: ctx.printer.printNode(ts.EmitHint.Unspecified, node, sourceFile).trim()
};
}
});
return result;
}
export function findLocalImportForExport(
ctx: SysApiContext, exportName: string, resolvedSymbol: ts.Symbol
): { moduleName: string; statementText: string } | null {
if (!ctx.entrySourceFile) {
return null;
}
return searchImportInSourceFile(ctx, ctx.entrySourceFile, exportName, resolvedSymbol);
}
export function findImportInModule(
ctx: SysApiContext, moduleSymbol: ts.Symbol,
memberName: string, targetSymbol: ts.Symbol
): { moduleName: string; statementText: string } | null {
const sourceFile: ts.Node | undefined = moduleSymbol.declarations?.[0];
if (!sourceFile || !ts.isSourceFile(sourceFile)) {
return null;
}
let result: { moduleName: string; statementText: string } | null = null;
ts.forEachChild(sourceFile, (node: ts.Node): void => {
if (result || !ts.isImportDeclaration(node)) {
return;
}
if (!node.moduleSpecifier || !ts.isStringLiteral(node.moduleSpecifier) || !node.importClause) {
return;
}
if (importClauseMatchesExport(ctx.checker, node.importClause, memberName, targetSymbol)) {
result = {
moduleName: node.moduleSpecifier.text,
statementText: ctx.printer.printNode(ts.EmitHint.Unspecified, node, sourceFile as ts.SourceFile).trim()
};
}
});
return result;
}
export function findReexportForSymbol(
printer: ts.Printer, memberSymbol: ts.Symbol
): string | null {
for (const decl of memberSymbol.declarations ?? []) {
if (!ts.isExportSpecifier(decl)) {
continue;
}
const specifier = extractModuleSpecifierFromExport(decl);
if (specifier && isExternalModuleSpecifier(specifier)) {
const stmt = extractStatementText(printer, decl);
if (stmt) {
return stmt.replace(/^export\s+/, 'import ');
}
}
}
return null;
}