From 6d4001463a5294847432e54f3142785ee6326422 Mon Sep 17 00:00:00 2001 From: yangbo_404 Date: Fri, 4 Jul 2025 14:27:44 +0800 Subject: [PATCH] update common utils Signed-off-by: yangbo_404 --- BUILD.gn | 29 + build-tools/babel.config.js | 40 + .../api-check-wrapper/index.ts | 17 + .../utils/api_check_wrapper_typedef.ts | 93 ++ .../utils/api_check_plugin_typedef.ts | 123 +++ .../utils/api_check_plugin_utils.ts | 902 ++++++++++++++++++ build-tools/package.json | 11 +- build_api_check_plugin.py | 74 ++ 8 files changed, 1287 insertions(+), 2 deletions(-) create mode 100644 build-tools/babel.config.js create mode 100755 build_api_check_plugin.py diff --git a/BUILD.gn b/BUILD.gn index d5d74aba31..b1c59a4421 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -375,3 +375,32 @@ ohos_copy("ohos_ets_api") { part_name = "sdk" subsystem_name = "sdk" } + +action("gen_api_check_plugin") { + deps = [ + "//developtools/ace_ets2bundle/arkui-plugins:ui_plugin" + ] + npm_path = "//prebuilts/build-tools/common/nodejs/current/bin/npm" + script = "build_api_check_plugin.py" + args = [ + "--source_path", + rebase_path(get_path_info("./build-tools", "abspath")), + "--output_path", + rebase_path("$target_gen_dir"), + "--npm", + rebase_path(npm_path), + ] + outputs = [ "$target_gen_dir" ] +} + +ohos_copy("api_check_plugin") { + deps = [ + ":gen_api_check_plugin" + ] + sources = [ + rebase_path("$target_gen_dir") + ] + outputs = [ target_out_dir + "/$target_name" ] + module_source_dir = target_out_dir + "/$target_name" + module_install_name = "" +} diff --git a/build-tools/babel.config.js b/build-tools/babel.config.js new file mode 100644 index 0000000000..6d04bc9283 --- /dev/null +++ b/build-tools/babel.config.js @@ -0,0 +1,40 @@ +/* + * 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. + */ + +module.exports = function(api) { + api.cache(true); + + const presets = ['@babel/typescript']; + const plugins = [ + '@babel/plugin-transform-modules-commonjs', + '@babel/plugin-proposal-class-properties', + [ + '@babel/plugin-transform-arrow-functions', + { + spec: true + } + ] + ]; + const ignore = [ + '**/test/**', + '**/node_modules/**' + ]; + + return { + presets, + plugins, + ignore + }; +}; diff --git a/build-tools/compiler-plugins/api-check-plugin-static/api-check-wrapper/index.ts b/build-tools/compiler-plugins/api-check-plugin-static/api-check-wrapper/index.ts index e69de29bb2..00f3775610 100644 --- a/build-tools/compiler-plugins/api-check-plugin-static/api-check-wrapper/index.ts +++ b/build-tools/compiler-plugins/api-check-plugin-static/api-check-wrapper/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export * from './utils/api_check_wrapper_typedef'; +export * from './utils/api_check_wrapper_enums'; \ No newline at end of file diff --git a/build-tools/compiler-plugins/api-check-plugin-static/api-check-wrapper/utils/api_check_wrapper_typedef.ts b/build-tools/compiler-plugins/api-check-plugin-static/api-check-wrapper/utils/api_check_wrapper_typedef.ts index e69de29bb2..c9c0d88efb 100644 --- a/build-tools/compiler-plugins/api-check-plugin-static/api-check-wrapper/utils/api_check_wrapper_typedef.ts +++ b/build-tools/compiler-plugins/api-check-plugin-static/api-check-wrapper/utils/api_check_wrapper_typedef.ts @@ -0,0 +1,93 @@ +/* + * 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. + */ + +import { DiagnosticCategory } from "./api_check_wrapper_enums"; +import * as arkts from '@koalaui/libarkts'; + +/** + * ApiCheckWrapper服务,绑定校验规则 + */ +export interface ApiCheckWrapperServiceHost { + getJsDocNodeCheckedConfig: (currentFileName: string, symbolSourceFilePath: string) => JsDocNodeCheckConfig; + // getJsDocNodeConditionCheckedResult: (jsDocFileCheckedInfo: FileCheckModuleInfo, jsDocs: JSDoc[]) => ConditionCheckResult; + getFileCheckedModuleInfo: (containFilePath: string) => FileCheckModuleInfo; + pushLogInfo: (apiName: string, currentFilePath: string, currentAddress: CurrentAddress, logLevel: DiagnosticCategory, logMessage: string) => void; + collectImportInfo: (moduleName: string[], modulePath: string, currentFilePath: string) => void; +} + +export interface JsDocNodeCheckConfig { + nodeNeedCheck: boolean; + checkConfig: JsDocNodeCheckConfigItem[]; +} + +export interface JsDocNodeCheckConfigItem { + tagName: string[]; + message: string; + type: DiagnosticCategory; + tagNameShouldExisted: boolean; + checkValidCallback?: (jsDocs: JSDoc[], config: JsDocNodeCheckConfigItem) => boolean; +} + +/** + * JSDoc类型 + */ +export interface JSDoc { + peer: string; + records: JSDocTag[]; +} + +/** + * JSDoc标签类型 + */ +export interface JSDocTag { + name: string; + param: string; + comment: string; + peer: string; +} + +export interface CurrentAddress { + line: number; + column: number; +} + +export interface FileCheckModuleInfo { + currentFileName: string; + fileNeedCheck: boolean; +} + +export interface ConditionCheckResult { + valid: boolean; + type?: DiagnosticCategory; + message?: string; +} + +export interface ASTDeclaration extends arkts.AstNode { + kind: number; + pos: number; + end: number; + parent?: ASTDeclaration; + jsDoc?: JSDoc[]; +} + +export interface ASTIdentifier extends arkts.AstNode { + kind: number; + text: string; +} + +export interface ASTSourceFile extends arkts.AstNode { + fileName: string; + text: string; +} \ No newline at end of file diff --git a/build-tools/compiler-plugins/api-check-plugin-static/utils/api_check_plugin_typedef.ts b/build-tools/compiler-plugins/api-check-plugin-static/utils/api_check_plugin_typedef.ts index e69de29bb2..bd1a625a1e 100644 --- a/build-tools/compiler-plugins/api-check-plugin-static/utils/api_check_plugin_typedef.ts +++ b/build-tools/compiler-plugins/api-check-plugin-static/utils/api_check_plugin_typedef.ts @@ -0,0 +1,123 @@ +/* + * 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. + */ + +import { JSDoc, JsDocNodeCheckConfigItem } from "../api-check-wrapper"; +import { PermissionVaildTokenState } from "./api_check_plugin_enums"; + +// 定义 JSON 数据结构接口 +export interface WindowConfig { + designWidth: number; + autoDesignWidth: boolean; +} + +export interface FormConfig { + name: string; + displayName: string; + description: string; + src: string; // 目标字段 + uiSyntax: string; + window: WindowConfig; + colorMode: string; + isDynamic: boolean; + isDefault: boolean; + updateEnabled: boolean; + scheduledUpdateTime: string; + updateDuration: number; + defaultDimension: string; + supportDimensions: string[]; +} + +export interface ConfigSchema { + forms: FormConfig[]; +} + +/** + * 工程编译配置 + */ +export interface ProjectConfig { + bundleName: string; + moduleName: string; + cachePath: string; + aceModuleJsonPath: string; + compileMode: string; + permissions: ConfigPermission; + requestPermissions: string[]; + definePermissions: string[]; + projectRootPath: string; + isCrossplatform: boolean; + ignoreCrossplatformCheck: boolean; + bundleType: string; + compileSdkVersion: number; + compatibleSdkVersion: number; + projectPath: string; + aceProfilePath: string; + cardPageSet: string[]; + compileSdkPath: string; + systemModules: string[]; + allModulesPaths: string[]; + sdkConfigs: SdkConfig[]; + externalApiPaths: string; + externalSdkPaths: string[]; + sdkConfigPrefix: string; + deviceTypes: string[]; + deviceTypesMessage: string; + runtimeOS: string; + syscapIntersectionSet: Set; + syscapUnionSet: Set; + permissionsArray: string[]; + buildSdkPath: string; + nativeDependencies: string[]; + aceSoPath: string; +} + +export interface CheckValidCallbackInterface { + (jsDocTag: JSDoc[], config: JsDocNodeCheckConfigItem): boolean; +} + +export interface SyscapConfig { + sysCaps: string[] +} + +export interface SdkConfig { + prefix: string; + apiPath: string[]; +} + +export interface GlobalObject { + projectConfig: ProjectConfig +} + +export interface PermissionVaildCalcInfo { + valid: boolean; + currentToken: PermissionVaildTokenState; + finish: boolean; + currentPermissionMatch: boolean; +} + +export interface PermissionValidCalcGroup { + subQueue: string[]; + includeParenthesis: boolean; +} + +export interface PermissionModule { + modulePath: string; + testPermissions: string[]; + permissions: string[]; +} + +export interface ConfigPermission { + requestPermissions: Array<{ name: string }>; + definePermissions: Array<{ name: string }>; +} \ No newline at end of file diff --git a/build-tools/compiler-plugins/api-check-plugin-static/utils/api_check_plugin_utils.ts b/build-tools/compiler-plugins/api-check-plugin-static/utils/api_check_plugin_utils.ts index e69de29bb2..8d6772a9c1 100644 --- a/build-tools/compiler-plugins/api-check-plugin-static/utils/api_check_plugin_utils.ts +++ b/build-tools/compiler-plugins/api-check-plugin-static/utils/api_check_plugin_utils.ts @@ -0,0 +1,902 @@ +/* + * 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. + */ + +import fs from 'fs'; +import path from 'path'; +import { globalObject } from '../index'; +import { + CheckValidCallbackInterface, + ConfigPermission, + ConfigSchema, + PermissionValidCalcGroup, + PermissionVaildCalcInfo, + ProjectConfig, + SdkConfig, + SyscapConfig +} from './api_check_plugin_typedef'; +import { + MESSAGE_CONFIG_COLOR_ERROR, + MESSAGE_CONFIG_COLOR_RED, + MESSAGE_CONFIG_COLOR_RESET, + MESSAGE_CONFIG_COLOR_WARNING, + MESSAGE_CONFIG_HEADER_ERROR, + MESSAGE_CONFIG_HEADER_WARNING, + PERMISSION_TAG_CHECK_ERROR, + PERMISSION_TAG_CHECK_NAME, + RUNTIME_OS_OH, + SINCE_TAG_CHECK_ERROER, + SINCE_TAG_NAME, + STAGE_COMPILE_MODE, + SYSCAP_TAG_CHECK_NAME +} from './api_check_plugin_define'; +import { + CurrentAddress, + DiagnosticCategory, + JSDoc, + JsDocNodeCheckConfigItem, + JSDocTag +} from '../api-check-wrapper'; +import { PermissionVaildTokenState } from './api_check_plugin_enums'; + +/** + * 从 JSON 文件中提取所有 src 字段到数组 + * @param filePath JSON 文件的绝对路径 + * @returns 包含所有 src 字段的字符串数组 + * @throws 文件不存在、JSON 解析错误或数据结构不符时抛出异常 + */ +export function extractSrcPaths(filePath: string): string[] { + // 1. 验证路径格式和存在性 + if (!path.isAbsolute(filePath)) { + throw new Error(`路径必须是绝对路径: ${filePath}`); + } + + if (!fs.existsSync(filePath)) { + throw new Error(`文件不存在: ${filePath}`); + } + + try { + // 2. 读取并解析 JSON 文件 + const rawData = fs.readFileSync(filePath, 'utf-8'); + const config: ConfigSchema = JSON.parse(rawData); + + // 3. 验证数据结构 + if (!config.forms || !Array.isArray(config.forms)) { + throw new Error('JSON 缺少 forms 数组'); + } + + // 4. 提取所有 src 字段 + const srcPaths: string[] = []; + for (const form of config.forms) { + if (form.src && typeof form.src === 'string') { + let src = form.src.replace(/^\.\/ets/, ''); + srcPaths.push(globalObject.projectConfig?.projectPath + src); + } else { + console.warn(`跳过无效 src 字段的表单项: ${form.name}`); + } + } + + // 5. 返回结果数组 + return srcPaths; + } catch (error) { + // 6. 增强错误信息 + if (error instanceof SyntaxError) { + throw new SyntaxError(`JSON 解析错误: ${error.message}`); + } + throw new Error(`处理失败: ${error instanceof Error ? error.message : String(error)}`); + } +} + +export function isCardFile(file: string): boolean { + if (globalObject.projectConfig.cardPageSet.includes(file)) { + return true; + } + return false; +} + +/** + * 校验since标签,当前api版本是否小于等于compatibleSdkVersion + * + * @param { JSDoc[] } jsDocs + * @param config + * @returns + */ +export function checkSinceTag(jsDocs: JSDoc[], config: JsDocNodeCheckConfigItem): boolean { + if (jsDocs && jsDocs.length > 0) { + const minorJSDocVersion: number = getJSDocMinorVersion(jsDocs); + const compatibleSdkVersion: number = globalObject.projectConfig.compatibleSdkVersion; + if (minorJSDocVersion > compatibleSdkVersion) { + config.message = SINCE_TAG_CHECK_ERROER.replace('$SINCE1', minorJSDocVersion.toString()) + .replace('$SINCE2', compatibleSdkVersion.toString()); + return true; + } + } + return false; +} + +/** + * 获取版本号最小的JSDoc + * @param { JSDoc[] } jsDocs + * @returns { number } + */ +function getJSDocMinorVersion(jsDocs: JSDoc[]): number { + let minorVersion: number = 0; + if (jsDocs && jsDocs.length > 0) { + for (let i = 0; i < jsDocs.length; i++) { + const jsdoc: JSDoc = jsDocs[i]; + if (jsdoc.records && jsdoc.records.length > 0) { + for (let j = 0; j < jsdoc.records.length; j++) { + const tag: JSDocTag = jsdoc.records[j]; + if (tag.name === SINCE_TAG_NAME) { + const currentVersion: number = Number.parseInt(tag.comment); + if (minorVersion === 0 || + !Number.isNaN(currentVersion) && currentVersion > minorVersion) { + minorVersion = currentVersion; + } + break; + } + } + } + } + } + return minorVersion; +} + +/** + * 获取最新版本的JSDoc + * @param { JSDoc[] } jsDocs + * @returns { JSDoc } + */ +function getCurrentJSDoc(jsDocs: JSDoc[]): JSDoc { + let minorVersion: number = 0; + let currentJsDoc: JSDoc = jsDocs[0]; + if (jsDocs && jsDocs.length > 0) { + for (let i = 0; i < jsDocs.length; i++) { + const jsdoc: JSDoc = jsDocs[i]; + if (jsdoc.records && jsdoc.records.length > 0) { + for (let j = 0; j < jsdoc.records.length; j++) { + const tag: JSDocTag = jsdoc.records[j]; + if (tag.name === SINCE_TAG_NAME) { + const currentVersion: number = Number.parseInt(tag.comment); + if (!Number.isNaN(currentVersion) && minorVersion > currentVersion) { + minorVersion = currentVersion; + currentJsDoc = jsdoc; + } + break; + } + } + } + } + } + return currentJsDoc; +} + +/** + * 获取最新版本的JSDoc + * @param { JSDoc } jsDoc + * @param { string } tagName + * @returns { JSDocTag | undefined } + */ +function getJSDocTag(jsDoc: JSDoc, tagName: string): JSDocTag | undefined { + const jsDocTag: JSDocTag | undefined = jsDoc.records.find((item: JSDocTag) => { + return item.name === tagName; + }); + return jsDocTag; +} + +/** + * STER1. Parse the permission information configured on the API + * STEP2. Recursive queue to obtain whether the current permission configuration supports it + */ +function validPermission(comment: string, permissionsArray: string[]): boolean { + const permissionsItem: string[] = getSplitsArrayWithDesignatedCharAndStr(comment ?? '', ' ') + .filter((item) => { + return item !== ''; + }); + const permissionsQueue: string[] = []; + permissionsItem.forEach((item: string) => { + //STEP1.1 Parse'(' + const leftParenthesisItem: string[] = getSplitsArrayWithDesignatedCharAndArrayStr([item], '('); + //STEP1.2 Parse')' + const rightParenthesisItem: string[] = getSplitsArrayWithDesignatedCharAndArrayStr(leftParenthesisItem, ')'); + permissionsQueue.push(...rightParenthesisItem); + }); + //STEP2 + const calcValidResult: PermissionVaildCalcInfo = { + valid: false, + currentToken: PermissionVaildTokenState.Init, + finish: false, + currentPermissionMatch: true, + }; + validPermissionRecursion(permissionsQueue, permissionsArray, calcValidResult); + return calcValidResult.valid; +} + +function validPermissionRecursion(permissionsQueue: string[], permissions: string[], + calcValidResult: PermissionVaildCalcInfo): void { + if (permissionsQueue.some(item => ['(', ')'].includes(item))) { + const groups: PermissionValidCalcGroup[] = groupWithParenthesis(permissionsQueue); + const groupJoin: string[] = getGroupItemPermission(groups, calcValidResult, permissions); + getPermissionVaildAtoms(groupJoin, calcValidResult, permissions ?? []); + } else { + getPermissionVaildAtoms(permissionsQueue, calcValidResult, permissions ?? []); + } +} + +function getSplitsArrayWithDesignatedCharAndStr(permission: string, designatedChar: string): string[] { + return permission.split(designatedChar).map(item => item.trim()); +} + +function getGroupItemPermission( + groups: PermissionValidCalcGroup[], + calcValidResult: PermissionVaildCalcInfo, + permissions: string[]): string[] { + const groupJoin: string[] = []; + groups.forEach((groupItem: PermissionValidCalcGroup) => { + if (groupItem.includeParenthesis) { + const calcValidResultItem: PermissionVaildCalcInfo = { + ...calcValidResult, + }; + const subStack: string[] = groupItem.subQueue.slice(1, groupItem.subQueue.length - 1); + validPermissionRecursion(subStack, permissions, calcValidResultItem); + if (calcValidResultItem.valid) { + groupJoin.push(''); + } else { + groupJoin.push('NA'); + } + } else { + groupJoin.push(...groupItem.subQueue); + } + }); + return groupJoin; +} + +function groupWithParenthesis(stack: string[]): PermissionValidCalcGroup[] { + let currentLeftParenthesisCount: number = 0; + const groups: PermissionValidCalcGroup[] = []; + let currentGroupItem: PermissionValidCalcGroup = { + subQueue: [], + includeParenthesis: false, + }; + stack.forEach((item: string, index: number) => { + if (item === '(') { + if (currentLeftParenthesisCount === 0) { + groups.push(currentGroupItem); + currentGroupItem = { + subQueue: [item], + includeParenthesis: true + }; + } else { + currentGroupItem.subQueue.push(item); + } + currentLeftParenthesisCount++; + } else if (item === ')') { + currentLeftParenthesisCount--; + currentGroupItem.subQueue.push(item); + if (currentLeftParenthesisCount === 0) { + groups.push(currentGroupItem); + currentGroupItem = { + subQueue: [], + includeParenthesis: false, + }; + } + } else { + currentGroupItem.subQueue.push(item); + if (index === stack.length - 1) { + groups.push(currentGroupItem); + } + } + }); + return groups; +} + +function getPermissionVaildAtoms(atomStacks: string[], calcValidResult: PermissionVaildCalcInfo, + configPermissions: string[]): void { + if (calcValidResult.finish) { + return; + } + if (atomStacks[0] === 'and') { + calcValidResult.currentToken = PermissionVaildTokenState.And; + } else if (atomStacks[0] === 'or') { + calcValidResult.currentToken = PermissionVaildTokenState.Or; + } else { + if (calcValidResult.currentToken === PermissionVaildTokenState.Or) { + if (inValidOrExpression( + atomStacks, + calcValidResult, + configPermissions + )) { + calcValidResult.currentPermissionMatch = false; + } + } else if (calcValidResult.currentToken === PermissionVaildTokenState.And) { + if (inValidAndExpression( + atomStacks, + calcValidResult, + configPermissions + )) { + calcValidResult.currentPermissionMatch = false; + } + } else { + calcValidResult.currentPermissionMatch = + validPermissionItem(atomStacks[0], configPermissions); + } + } + if (atomStacks.length > 1) { + getPermissionVaildAtoms( + atomStacks.slice(1), + calcValidResult, + configPermissions + ); + } else { + calcValidResult.valid = calcValidResult.currentPermissionMatch; + calcValidResult.finish = true; + } +} + +function inValidOrExpression( + atomStacks: string[], + calcValidResult: PermissionVaildCalcInfo, + configPermissions: string[]): boolean { + if ( + !calcValidResult.currentPermissionMatch && + !validPermissionItem(atomStacks[0], configPermissions) + ) { + calcValidResult.valid = false; + return true; + } + calcValidResult.currentPermissionMatch = true; + return false; +} + +function inValidAndExpression( + atomStacks: string[], + calcValidResult: PermissionVaildCalcInfo, + configPermissions: string[]): boolean { + if ( + !calcValidResult.currentPermissionMatch || + !validPermissionItem(atomStacks[0], configPermissions) + ) { + calcValidResult.valid = false; + return true; + } + calcValidResult.currentPermissionMatch = + validPermissionItem(atomStacks[0], configPermissions); + return false; +} + +function validPermissionItem(atomStackItem: string, configPermissions: string[]): boolean { + return atomStackItem === '' || configPermissions.includes(atomStackItem); +} + +function getSplitsArrayWithDesignatedCharAndArrayStr( + leftParenthesisItems: string[], + designatedChar: string +): string[] { + const rightParenthesisItems: string[] = []; + leftParenthesisItems.forEach((leftParenthesisItem: string) => { + if (leftParenthesisItem.includes(designatedChar)) { + const rightParenthesis: string[] = + getSplitsArrayWithDesignatedCharAndStr( + leftParenthesisItem, + designatedChar + ); + rightParenthesis.forEach((item: string) => { + if (item === '') { + rightParenthesisItems.push(designatedChar); + } else { + rightParenthesisItems.push(item); + } + }); + } else { + rightParenthesisItems.push(leftParenthesisItem); + } + }); + return rightParenthesisItems; +} + +/** +* get jsDocNodeCheckConfigItem object +* +* @param {string[]} tagName - tag name +* @param {string} message - error message +* @param {DiagnosticCategory} type - error type +* @param {boolean} tagNameShouldExisted - tag is required +* @param {CheckValidCallbackInterface} checkValidCallback +* @returns {JsDocNodeCheckConfigItem} +*/ +export function getJsDocNodeCheckConfigItem(tagName: string[], message: string, type: DiagnosticCategory, + tagNameShouldExisted: boolean, checkValidCallback?: CheckValidCallbackInterface): JsDocNodeCheckConfigItem { + return { + tagName: tagName, + message: message, + type: type, + tagNameShouldExisted: tagNameShouldExisted, + checkValidCallback: checkValidCallback + }; +} + +/** + * 创建/清空工程配置 + * @param { ProjectConfig } projectConfig + * @returns { ProjectConfig } + */ +export function createOrCleanProjectConfig(): ProjectConfig { + return { + bundleName: '', + moduleName: '', + cachePath: '', + aceModuleJsonPath: '', + compileMode: '', + permissions: { + requestPermissions: [], + definePermissions: [] + }, + requestPermissions: [], + definePermissions: [], + projectRootPath: '', + isCrossplatform: false, + ignoreCrossplatformCheck: false, + bundleType: '', + compileSdkVersion: 0, + compatibleSdkVersion: 0, + projectPath: '', + aceProfilePath: '', + cardPageSet: [], + compileSdkPath: '', + systemModules: [], + allModulesPaths: [], + sdkConfigs: [], + externalApiPaths: '', + externalSdkPaths: [], + sdkConfigPrefix: '', + deviceTypes: [], + deviceTypesMessage: '', + runtimeOS: '', + syscapIntersectionSet: new Set([]), + syscapUnionSet: new Set([]), + permissionsArray: [], + buildSdkPath: '', + nativeDependencies: [], + aceSoPath: '' + }; +} + +/** + * 初始化工程配置 + * @param { ProjectConfig } projectConfig + */ +export function initProjectConfig(): void { + // 绑定cardPageSet + readCardPageSet(); + // 绑定systemModules + readSystemModules(); + // 绑定syscap + readSyscapInfo(); + // 绑定permission + readPermissions(); +} + +/** + * read permissionInfo to this.share.projectConfig + */ +export function readPermissions(): void { + const permission: ConfigPermission = globalObject.projectConfig.permissions; + if (permission.requestPermissions) { + globalObject.projectConfig.requestPermissions = getPermissionFromConfig(permission.requestPermissions); + } + if (permission.definePermissions) { + globalObject.projectConfig.definePermissions = getPermissionFromConfig(permission.definePermissions); + } + globalObject.projectConfig.permissionsArray = + [...globalObject.projectConfig.requestPermissions, ...globalObject.projectConfig.definePermissions]; +} + +function getPermissionFromConfig(array: Array<{ name: string }>): string[] { + return array.map((item: { name: string }) => { + return String(item.name); + }); +} + +function readSystemModules() { + const apiDirPath = path.resolve(globalObject.projectConfig.buildSdkPath, './api'); + const arktsDirPath = path.resolve(globalObject.projectConfig.buildSdkPath, './arkts'); + const kitsDirPath = path.resolve(globalObject.projectConfig.buildSdkPath, './kits'); + const systemModulePathArray = [apiDirPath, arktsDirPath, kitsDirPath]; + + systemModulePathArray.forEach(systemModulesPath => { + if (fs.existsSync(systemModulesPath)) { + const modulePaths = []; + readFile(systemModulesPath, modulePaths); + globalObject.projectConfig.systemModules.push(...fs.readdirSync(systemModulesPath)); + modulePaths.filter(filePath => { + const dirName = path.dirname(filePath); + return !(dirName === apiDirPath || dirName === arktsDirPath || dirName === kitsDirPath); + }).map((filePath: string) => { + return filePath + .replace(apiDirPath, '') + .replace(arktsDirPath, '') + .replace(kitsDirPath, '') + .replace(/(^\\)|(.d.e?ts$)/g, '') + .replace(/\\/g, '/'); + }); + globalObject.projectConfig.allModulesPaths.push(...modulePaths); + } + }); + const defaultSdkConfigs: SdkConfig[] = [ + { + 'apiPath': systemModulePathArray, + 'prefix': '@ohos' + }, { + 'apiPath': systemModulePathArray, + 'prefix': '@system' + }, { + 'apiPath': systemModulePathArray, + 'prefix': '@arkts' + } + ]; + const externalApiPathStr = globalObject.projectConfig.externalApiPaths || ''; + const externalApiPaths = externalApiPathStr.split(path.delimiter); + globalObject.projectConfig.externalSdkPaths = [...externalApiPaths]; + const extendSdkConfigs: SdkConfig[] = []; + collectExternalModules(externalApiPaths, extendSdkConfigs); + globalObject.projectConfig.sdkConfigs = [...defaultSdkConfigs, ...extendSdkConfigs]; +} + +function collectExternalModules(sdkPaths: string[], extendSdkConfigs: SdkConfig[]): void { + for (let i = 0; i < sdkPaths.length; i++) { + const sdkPath = sdkPaths[i]; + const sdkConfigPath = path.resolve(sdkPath, 'sdkConfig.json'); + if (!fs.existsSync(sdkConfigPath)) { + continue; + } + const sdkConfig: SdkConfig = JSON.parse(fs.readFileSync(sdkConfigPath, 'utf-8')); + if (!sdkConfig.apiPath) { + continue; + } + let externalApiPathArray: string[] = []; + if (Array.isArray(sdkConfig.apiPath)) { + externalApiPathArray = sdkConfig.apiPath; + } else { + externalApiPathArray.push(sdkConfig.apiPath); + } + const resolveApiPathArray: string[] = []; + externalApiPathArray.forEach((element: string) => { + const resolvePath: string = path.resolve(sdkPath, element); + resolveApiPathArray.push(resolvePath); + if (fs.existsSync(resolvePath)) { + const extrenalModulePaths = []; + globalObject.projectConfig.systemModules.push(...fs.readdirSync(resolvePath)); + readFile(resolvePath, extrenalModulePaths); + globalObject.projectConfig.allModulesPaths.push(...extrenalModulePaths); + } + }); + globalObject.projectConfig.sdkConfigPrefix += `|${sdkConfig.prefix.replace(/^@/, '')}`; + sdkConfig.apiPath = resolveApiPathArray; + extendSdkConfigs.push(sdkConfig); + } +} + +/** + * 根据配置读取卡片列表 + */ +function readCardPageSet(): void { + if (globalObject.projectConfig.aceModuleJsonPath && fs.existsSync(globalObject.projectConfig.aceModuleJsonPath)) { + globalObject.projectConfig.compileMode = STAGE_COMPILE_MODE; + const moduleJson: any = JSON.parse(fs.readFileSync(globalObject.projectConfig.aceModuleJsonPath).toString()); + const extensionAbilities: any = moduleJson?.module?.extensionAbilities; + if (extensionAbilities && extensionAbilities.length > 0) { + setCardPages(extensionAbilities); + } + } +} + +function setCardPages(extensionAbilities: any) { + if (extensionAbilities && extensionAbilities.length > 0) { + extensionAbilities.forEach((extensionAbility: any) => { + if (extensionAbility.type === 'form' && extensionAbility.metadata) { + extensionAbility.metadata.forEach((metadata: any) => { + if (metadata.resource) { + readCardResource(metadata.resource); + } + }); + } + }); + } +} + +function readCardResource(resource: string) { + const cardJsonFileName: string = `${resource.replace(/\$profile\:/, '')}.json`; + const modulePagePath: string = path.resolve(globalObject.projectConfig.aceProfilePath, cardJsonFileName); + if (fs.existsSync(modulePagePath)) { + const cardConfig: any = JSON.parse(fs.readFileSync(modulePagePath, 'utf-8')); + if (cardConfig.forms) { + cardConfig.forms.forEach((form: any) => { + readCardForm(form); + }); + } + } +} + +function readCardForm(form: any) { + if ((form.type && form.type === 'eTS') || (form.uiSyntax && form.uiSyntax === 'arkts')) { + const cardPath = path.resolve(globalObject.projectConfig.projectPath, '..', form.src); + if (cardPath && fs.existsSync(cardPath) && !globalObject.projectConfig.cardPageSet.includes(cardPath)) { + globalObject.projectConfig.cardPageSet.push(cardPath); + } + } +} + +/** + * 更新工程配置 + * @param { ProjectConfig } targetProjectConfig + * @param { ProjectConfig } newProjectConfig + */ +export function updateProjectConfig(newProjectConfig: ProjectConfig): void { + Object.assign(globalObject.projectConfig, { + bundleName: newProjectConfig.bundleName, + moduleName: newProjectConfig.moduleName, + cachePath: newProjectConfig.cachePath, + requestPermissions: newProjectConfig.requestPermissions, + projectRootPath: newProjectConfig.projectRootPath, + isCrossplatform: newProjectConfig.isCrossplatform, + ignoreCrossplatformCheck: newProjectConfig.ignoreCrossplatformCheck, + bundleType: newProjectConfig.bundleType, + compileSdkVersion: newProjectConfig.compileSdkVersion, + compatibleSdkVersion: newProjectConfig.compatibleSdkVersion, + projectPath: newProjectConfig.projectPath, + aceProfilePath: newProjectConfig.aceProfilePath, + buildSdkPath: newProjectConfig.buildSdkPath + }); +} + +export function readFile(dir: string, utFiles: string[]): void { + try { + const files: string[] = fs.readdirSync(dir); + files.forEach((element) => { + const filePath: string = path.join(dir, element); + const status: fs.Stats = fs.statSync(filePath); + if (status.isDirectory()) { + readFile(filePath, utFiles); + } else { + utFiles.push(filePath); + } + }); + } catch (e) { + console.error(MESSAGE_CONFIG_COLOR_RED, 'ArkTS ERROR: ' + e, MESSAGE_CONFIG_COLOR_RESET); + } +} + +/** +* Determine the necessity of permission check +* +* @param { JSDoc[] } jsDocs +* @param { JsDocNodeCheckConfigItem } config +* @returns { boolean } +*/ +export function checkPermissionTag(jsDocs: JSDoc[], config: JsDocNodeCheckConfigItem): boolean { + const currentJSDoc: JSDoc = getCurrentJSDoc(jsDocs); + const jsDocTag: JSDocTag | undefined = getJSDocTag(currentJSDoc, PERMISSION_TAG_CHECK_NAME); + if (!jsDocTag) { + return false; + } + config.message = PERMISSION_TAG_CHECK_ERROR.replace('$DT', jsDocTag.comment); + return jsDocTag.comment !== '' && !validPermission(jsDocTag.comment, globalObject.projectConfig.permissionsArray); +} + +/** + * Confirm compliance since + * Only major version can be passed in, such as "19"; + * major and minor version can be passed in, such as "19.1"; major minor and patch + * patch version can be passed in, such as "19.1.2" + * the major version be from 1-999 + * the minor version be from 0-999 + * the patch version be from 0-999 + * + * @param {string} since + * @return {boolean} + */ +function isCompliantSince(since: string): boolean { + return /^(?!0\d)[1-9]\d{0,2}(?:\.[1-9]\d{0,2}|\.0){0,2}$\d{0,2}$/.test(since); +} + +/** + * compare point version + * @param { string } firstVersion + * @param { string } secondVersion + * @returns { number } + */ +function comparePointVersion(firstVersion: string, secondVersion: string): number { + const firstPointVersion = firstVersion.split('.'); + const secondPointVersion = secondVersion.split('.'); + for (let i = 0; i < 3; i++) { + const part1 = parseInt(firstPointVersion[i] || '0', 10); + const part2 = parseInt(secondPointVersion[i] || '0', 10); + if (part1 < part2) { + return -1; + } + if (part1 > part2) { + return 1; + } + } + return 0; +} + +/** + * Determine the necessity of syscap check. + * @param { JSDoc[] } jsDocs + * @param { JsDocNodeCheckConfigItem } config + * @returns { boolean } + */ +export function checkSyscapTag(jsDocs: JSDoc[], config: JsDocNodeCheckConfigItem): boolean { + let currentSyscapValue: string = ''; + if (jsDocs && jsDocs.length > 0) { + const jsDoc: JSDoc = getCurrentJSDoc(jsDocs); + for (let i = 0; i < jsDoc.records.length; i++) { + const jsDocTag: JSDocTag = jsDoc.records[i]; + if (jsDocTag && jsDocTag.name === SYSCAP_TAG_CHECK_NAME) { + currentSyscapValue = jsDocTag.comment; + break; + } + } + } + return globalObject.projectConfig.syscapIntersectionSet && !globalObject.projectConfig.syscapIntersectionSet.has(currentSyscapValue); +} + +/** + * read syscapInfo to projectConfig + */ +export function readSyscapInfo(): void { + globalObject.projectConfig.deviceTypesMessage = globalObject.projectConfig.deviceTypes.join(','); + const deviceDir: string = path.resolve(__dirname, '../../../../../api/device-define/'); + const deviceInfoMap: Map = new Map(); + const syscaps: Array = []; + let allSyscaps: string[] = []; + globalObject.projectConfig.deviceTypes.forEach((deviceType: string) => { + collectOhSyscapInfos(deviceType, deviceDir, deviceInfoMap); + }); + if (globalObject.projectConfig.runtimeOS !== RUNTIME_OS_OH) { + collectExternalSyscapInfos(globalObject.projectConfig.externalSdkPaths, globalObject.projectConfig.deviceTypes, + deviceInfoMap); + } + deviceInfoMap.forEach((value: string[]) => { + syscaps.push(value); + allSyscaps = allSyscaps.concat(value); + }); + const intersectNoRepeatTwice = (arrs: Array) => { + return arrs.reduce(function (prev: string[], cur: string[]) { + return Array.from(new Set(cur.filter((item: string) => { + return prev.includes(item); + }))); + }); + }; + let syscapIntersection: string[] = []; + if (globalObject.projectConfig.deviceTypes.length === 1 || syscaps.length === 1) { + syscapIntersection = syscaps[0]; + } else if (syscaps.length > 1) { + syscapIntersection = intersectNoRepeatTwice(syscaps); + } + globalObject.projectConfig.syscapIntersectionSet = new Set(syscapIntersection); + globalObject.projectConfig.syscapUnionSet = new Set(allSyscaps); +} + +function collectOhSyscapInfos(deviceType: string, deviceDir: string, deviceInfoMap: Map) { + let syscapFilePath: string = ''; + if (deviceType === 'phone') { + syscapFilePath = path.resolve(deviceDir, 'default.json'); + } else { + syscapFilePath = path.resolve(deviceDir, deviceType + '.json'); + } + if (fs.existsSync(syscapFilePath)) { + const content: SyscapConfig = JSON.parse(fs.readFileSync(syscapFilePath, 'utf-8')); + if (deviceInfoMap.get(deviceType)) { + deviceInfoMap.set(deviceType, (deviceInfoMap.get(deviceType) as string[]).concat(content.sysCaps)); + } else { + deviceInfoMap.set(deviceType, content.sysCaps); + } + } +} + +function collectExternalSyscapInfos( + externalApiPaths: string[], + deviceTypes: string[], + deviceInfoMap: Map +) { + const externalDeviceDirs: string[] = []; + externalApiPaths.forEach((externalApiPath: string) => { + const externalDeviceDir: string = path.resolve(externalApiPath, './api/device-define'); + if (fs.existsSync(externalDeviceDir)) { + externalDeviceDirs.push(externalDeviceDir); + } + }); + externalDeviceDirs.forEach((externalDeviceDir: string) => { + deviceTypes.forEach((deviceType: string) => { + let syscapFilePath: string = ''; + const files: string[] = fs.readdirSync(externalDeviceDir); + files.forEach((fileName: string) => { + if (fileName.startsWith(deviceType)) { + syscapFilePath = path.resolve(externalDeviceDir, fileName); + if (fs.existsSync(syscapFilePath)) { + const content: SyscapConfig = JSON.parse(fs.readFileSync(syscapFilePath, 'utf-8')); + if (deviceInfoMap.get(deviceType)) { + deviceInfoMap.set(deviceType, (deviceInfoMap.get(deviceType) as string[]).concat(content.sysCaps)); + } else { + deviceInfoMap.set(deviceType, content.sysCaps); + } + } + } + }); + }); + }); +} + +export function pushLog(apiName: string, currentFilePath: string, currentAddress: CurrentAddress, + logLevel: DiagnosticCategory, logMessage: string) { + // 组装文件全路径 + const fileFullPath: string = currentFilePath + `(${currentAddress.column}:${currentAddress.line}).`; + // 替换api名称 + logMessage = logMessage.replace('{0}', apiName); + // 打印日志信息 + printMessage(fileFullPath, logMessage, logLevel); +} + +/** + * 日志打印 + * @param fileInfo + * @param message + * @param level + */ +function printMessage(fileInfo: string, message: string, level: DiagnosticCategory) { + let messageHead: string = MESSAGE_CONFIG_HEADER_WARNING; + let messageColor: string = MESSAGE_CONFIG_COLOR_WARNING; + if (level === DiagnosticCategory.Error) { + messageHead = MESSAGE_CONFIG_HEADER_ERROR; + messageColor = MESSAGE_CONFIG_COLOR_ERROR; + } + // TODO: 待工具链日志输出方式确认后同步适配 + console.log(`%c${messageHead}${fileInfo}\n ${message}`, messageColor); +} + +export function collectInfo(moduleName: string[], modulePath: string, currentFilePath: string) { + // 收集so模块依赖 + if (/lib(\S+)\.so/g.test(modulePath) && !globalObject.projectConfig.nativeDependencies.includes(currentFilePath)) { + globalObject.projectConfig.nativeDependencies.push(currentFilePath); + } +} + +export function writeUseOSFiles(useOSFiles: string[]): void { + let info: string = useOSFiles.join('\n'); + if (!fs.existsSync(globalObject.projectConfig.aceSoPath)) { + const parent: string = path.resolve(globalObject.projectConfig.aceSoPath, '..'); + if (!(fs.existsSync(parent) && !fs.statSync(parent).isFile())) { + mkDir(parent); + } + } else { + const currentUseOSFiles: string[] = fs.readFileSync(globalObject.projectConfig.aceSoPath, 'utf-8').split('\n'); + useOSFiles.forEach((filePath: string) => { + if (!currentUseOSFiles.includes(filePath)) { + currentUseOSFiles.push(filePath); + } + }); + info = currentUseOSFiles.join('\n'); + } + fs.writeFileSync(globalObject.projectConfig.aceSoPath, info); +} + +function mkDir(path_: string): void { + const parent: string = path.join(path_, '..'); + if (!(fs.existsSync(parent) && !fs.statSync(parent).isFile())) { + mkDir(parent); + } + fs.mkdirSync(path_); +} \ No newline at end of file diff --git a/build-tools/package.json b/build-tools/package.json index 130a687306..9c031e5009 100644 --- a/build-tools/package.json +++ b/build-tools/package.json @@ -5,7 +5,8 @@ "main": "delete_systemapi_plugin.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "postinstall": "cd arkui_transformer && npm install" + "postinstall": "cd arkui_transformer && npm install", + "compile:plugins": "./node_modules/.bin/babel ./compiler-plugins/api-check-plugin-static --out-dir ./compiler-plugins/api-check-plugin-static/lib --extensions .ts" }, "author": "", "license": "ISC", @@ -13,6 +14,12 @@ "commander": "^13.1.0", "fs": "^0.0.1-security", "path": "^0.12.7", - "typescript": "npm:ohos-typescript@4.9.5-r5" + "typescript": "npm:ohos-typescript@4.9.5-r5", + "@babel/cli": "7.20.7", + "@babel/core": "7.20.12", + "@babel/plugin-proposal-class-properties": "7.18.6", + "@babel/preset-env": "7.20.2", + "@babel/preset-typescript": "7.18.6", + "@babel/runtime": "7.20.13" } } diff --git a/build_api_check_plugin.py b/build_api_check_plugin.py new file mode 100755 index 0000000000..8a77359a94 --- /dev/null +++ b/build_api_check_plugin.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# 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. + +import argparse +import os +import shutil +import subprocess +import sys +import tarfile + + +def copy_files(source_path, dest_path, is_file=False): + try: + if is_file: + os.makedirs(os.path.dirname(dest_path), exist_ok=True) + shutil.copy(source_path, dest_path) + else: + shutil.copytree(source_path, dest_path, dirs_exist_ok=True, + symlinks=True) + except Exception as err: + raise Exception("Copy files failed. Error: " + str(err)) from err + + +def run_cmd(cmd, execution_path=None): + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, + stdin=subprocess.PIPE, + stderr=subprocess.PIPE, + cwd=execution_path) + stdout, stderr = proc.communicate(timeout=1000) + if proc.returncode != 0: + raise Exception(stderr.decode()) + + +def build(options): + build_cmd = [options.npm, 'run', 'compile:plugins'] + run_cmd(build_cmd, options.source_path) + + +def copy_output(options): + copy_files(os.path.join(options.source_path, './compiler-plugins/api-check-plugin-static/lib'), + os.path.join(options.output_path, 'api-check-plugin')) + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument('--npm', help='path to a npm exetuable') + parser.add_argument('--source_path', help='path to api_check_plugin source') + parser.add_argument('--output_path', help='path to output') + + options = parser.parse_args() + return options + + +def main(): + options = parse_args() + + build(options) + copy_output(options) + + +if __name__ == '__main__': + sys.exit(main()) \ No newline at end of file -- Gitee