* 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 { logger } from './compile_info';
import { harFilesRecord, GeneratedFileInHar, toUnixPath } from './utils';
import { resolveModuleNames as resolveModuleNamesOrig } from './ets_checker';
export function escapeRegExp(text: string): string {
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
export function splitLeadingTrivia(text: string): { leading: string; body: string } {
let pos: number = 0;
while (pos < text.length) {
const wsMatch: RegExpMatchArray | null = text.substring(pos).match(/^\s+/);
if (wsMatch) {
pos += wsMatch[0].length; continue;
}
const blockMatch: RegExpMatchArray | null = text.substring(pos).match(/^\/\*[\s\S]*?\*\//);
if (blockMatch) {
pos += blockMatch[0].length; continue;
}
const lineMatch: RegExpMatchArray | null = text.substring(pos).match(/^\/\/.*$/m);
if (lineMatch) {
pos += lineMatch[0].length; continue;
}
break;
}
return { leading: text.substring(0, pos), body: text.substring(pos) };
}
export function isExternalModuleSpecifier(specifier: string): boolean {
return !specifier.startsWith('.') && !specifier.startsWith('/');
}
export function isNameReferencedInText(name: string, text: string): boolean {
return new RegExp(`\\b${escapeRegExp(name)}\\b`).test(text);
}
export function isNamespaceReferencedInText(nsName: string, text: string): boolean {
return new RegExp(`\\b${escapeRegExp(nsName)}\\.`).test(text);
}
export function findNamesMatchingPattern(
names: string[], text: string, requireNamespaceDot: boolean
): string[] {
return names.filter((name: string): boolean =>
requireNamespaceDot ? isNamespaceReferencedInText(name, text) : isNameReferencedInText(name, text)
);
}
export function sortImportLinesFirst(lines: string[]): string[] {
const importLines: string[] = [];
const declarationLines: string[] = [];
for (const line of lines) {
if (line.startsWith('import ')) {
importLines.push(line);
} else {
declarationLines.push(line);
}
}
return [...importLines, ...declarationLines];
}
export function isStructDeclaration(node: ts.Node): boolean {
return ts.isStructDeclaration ? ts.isStructDeclaration(node) : false;
}
export function isTypeParameterSymbol(symbol: ts.Symbol): boolean {
return (symbol.flags & ts.SymbolFlags.TypeParameter) !== 0;
}
export function isEnumMemberSymbol(symbol: ts.Symbol): boolean {
return (symbol.flags & ts.SymbolFlags.EnumMember) !== 0;
}
export function isAutoGeneratedHeritageExpression(expr: ts.Expression): boolean {
if (ts.isObjectLiteralExpression(expr)) {
return true;
}
if (ts.isIdentifier(expr) && expr.pos === expr.end) {
return true;
}
return false;
}
export function isAutoGeneratedConstructor(member: ts.ClassElement): boolean {
if (!ts.isConstructorDeclaration(member)) {
return false;
}
if (member.getStart() === member.getEnd()) {
return true;
}
if ((member as ts.ConstructorDeclaration).virtual === true) {
return true;
}
if (member.parameters.length > 0 &&
member.parameters.every((p: ts.ParameterDeclaration): boolean =>
ts.isIdentifier(p.name) && p.name.text === '' && !!p.questionToken)) {
return true;
}
return false;
}
export function isRightOfMatchingQualifiedName(n: ts.Identifier, emitName: string): boolean {
if (!n.parent || !ts.isQualifiedName(n.parent) || n.parent.right !== n) {
return false;
}
const dotIdx: number = emitName.indexOf('.');
if (dotIdx < 0) {
return false;
}
const nsPart: string = emitName.substring(0, dotIdx);
const leftText: string = n.parent.left.getText(n.getSourceFile());
return leftText === nsPart;
}
export function getLocalNameOfDeclaration(declaration: ts.Node): string {
const named = declaration as ts.Node & { name?: ts.Node };
if (named.name && ts.isIdentifier(named.name)) {
return named.name.text;
}
return 'default';
}
export function findAncestorByKind(node: ts.Node, kind: ts.SyntaxKind): ts.Node | null {
let current: ts.Node | undefined = node.parent;
while (current) {
if (current.kind === kind) {
return current;
}
if (ts.isSourceFile(current)) {
break;
}
current = current.parent;
}
return null;
}
export function getDeclarationNode(node: ts.Node): ts.Node {
if (ts.isVariableDeclaration(node)) {
return findAncestorByKind(node, ts.SyntaxKind.VariableStatement) ?? node;
}
return node;
}
export function nodeHasModifier(node: ts.Node, kind: ts.SyntaxKind): boolean {
const decl: ts.Node = getDeclarationNode(node);
if (!ts.canHaveModifiers(decl)) {
return false;
}
const modifiers: readonly ts.ModifierLike[] | undefined = ts.getModifiers(decl);
if (!modifiers) {
return false;
}
return modifiers.some((m: ts.ModifierLike): boolean => m.kind === kind);
}
export function isTypeAliasLike(node: ts.Node): boolean {
const decl: ts.Node = getDeclarationNode(node);
return ts.isTypeAliasDeclaration(decl);
}
export function applyModifiersToNode(
node: ts.Node, newModifiers: readonly (ts.Decorator | ts.Modifier)[]
): ts.Node {
const modArray = ts.factory.createNodeArray(newModifiers);
if (isStructDeclaration(node)) {
const n = node as ts.StructDeclaration;
return ts.factory.updateStructDeclaration(n, modArray, n.name, n.typeParameters, n.heritageClauses, n.members);
}
if (ts.isClassDeclaration(node)) {
return ts.factory.updateClassDeclaration(node, modArray, node.name, node.typeParameters, node.heritageClauses, node.members);
}
if (ts.isInterfaceDeclaration(node)) {
return ts.factory.updateInterfaceDeclaration(node, modArray, node.name, node.typeParameters, node.heritageClauses, node.members);
}
if (ts.isEnumDeclaration(node)) {
return ts.factory.updateEnumDeclaration(node, modArray, node.name, node.members);
}
if (ts.isTypeAliasDeclaration(node)) {
return ts.factory.updateTypeAliasDeclaration(node, modArray, node.name, node.typeParameters, node.type);
}
if (ts.isFunctionDeclaration(node)) {
return ts.factory.updateFunctionDeclaration(node, modArray, node.asteriskToken, node.name, node.typeParameters, node.parameters, node.type, node.body);
}
if (ts.isVariableStatement(node)) {
return ts.factory.updateVariableStatement(node, modArray, node.declarationList);
}
if (ts.isModuleDeclaration(node)) {
return ts.factory.updateModuleDeclaration(node, modArray, node.name, node.body);
}
return node;
}
export function withNamespaceMemberModifiers(node: ts.Node): ts.Node {
const decorators: readonly ts.Decorator[] = ts.canHaveDecorators(node) ? ts.getDecorators(node) ?? [] : [];
const modifiers: readonly ts.Modifier[] = ts.canHaveModifiers(node) ? ts.getModifiers(node) ?? [] : [];
const keptModifiers: ts.Modifier[] = modifiers.filter((m: ts.Modifier): boolean =>
m.kind !== ts.SyntaxKind.ExportKeyword &&
m.kind !== ts.SyntaxKind.DeclareKeyword &&
m.kind !== ts.SyntaxKind.DefaultKeyword
);
const newModifiers: readonly (ts.Decorator | ts.Modifier)[] = [...decorators, ts.factory.createModifier(ts.SyntaxKind.ExportKeyword), ...keptModifiers];
return applyModifiersToNode(node, newModifiers);
}
export function transformToExportDeclare(text: string, node: ts.Node): string {
const hasDeclare: boolean = nodeHasModifier(node, ts.SyntaxKind.DeclareKeyword);
const hasDefault: boolean = nodeHasModifier(node, ts.SyntaxKind.DefaultKeyword);
const { leading, body } = splitLeadingTrivia(text);
let transformed: string = body;
if (hasDefault) {
transformed = transformed.replace(/^export\s+default\s+/, 'export declare ');
} else {
const hasExport: boolean = nodeHasModifier(node, ts.SyntaxKind.ExportKeyword);
const isTypeAlias: boolean = isTypeAliasLike(node);
if (isTypeAlias) {
if (hasExport && hasDeclare) {
transformed = transformed.replace(/^export\s+declare\s+/, 'export ');
}
else if (hasDeclare) {
transformed = transformed.replace(/^declare\s+/, 'export ');
}
else if (!hasExport) {
transformed = 'export ' + transformed;
}
} else if (hasExport && hasDeclare) {
} else if (hasExport) {
transformed = transformed.replace(/^export\s+/, 'export declare ');
} else if (hasDeclare) {
transformed = transformed.replace(/^declare\s+/, 'export declare ');
} else {
transformed = 'export declare ' + transformed;
}
}
return leading + transformed;
}
export function ensureDeclareKeyword(text: string, node: ts.Node): string {
const { leading, body } = splitLeadingTrivia(text);
if (/^declare\s/.test(body)) {
return text;
}
return leading + 'declare ' + body;
}
export function stripExportFromText(text: string, node: ts.Node): string {
const hasDeclare: boolean = nodeHasModifier(node, ts.SyntaxKind.DeclareKeyword);
const hasDefault: boolean = nodeHasModifier(node, ts.SyntaxKind.DefaultKeyword);
const { leading, body } = splitLeadingTrivia(text);
let stripped: string = body;
if (hasDefault) {
stripped = stripped.replace(/^export\s+default\s+/, hasDeclare ? '' : 'declare ');
} else {
stripped = stripped.replace(/^export\s+/, hasDeclare ? '' : 'declare ');
}
return leading + stripped;
}
export function stripDefaultKeywordFromText(text: string, node: ts.Node): string {
if (nodeHasModifier(node, ts.SyntaxKind.DefaultKeyword)) {
const { leading, body } = splitLeadingTrivia(text);
return leading + body.replace(/^export\s+default\s+/, 'export declare ');
}
return text;
}
export function filterAutoGeneratedHeritage(
clauses: ts.NodeArray<ts.HeritageClause> | undefined
): ts.NodeArray<ts.HeritageClause> | undefined {
if (!clauses) {
return clauses;
}
const filtered: ts.HeritageClause[] = clauses.filter((clause: ts.HeritageClause): boolean => {
if (clause.token !== ts.SyntaxKind.ExtendsKeyword) {
return true;
}
return !clause.types.some((t: ts.ExpressionWithTypeArguments): boolean =>
isAutoGeneratedHeritageExpression(t.expression));
});
if (filtered.length === clauses.length) {
return clauses;
}
if (filtered.length === 0) {
return undefined;
}
return ts.factory.createNodeArray(filtered);
}
export function sanitizeStructOrClass(node: ts.ClassLikeDeclaration): ts.Node {
const filteredMembers: ts.ClassElement[] = node.members.filter(
(m: ts.ClassElement): boolean => !isAutoGeneratedConstructor(m)
);
const filteredHeritage: ts.NodeArray<ts.HeritageClause> | undefined = filterAutoGeneratedHeritage(node.heritageClauses);
if (isStructDeclaration(node)) {
return ts.factory.updateStructDeclaration(node, node.modifiers, node.name, node.typeParameters,
filteredHeritage, ts.factory.createNodeArray(filteredMembers));
}
return ts.factory.updateClassDeclaration(node, node.modifiers, node.name, node.typeParameters, filteredHeritage, ts.factory.createNodeArray(filteredMembers));
}
export function sanitizeDeclarationNode(node: ts.Node): ts.Node {
if (isStructDeclaration(node) || ts.isClassDeclaration(node)) {
return sanitizeStructOrClass(node);
}
if (ts.isModuleDeclaration(node) && node.body && ts.isModuleBlock(node.body)) {
let changed: boolean = false;
const sanitizedStmts: ts.Statement[] = node.body.statements.map(
(stmt: ts.Statement): ts.Statement => {
const sanitized: ts.Node = sanitizeDeclarationNode(stmt);
if (sanitized !== stmt) {
changed = true;
}
return sanitized as ts.Statement;
}
);
if (changed) {
const newBody: ts.ModuleBlock = ts.factory.updateModuleBlock(
node.body, ts.factory.createNodeArray(sanitizedStmts)
);
return ts.factory.updateModuleDeclaration(
node, node.modifiers, node.name, newBody
);
}
}
return node;
}
export function findAncestorImportDeclaration(node: ts.Node): ts.ImportDeclaration | null {
let current: ts.Node | undefined = node;
while (current) {
if (ts.isImportDeclaration(current)) {
return current;
}
if (ts.isSourceFile(current)) {
break;
}
current = current.parent;
}
return null;
}
export function extractModuleSpecifierFromExport(decl: ts.ExportSpecifier): string | null {
if (!ts.isNamedExports(decl.parent)) {
return null;
}
const exportDecl: ts.Node = decl.parent.parent;
if (ts.isExportDeclaration(exportDecl) && exportDecl.moduleSpecifier && ts.isStringLiteral(exportDecl.moduleSpecifier)) {
return exportDecl.moduleSpecifier.text;
}
return null;
}
export function extractModuleSpecifierFromImport(decl: ts.Declaration): string | null {
const importDecl: ts.ImportDeclaration | null = findAncestorImportDeclaration(decl);
if (importDecl && importDecl.moduleSpecifier && ts.isStringLiteral(importDecl.moduleSpecifier)) {
return importDecl.moduleSpecifier.text;
}
return null;
}
export function extractModuleSpecifier(decl: ts.Declaration): string | null {
if (ts.isExportSpecifier(decl)) {
return extractModuleSpecifierFromExport(decl);
}
return extractModuleSpecifierFromImport(decl);
}
export function validateOutput(content: string, entryFile: string): void {
if (!content || content.trim().length === 0) {
return;
}
try {
const isDets: boolean = entryFile.endsWith('.d.ets');
const scriptKind: ts.ScriptKind = isDets
? (ts.ScriptKind as unknown as Record<string, number>).ETS ?? ts.ScriptKind.TS
: ts.ScriptKind.TS;
const sf: ts.SourceFile = ts.createSourceFile(entryFile, content, ts.ScriptTarget.Latest, true, scriptKind);
if (sf.parseDiagnostics && sf.parseDiagnostics.length > 0) {
const errors: string = sf.parseDiagnostics.map((d: ts.Diagnostic): string => {
const msg: string = typeof d.messageText === 'string'
? d.messageText
: (d.messageText as ts.DiagnosticMessageChain).messageText;
return `line ${d.line + 1}: ${msg}`;
}).join('; ');
logger.debug(`Declaration merge output has parse errors in ${entryFile}: ${errors}`);
}
} catch (e) {
const errMsg: string = e instanceof Error ? e.message : String(e);
logger.debug(`Declaration merge output validation failed for ${entryFile}: ${errMsg}`);
}
}
export function resolveWithFallback(
moduleName: string,
sourceContainingFile: string | null,
resolved: ts.ResolvedModuleFull | null
): ts.ResolvedModuleFull | null {
if (resolved) {
return resolved;
}
if (!sourceContainingFile) {
return null;
}
const fallback: (ts.ResolvedModuleFull | null)[] =
resolveModuleNamesOrig([moduleName], sourceContainingFile);
return fallback[0] ?? null;
}
export function buildBidirectionalMaps(): {
sourceToDecl: Map<string, string>;
declToSource: Map<string, string>;
} {
const sourceToDecl: Map<string, string> = new Map();
const declToSource: Map<string, string> = new Map();
harFilesRecord.forEach((value: GeneratedFileInHar): void => {
if (value.originalDeclarationCachePath) {
sourceToDecl.set(toUnixPath(value.sourcePath), toUnixPath(value.originalDeclarationCachePath));
declToSource.set(toUnixPath(value.originalDeclarationCachePath), toUnixPath(value.sourcePath));
}
});
return { sourceToDecl, declToSource };
}
export function createDeclarationModuleResolver(
projectPath: string
): (moduleNames: string[], containingFile: string) => (ts.ResolvedModuleFull | null)[] {
const { sourceToDecl, declToSource } = buildBidirectionalMaps();
return (
moduleNames: string[],
containingFile: string
): (ts.ResolvedModuleFull | null)[] => {
const resolved: (ts.ResolvedModuleFull | null)[] =
resolveModuleNamesOrig(moduleNames, containingFile);
const sourceContainingFile: string | null =
declToSource.get(toUnixPath(containingFile)) ?? null;
for (let i = 0; i < resolved.length; i++) {
resolved[i] = resolveWithFallback(moduleNames[i], sourceContainingFile, resolved[i]);
}
for (let i = 0; i < resolved.length; i++) {
const mod = resolved[i];
if (!mod?.resolvedFileName) {
continue;
}
const declPath: string | undefined = sourceToDecl.get(toUnixPath(mod.resolvedFileName));
if (declPath) {
resolved[i] = {
resolvedFileName: declPath,
extension: declPath.endsWith('.d.ets') ? ts.Extension.Dets : ts.Extension.Dts,
};
}
}
return resolved;
};
}
export enum EntityKind {
Exported,
TypeDependency,
SystemApiImport,
SystemApiReexport,
}
export type SourceFileExt = '.d.ets' | '.d.ts';
export interface MergeEntity {
kind: EntityKind;
symbol: ts.Symbol;
declarations: ts.Declaration[];
emitName: string;
exportNames: string[];
isDefaultExport: boolean;
systemApiInfo?: { moduleName: string; name: string; statementText: string; exportStatementText?: string };
isolationNamespace?: string;
preferredName: string;
sourceFileExt: SourceFileExt;
aliases: string[];
usageExts?: SourceFileExt[];
}
export interface NamespaceEmitItem {
symbol: ts.Symbol;
declarations: ts.Declaration[];
emitName: string;
aliases: string[];
}
export interface NamespaceBlockMember extends NamespaceEmitItem {
sourceFileExt: SourceFileExt;
}
export interface NamespaceSystemApiMember {
name: string;
importStatement: string;
}
export interface NamespaceBlock {
name: string;
members: NamespaceBlockMember[];
sourceFileExt: SourceFileExt;
systemApiMembers: NamespaceSystemApiMember[];
}
export interface EmitContext {
primaryLines: string[];
companionLines: string[];
emittedStatements: Set<string>;
companionEmittedStatements: Set<string>;
primaryEmittedTexts: Set<string>;
companionEmittedTexts: Set<string>;
crossExtExportedNames: string[];
crossExtImportNamespaces: Set<string>;
crossExtImportNames: string[];
crossExtNamespaceImports: Set<string>;
exportRenames: Array<{ emitName: string; exportName: string }>;
primaryExt: SourceFileExt;
companionModuleSpecifier: string;
}
export interface DeclarationMergeOptions {
entryFile?: string;
entryFiles?: string[];
projectPath: string;
isByteCodeHar: boolean;
moduleRootPath?: string;
packageDir?: string;
systemModules?: string[];
sdkPath?: string;
resolveModuleNames?: (
moduleNames: string[],
containingFile: string
) => (ts.ResolvedModuleFull | null)[];
}