* Copyright (c) 2025 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.
*/
const ts = require('typescript');
const fs = require('fs');
const path = require('path');
const commander = require('commander');
const typeArray = ['long', 'double', 'int'];
const SPLIT_LENGTH = 100;
let inputDir = '';
let outputDir = '';
let index = '';
let jsDocContent = '';
let tagDataList = [];
const apiTransformPath = '\\APITransformOutPath';
const apiRemovalPath = '\\APIduplicateRemovalOutPath';
const jsDocTransformPath = '\\jsDocTransformOutPath';
function main() {
const program = new commander.Command();
program
.name('intToNumber')
.version('0.0.1');
program
.option('--path <string>', 'input path')
.option('--output <string>', 'output path')
.option('--index <string>', 'split index')
.action((opts) => {
if (!opts.path || !opts.output) {
console.error('Error: Must provide --path and --output parameters');
process.exit(1);
}
inputDir = path.resolve(opts.path);
outputDir = path.resolve(opts.output);
index = opts.index;
if (!fs.existsSync(inputDir)) {
console.error(`Error: Input directory does not exist: ${inputDir}`);
process.exit(1);
}
let apiUtFiles1 = [];
readFile(inputDir + '/api', apiUtFiles1);
readFile(inputDir + '/arkts', apiUtFiles1);
apiUtFiles1 = splitEntryArray(apiUtFiles1);
tsTransform(inputDir, apiTransformPath, apiUtFiles1, recursionAstCallback);
tsTransform(inputDir + apiTransformPath, apiRemovalPath, apiUtFiles1, recursionAstCallback2);
tsTransform(inputDir + apiRemovalPath, jsDocTransformPath, apiUtFiles1, parseJSDocCallback);
tsTransform(inputDir + jsDocTransformPath, outputDir, apiUtFiles1, parseJSDocCallback2);
});
program.parse(process.argv);
}
function splitEntryArray(files) {
if (index && index !== '') {
const splitNumber = Number.parseInt(index);
if (splitNumber < 9) {
files = files.splice(splitNumber * SPLIT_LENGTH, SPLIT_LENGTH);
} else {
files = files.splice(splitNumber * SPLIT_LENGTH, files.length - splitNumber * SPLIT_LENGTH);
}
}
return files;
}
* 读取目录下所有文件
* @param {string} dir 文件目录
* @param {Array} utFiles 所有文件
*/
function readFile(dir, utFiles) {
if (!fs.existsSync(dir)) {
return;
}
try {
const files = fs.readdirSync(dir);
files.forEach((element) => {
if (element === 'build-tools') {
return;
}
const filePath = path.join(dir, element);
const status = fs.statSync(filePath);
if (status.isDirectory()) {
readFile(filePath, utFiles);
} else {
utFiles.push(filePath.replace(inputDir, ''));
}
});
} catch (e) {
console.error('Error reading files: ' + e.message);
}
}
* 遍历所有文件进行处理
* @param {Array} utFiles 所有文件
* @param {recursionAstCallback} callback 回调函数
*/
function tsTransform(inputurl, outputPath, utFiles, callback) {
utFiles.forEach((url) => {
const apiBaseName = path.basename(url);
tagDataList = [];
if (/\.d\.ts/.test(apiBaseName) || /\.d\.ets/.test(apiBaseName)) {
let content = fs.readFileSync(inputurl + url, 'utf-8');
jsDocContent = content;
const fileName = processFileName(inputurl + url);
ts.transpileModule(content, {
compilerOptions: {
target: ts.ScriptTarget.ES2017,
etsAnnotationsEnable: true
},
fileName: fileName,
transformers: { before: [callback(inputurl + url, outputPath)] }
});
} else {
let content = fs.readFileSync(inputDir + url, 'utf-8');
const outputPath = replaceInputDirWithOutputDir(inputDir + url, inputDir, outputDir);
ensureDirectoryExists(outputPath);
fs.writeFileSync(outputPath, content);
}
});
}
* 统一处理文件名称,修改后缀等
* @param {string} filePath 文件路径
* @returns {string} filename 文件名称
*/
function processFileName(filePath) {
return path
.basename(filePath)
.replace(/\.d\.ts$/g, '.ts')
.replace(/\.d\.ets$/g, '.ets');
}
* 每个文件处理签回调函数第一个
* @callback recursionAstCallback
* @param {string} url 文件路径
* @returns {Function}
*/
function recursionAstCallback(url, outputDir) {
return (context) => {
return (sourceFile) => {
node = processVisitEachChild1(context, sourceFile);
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
const result = printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
const outputPath = url.replace(inputDir, inputDir + outputDir);
ensureDirectoryExists(outputPath);
fs.writeFileSync(outputPath, result);
return ts.factory.createSourceFile([], ts.SyntaxKind.EndOfFileToken, ts.NodeFlags.None);
};
};
}
* 每个文件处理签回调函数第一个
* @callback recursionAstCallback
* @param {string} url 文件路径
* @returns {Function}
*/
function recursionAstCallback2(url, outputDir) {
return (context) => {
return (sourceFile) => {
node = processVisitEachChild2(context, sourceFile);
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
const result = printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
const outputPath = url.replace(inputDir + apiTransformPath, inputDir + outputDir);
ensureDirectoryExists(outputPath);
fs.writeFileSync(outputPath, result);
return ts.factory.createSourceFile([], ts.SyntaxKind.EndOfFileToken, ts.NodeFlags.None);
};
};
}
* 每个文件处理签回调函数第一个
* @callback parseJSDocCallback
* @param {string} url 文件路径
* @returns {Function}
*/
function parseJSDocCallback(url, outputDir) {
return (context) => {
return (sourceFile) => {
node = parseJSDocVisitEachChild1(context, sourceFile);
changeContent(tagDataList);
const outputPath = url.replace(inputDir + apiRemovalPath, inputDir + outputDir);
ensureDirectoryExists(outputPath);
fs.writeFileSync(outputPath, jsDocContent);
return ts.factory.createSourceFile([], ts.SyntaxKind.EndOfFileToken, ts.NodeFlags.None);
};
};
}
* 每个文件处理签回调函数第一个
* @callback parseJSDocCallback
* @param {string} url 文件路径
* @returns {Function}
*/
function parseJSDocCallback2(url) {
return (context) => {
return (sourceFile) => {
const contents = fs.readFileSync(url, 'utf-8');
node = parseJSDocVisitEachChild2(context, sourceFile, contents);
changeContent(tagDataList);
const outputPath = replaceInputDirWithOutputDir(url, inputDir + jsDocTransformPath, outputDir);
ensureDirectoryExists(outputPath);
fs.writeFileSync(outputPath, jsDocContent);
return ts.factory.createSourceFile([], ts.SyntaxKind.EndOfFileToken, ts.NodeFlags.None);
};
};
}
* 遍历处理tsnode节点
* @param {context} 解下过后的内容
* @param {node} 解下过后的节点
* @returns ts.node
*/
function parseJSDocVisitEachChild1(context, node) {
return ts.visitEachChild(node, processAllNodes, context);
function processAllNodes(node) {
if (node.jsDoc) {
processAllNodesJSDoc(node.jsDoc);
}
return ts.visitEachChild(node, processAllNodes, context);
}
function jsDocNodeForeach(tags) {
tags.forEach(tag => {
if (!tag.typeExpression) {
return;
}
const typeExpr = tag.typeExpression;
const newTypeExpr = parseTypeExpr(typeExpr);
applJSDocTransformations(typeExpr.type, newTypeExpr, tagDataList, true);
});
}
function processAllNodesJSDoc(jsDocNode) {
jsDocNode.forEach(doc => {
if (!doc.tags) {
return;
}
if (/(long|double|int)/g.test(doc.getText())) {
jsDocNodeForeach(doc.tags);
}
const commentText = doc.getText();
const globalPosStart = doc.pos;
const regex = /\{@link\s+([\s\S]*?)\}/gi;
let match;
while ((match = regex.exec(commentText)) !== null) {
handleLinkMatch(match, globalPosStart);
}
});
}
function parseTypeExpr(typeExpr) {
let newTypeExpr = typeExpr;
if (typeExpr.type.kind === ts.SyntaxKind.JSDocNullableType) {
newTypeExpr = judgeKind(typeExpr.type);
} else {
newTypeExpr = parseTypeExpression(typeExpr.type);
}
return newTypeExpr;
}
function judgeKind(typeExprType) {
if (typeExprType.type.kind && typeExprType.type.kind === ts.SyntaxKind.ParenthesizedType) {
return parseTypeExpression(typeExprType.type.type);
} else {
return parseTypeExpression(typeExprType.type);
}
}
function parseTypeExpression(node) {
if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && typeArray.includes(node.typeName.getText())) {
node = ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword);
}
return ts.visitEachChild(node, parseTypeExpression, context);
}
}
* 处理单个 {@link} 匹配项,仅处理带函数签名 () 的场景
* 严格保留原始文本格式,不修改空格、换行、缩进
* @param {Array} match - 正则匹配结果
* @param {number} globalPosStart - 全局起始偏移位置
*/
function handleLinkMatch(match, globalPosStart) {
const fullMatch = match[0];
const linkContent = match[1];
let convertedText = fullMatch;
if (!/\(.*\)/.test(linkContent)) {
return;
}
const processedContent = processSignatureContent(linkContent);
convertedText = fullMatch.replace(linkContent, processedContent);
if (convertedText !== fullMatch) {
tagDataList.push({
pos: globalPosStart + match.index,
end: globalPosStart + match.index + fullMatch.length,
convertedText: convertedText
});
}
}
* 处理函数签名内容,替换类型并合并number
* 【核心:不修改原始空格、换行、缩进、括号格式】
* @param {string} content - 原始 {@link} 内部内容
* @returns {string} 处理后内容,格式完全保留
*/
function processSignatureContent(content) {
let result = content.replace(/\b(int|long|double)\b/g, 'number');
return result.replace(/(\()([\s\S]*?)(\))/g, (originalMatch, openBracket, paramStr, closeBracket) => {
const params = paramStr.split(',');
const processedParams = params.map(param => processSingleParameter(param));
return openBracket + processedParams.join(',') + closeBracket;
});
}
* 处理单个参数,合并number,保留原始空格/换行
* @param {string} param - 原始单个参数字符串(含空白符)
* @returns {string} 处理后参数,格式不变
*/
function processSingleParameter(param) {
if (!param.includes(':')){
return param;
}
const colonIndex = param.indexOf(':');
const paramNamePart = param.substring(0, colonIndex);
const typePart = param.substring(colonIndex + 1);
const typeItems = typePart.split(/\s*\|\s*/);
const newTypeItems = [];
let hasNumber = false;
for (const type of typeItems) {
const trimmedType = type.trim();
if (trimmedType === 'number') {
if (!hasNumber) {
newTypeItems.push(type);
hasNumber = true;
}
} else {
newTypeItems.push(type);
}
}
return paramNamePart + ':' + newTypeItems.join(' | ');
}
*
* @param {typeExpr} 原始typeExpr
* @param {newTypeExpr} 新typeExpr
* @param {content} 文本内容
*/
function applJSDocTransformations(typeExpr, newTypeExpr, tagDataList, isChange) {
if (!typeExpr && !newTypeExpr) {
return;
}
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
const finalContent = printer.printNode(ts.EmitHint.Unspecified, newTypeExpr)
.replace(/\s*;\s*/g, ', ')
.replace(/,\s*}$/, ' }')
.replace(/\s+/g, ' ')
.replace(/, \}/g, '}');
if (finalContent.includes('number') && typeExpr.kind === ts.SyntaxKind.JSDocNullableType && !finalContent.includes('?number') && isChange) {
if (typeExpr.type.type && typeExpr.type.type.kind === ts.SyntaxKind.UnionType) {
const data = {
pos: typeExpr.pos,
end: typeExpr.end,
convertedText: '?' + `(${finalContent})`
};
tagDataList.push(data);
} else {
const data = {
pos: typeExpr.pos,
end: typeExpr.end,
convertedText: '?' + finalContent
};
tagDataList.push(data);
}
} else if (finalContent.includes('number')) {
const data = {
pos: typeExpr.pos,
end: typeExpr.end,
convertedText: finalContent
};
tagDataList.push(data);
}
}
*
* @param {typeExpr} 原始typeExpr
* @param {newTypeExpr} 新typeExpr
* @param {content} 文本内容
*/
function changeContent(tagDataList) {
tagDataList.sort((a, b) => b.pos - a.pos);
for (const data of tagDataList) {
const before = jsDocContent.substring(0, data.pos);
const after = jsDocContent.substring(data.end);
if (jsDocContent.substring(data.pos, data.pos + 1) === ' ') {
jsDocContent = before + ` ${data.convertedText}` + after;
} else {
jsDocContent = before + `${data.convertedText}` + after;
}
}
}
* 遍历处理tsnode节点
* @param {context} 解下过后的内容
* @param {node} 解下过后的节点
* @returns ts.node
*/
function parseJSDocVisitEachChild2(context, node) {
return ts.visitEachChild(node, processAllNodes, context);
function processAllNodes(node) {
if (node.jsDoc) {
removeChangeData(node.jsDoc);
}
return ts.visitEachChild(node, processAllNodes, context);
}
function jsDocNodeForeach(tags) {
tags.forEach(tag => {
if (!tag.typeExpression) {
return;
}
writeDataToFile(tag);
});
}
function removeChangeData(node) {
node.forEach(doc => {
if (!doc.tags) {
return;
}
jsDocNodeForeach(doc.tags);
});
}
function writeDataToFile(tag) {
const typeExpr = tag.typeExpression;
if (ts.isJSDocNullableType(typeExpr.type) && typeExpr.type.type) {
const newTypeExpr = parseTypeExpression(typeExpr.type.type);
applJSDocTransformations(typeExpr.type.type, newTypeExpr, tagDataList, false);
} else {
const newTypeExpr = parseTypeExpression(typeExpr.type);
applJSDocTransformations(typeExpr.type, newTypeExpr, tagDataList, false);
}
}
function parseTypeExpression(node) {
if (ts.isUnionTypeNode(node)) {
const types = node.types;
const newTypes1 = [];
let newTypes2 = [];
types.forEach(type => {
newTypes1.push(collectNewTypes(type));
});
newTypes2 = duplicateRemoval(newTypes1);
node = ts.factory.updateUnionTypeNode(node, newTypes2);
}
return ts.visitEachChild(node, parseTypeExpression, context);
}
}
* @param {type} 需要去重的节点
*/
function collectNewTypes(type) {
if (ts.isTypeReferenceNode(type) && ts.isIdentifier(type.typeName) && typeArray.includes(type.typeName.getText())) {
return ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword);
} else {
return type;
}
}
*
* @param {type} 解析过后的节点
* @returns
*/
function duplicateRemoval(newTypesArr) {
let newTypesNodeArr = [];
let newTypesTextArr = [];
newTypesArr.forEach(type => {
if (!newTypesTextArr.includes(type.getText())) {
newTypesNodeArr.push(type);
newTypesTextArr.push(type.getText());
}
});
return newTypesNodeArr;
}
* 遍历处理tsnode节点
* @param {context} 解下过后的内容
* @param {node} 解下过后的节点
* @returns ts.node
*/
function processVisitEachChild1(context, node) {
return ts.visitEachChild(node, processAllNodes, context);
function processAllNodes(node) {
if (ts.isTypeReferenceNode(node) && ts.isIdentifier(node.typeName) && typeArray.includes(node.typeName.getText())) {
node = ts.factory.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword);
}
if (ts.isStructDeclaration(node)) {
node = processStructDeclaration(node);
}
return ts.visitEachChild(node, processAllNodes, context);
};
}
* 处理struct子节点
*/
function processStructDeclaration(node) {
const newMembers = [];
node.members.forEach((member, index) => {
if (index >= 1) {
newMembers.push(member);
}
});
node = ts.factory.updateStructDeclaration(
node,
node.modifiers,
node.name,
node.typeParameters,
node.heritageClauses,
newMembers
);
return node;
}
* 遍历处理tsnode节点去重
* @param {context} 解下过后的内容
* @param {node} 解下过后的节点
* @returns ts.node
*/
function processVisitEachChild2(context, node) {
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
return ts.visitEachChild(node, processAllNodes, context);
function processAllNodes(node) {
if (ts.isUnionTypeNode(node)) {
node = apiDuplicateRemoval(node);
}
if (ts.isStructDeclaration(node)) {
node = processStructDeclaration(node);
}
return ts.visitEachChild(node, processAllNodes, context);
};
function apiDuplicateRemoval(node) {
const types = node.types;
const newTypes = [];
const newTypesText = new Set([]);
types.forEach(type => {
const text = printer.printNode(ts.EmitHint.Unspecified, type);
if (!newTypesText.has(text)) {
newTypes.push(type);
newTypesText.add(text);
}
});
node = ts.factory.updateUnionTypeNode(node, newTypes);
return node;
}
}
* 将输入路径替换为输出路径
* @param {string} inputFilePath 输入文件的绝对路径
* @param {string} inputDir 输入目录的绝对路径
* @param {string} outputDir 输出目录的绝对路径
* @returns {string} 输出文件的绝对路径
*/
function replaceInputDirWithOutputDir(inputFilePath, inputDir, outputDir) {
return inputFilePath.replace(inputDir, outputDir);
}
* 确保目录结构存在,如果不存在则创建
* @param {string} filePath 文件路径
*/
function ensureDirectoryExists(filePath) {
try {
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
} catch (error) {
console.error(`Error creating directory: ${error.message}`);
}
}
main();