* Copyright (c) 2022-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.
*/
import * as arkts from '@koalaui/libarkts';
import { traverseProgram, getJSDocInformation } from '../utils/ts_wrapper_node_util';
import {
ApiCheckWrapperServiceHost,
JsDocNodeCheckConfig,
FileCheckModuleInfo,
JSDoc,
JSDocTag,
JsDocNodeCheckConfigItem,
CurrentAddress,
LegacyStructMap
} from '../utils/api_check_wrapper_typedef';
import { SINCE_TAG_NAME } from '../../utils/api_check_plugin_define';
import { parseJSDoc } from '../custom-plugins/custom-comment-parser';
import { globalObject } from '../../index';
import path from 'path';
import { getGlobalMonitor } from '../../utils/performance_monitor';
import { PERF } from '../../utils/perf_constants';
export let curApiCheckWrapper: ApiCheckWrapper;
export let curFileCheckModuleInfo: FileCheckModuleInfo;
* 导出arkts命名空间
*/
export const WrapperApi = {
...arkts
}
let checkedNode = new Map();
export class ApiCheckWrapper {
constructor(apiCheckHost: ApiCheckWrapperServiceHost) {
this.apiCheckHost = apiCheckHost;
}
setFileName(fileName: string): void {
this.fileName = fileName;
}
fileName: string = ''
sourcefile: string = ''
apiCheckHost: ApiCheckWrapperServiceHost
}
* API检查的入口逻辑,通过获取当前ets文件路径,设置参数,循环遍历节点。
*
* @param { ApiCheckWrapperServiceHost } apiCheckHost host对象,提供检查配置和工具方法
* @param { number | undefined } peer 上下文标识
*/
export function checkApiExpression(apiCheckHost: ApiCheckWrapperServiceHost, peer: arkts.KNativePointer | undefined): void {
const monitor = getGlobalMonitor();
monitor.start(PERF.CHECK_EXPR + '.total');
const contextPtr = arkts.arktsGlobal.compilerContext?.peer ?? peer;
if (contextPtr === null || contextPtr === undefined) {
monitor.end(PERF.CHECK_EXPR + '.total');
return;
}
curApiCheckWrapper = new ApiCheckWrapper(apiCheckHost);
curFileCheckModuleInfo = {} as FileCheckModuleInfo;
let fileNames: Map<arkts.KNativePointer, string> = new Map();
let legacyModuleList: string[] = [];
let legacyStructMap: Map<string, LegacyStructMap> = new Map();
monitor.start(PERF.GET_CONTEXT);
let program = arkts.getOrUpdateGlobalContext(contextPtr).program;
monitor.end(PERF.GET_CONTEXT);
const visited: Set<arkts.KNativePointer> = new Set();
const queue: arkts.Program[] = [program];
monitor.start(PERF.GET_LEGACY);
getLegacyModule(legacyStructMap, legacyModuleList);
monitor.end(PERF.GET_LEGACY);
monitor.start(PERF.TRAVERSE_FILES);
let fileCheckCount = 0;
while (queue.length > 0) {
const currProgram = queue.shift()!;
if (visited.has(currProgram.peer)) {
continue;
}
if (currProgram.peer !== program.peer) {
const name: string = fileNames.get(currProgram.peer)!;
if (currProgram.isBuiltSimultaneously && globalObject.projectConfig.compileFiles.includes(path.normalize(currProgram.sourceFilePath))) {
fileCheckCount++;
curApiCheckWrapper.fileName = currProgram.sourceFilePath;
curFileCheckModuleInfo.currentFileName = currProgram.fileName;
checkedNode = new Map();
traverseProgram(currProgram.ast as arkts.ETSModule);
}
}
visited.add(currProgram.peer);
for (const externalSource of currProgram.getExternalSources()) {
visitNextProgramInQueue(queue, visited, externalSource, fileNames);
}
}
monitor.end(PERF.TRAVERSE_FILES);
monitor.end(PERF.CHECK_EXPR + '.total');
}
* 从项目配置的依赖模块中收集需要验证的节点。
*
* @param { Map<string,LegacyStructMap> } legacyStructMap 用于存储需要验证的结构映射的Map(键为模块名)
* @param { string[] } legacyModuleList 用于存储需要验证的节点数组
*/
function getLegacyModule(legacyStructMap: Map<string, LegacyStructMap>, legacyModuleList: string[]): void {
const moduleList = globalObject.projectConfig?.dependentModuleList;
if (moduleList === undefined) {
return;
}
for (const module of moduleList) {
const moduleName = module.moduleName;
if (!legacyStructMap.has(moduleName)) {
legacyStructMap.set(moduleName, {});
legacyModuleList.push(moduleName);
}
}
}
* 匹配当前文件名是否需要验证。
*
* @param { (string | RegExp)[] } prefixCollection 前缀集合,元素可为字符串或正则表达式
* @param { string } name 需要检查的文件名
* @returns { boolean } 若文件名匹配任一前缀,返回true;否则返回false
*/
function matchPrefix(prefixCollection: (string | RegExp)[], name: string): boolean {
for (const prefix of prefixCollection) {
let regex: RegExp;
if (typeof prefix === 'string') {
regex = new RegExp('^' + prefix);
} else {
regex = new RegExp('^' + prefix.source);
}
if (regex.test(name)) {
return true;
}
}
return false;
}
* 将program节点加入遍历队列并记录文件名。
*
* @param { arkts.Program[] } queue 程序遍历队列,用于存储待处理的程序对象
* @param { Set<number> } visited 已访问程序的标识集合,用于去重
* @param { arkts.ExternalSource } externalSource 外部源对象,包含关联的程序
* @param { Map<number,string> } fileNames 用于存储程序标识与文件名映射的Map
*/
function visitNextProgramInQueue(
queue: arkts.Program[],
visited: Set<arkts.KNativePointer>,
externalSource: arkts.ExternalSource,
fileNames: Map<arkts.KNativePointer, string>
): void {
const nextProgramArr: readonly arkts.Program[] = externalSource.programs ?? [];
for (const nextProgram of nextProgramArr) {
fileNames.set(nextProgram.peer, externalSource.getName());
if (!visited.has(nextProgram.peer)) {
queue.push(nextProgram);
}
}
}
* 检查Identifier节点对应的声明是否符合API规范(基于JSDoc配置)。
*
* @param { arkts.AstNode } node 需要检查的标识符AST节点
*/
export function checkIdentifier(node: arkts.Identifier): void {
const monitor = getGlobalMonitor();
monitor.start(PERF.CHECK_ID);
monitor.start(PERF.GET_DECL);
const decl = arkts.getDecl(node);
monitor.end(PERF.GET_DECL);
if (decl === undefined || decl === null) {
monitor.end(PERF.CHECK_ID);
return;
}
let sysPath = getSysPath(decl);
if (sysPath === undefined || sysPath === null) {
monitor.end(PERF.CHECK_ID);
return;
}
monitor.start(PERF.GET_CHECK_CFG);
let checkPram: JsDocNodeCheckConfig = curApiCheckWrapper.apiCheckHost.getJsDocNodeCheckedConfig(
curFileCheckModuleInfo.currentFileName, sysPath);
monitor.end(PERF.GET_CHECK_CFG);
if (!checkPram.nodeNeedCheck) {
monitor.end(PERF.CHECK_ID);
return;
}
if (confirmNodeChecked(node.name, node.startPosition.getIndex())) {
monitor.end(PERF.CHECK_ID);
return;
}
monitor.start(PERF.CHECK_JSDOC);
expressionCheckByJsDoc(decl, node, checkPram.checkConfig);
monitor.end(PERF.CHECK_JSDOC);
monitor.end(PERF.CHECK_ID);
}
* 获取校验节点的行列信息,实现打印报错去重。
*
* @param { string } nodeName 标识符节点的名称
* @param { number } index 偏移量
*/
export function confirmNodeChecked(nodeName: string, index: number): boolean {
const nodeKey = `${curApiCheckWrapper.fileName}_${nodeName}_${index}`;
if (checkedNode.has(nodeKey) && checkedNode.get(nodeKey) !== undefined) {
return true;
} else {
checkedNode.set(nodeKey, nodeName);
return false;
}
}
* 获取声明节点的Api文件路径
*
* @param { arkts.AstNode } decl 声明节点
* @returns { string } Api文件路径
*/
function getSysPath(decl: arkts.AstNode): string {
let program = arkts.getProgramFromAstNode(decl);
return program?.sourceFilePath || '';
}
* 通过声明节点获取Jsdoc注释内容
*
* @param { arkts.AstNode } decl 声明节点
* @returns { string | undefined } 注释信息
*/
export function getPeerJsDocs(decl: arkts.AstNode): string | undefined {
return getJSDocInformation(decl);
}
* 遍历jsdoc信息,对AST节点进行规则校验并打印报错信息。
*
* @param { arkts.AstNode } declaration 声明节点
* @param { arkts.Identifier } identifier AST节点
* @param { CurrentAddress } address 当前文件地址
* @param { JsDocNodeCheckConfigItem[] } checkConfig 校验配置
*/
function expressionCheckByJsDoc(declaration: arkts.AstNode, identifier: arkts.Identifier, checkConfig: JsDocNodeCheckConfigItem[]): void {
const monitor = getGlobalMonitor();
monitor.start(PERF.PARSE_JSDOC);
const jsDocsString: string | undefined = getPeerJsDocs(declaration);
const jsDocs: JSDoc[] = parseJSDoc(jsDocsString);
monitor.end(PERF.PARSE_JSDOC);
monitor.start(PERF.GET_CURRENT_JSDOC);
const jsDocTags: JSDocTag[] = getCurrentJSDoc(jsDocs);
monitor.end(PERF.GET_CURRENT_JSDOC);
let address: CurrentAddress | undefined = undefined;
for (let i = 0; i < checkConfig.length; i++) {
const config: JsDocNodeCheckConfigItem = checkConfig[i];
let tagNameCheckNecessity = true;
if (config.checkJsDocSuppressorValidCallback) {
monitor.start(PERF.CHECK_CALLBACK);
tagNameCheckNecessity = config.checkJsDocSuppressorValidCallback(jsDocTags, config, identifier, declaration);
monitor.end(PERF.CHECK_CALLBACK);
}
if (!tagNameCheckNecessity) {
continue;
}
let tagNameExisted = false;
for (const item of jsDocTags) {
if (!config.tagName.includes(item.tag)) {
continue;
}
tagNameExisted = true;
if (!tagNameExisted || config.tagNameShouldExisted) {
continue;
}
if (address === undefined) {
monitor.start(PERF.GET_ADDRESS);
address = getCurrentAddressByNode(identifier);
monitor.end(PERF.GET_ADDRESS);
}
curApiCheckWrapper.apiCheckHost.pushLogInfo(
identifier.name,
curApiCheckWrapper.fileName,
address,
config.type,
config.message
);
break;
}
if (config.tagNameShouldExisted && !tagNameExisted) {
if (address === undefined) {
monitor.start(PERF.GET_ADDRESS);
address = getCurrentAddressByNode(identifier);
monitor.end(PERF.GET_ADDRESS);
}
curApiCheckWrapper.apiCheckHost.pushLogInfo(
identifier.name, curApiCheckWrapper.fileName,
address, config.type, config.message);
}
}
}
* 获取AST节点在源文件中的行列位置信息
*
* @param { arkts.AstNode } node 需要获取行列信息的节点
* @returns { CurrentAddress } 节点行列信息
*/
export function getCurrentAddressByNode(node: arkts.AstNode): CurrentAddress {
let address = {} as CurrentAddress;
let startPosition = node.startPosition;
address.column = startPosition.getCol();
address.line = startPosition.getLine() + 1;
return address;
}
* 通过比较JSDoc注释中@since标签的版本号,筛选出版本号最大的注释对象,
* 返回其包含的所有标签(JSDocTag),用于获取最新版本的API文档注释信息。
*
* @param { JSDoc[] } jsDocs JSDoc注释对象数组
* @returns { JSDocTag[] } 最新版本JSDoc注释中的标签数组;若无有效注释,返回空数组
*/
function getCurrentJSDoc(jsDocs: JSDoc[]): JSDocTag[] {
let jsDocTags: JSDocTag[] = [];
let maxVersion: string = '0';
if (!jsDocs || jsDocs.length === 0) {
return jsDocTags;
}
for (const jsdoc of jsDocs) {
if (!jsdoc.tags || jsdoc.tags.length === 0) {
continue;
}
for (const tag of jsdoc.tags) {
if (tag.tag !== SINCE_TAG_NAME) {
continue;
}
const currentVersion: string = (tag.name ?? tag.comment ?? '0').trim();
if (!currentVersion || currentVersion === '0') {
continue;
}
if (compareVersionStrings(currentVersion, maxVersion) > 0) {
maxVersion = currentVersion;
jsDocTags = jsdoc.tags;
}
}
}
return jsDocTags;
}
function compareVersionStrings(v1: string, v2: string): number {
const parseVersion = (v: string): number[] => {
const parts = v.split('.').map(p => parseInt(p, 10) || 0);
return [parts[0] || 0, parts[1] || 0, parts[2] || 0];
};
const p1 = parseVersion(v1);
const p2 = parseVersion(v2);
for (let i = 0; i < 3; i++) {
if (p1[i] > p2[i]) {
return 1;
}
if (p1[i] < p2[i]) {
return -1;
}
}
return 0;
}