From 11767d986a3aff1c2b960196764041068ffdfcce Mon Sep 17 00:00:00 2001 From: bulutcangocer Date: Tue, 19 Aug 2025 15:56:30 +0300 Subject: [PATCH 1/2] feat: added api version validator for some conditions Signed-off-by: bulutcangocer --- .../fast_build/system_api/api_check_utils.ts | 17 +- .../system_api/sdk_api_version_validator.ts | 278 ++++++++++++++++++ 2 files changed, 293 insertions(+), 2 deletions(-) create mode 100644 compiler/src/fast_build/system_api/sdk_api_version_validator.ts diff --git a/compiler/src/fast_build/system_api/api_check_utils.ts b/compiler/src/fast_build/system_api/api_check_utils.ts index 71be695cd..5da0d3e1c 100644 --- a/compiler/src/fast_build/system_api/api_check_utils.ts +++ b/compiler/src/fast_build/system_api/api_check_utils.ts @@ -73,6 +73,12 @@ import { VERSION_CHECK_FUNCTION_NAME } from './api_check_define'; import { JsDocCheckService } from './api_check_permission'; +import { SdkApiVersionValidator } from './sdk_api_version_validator'; + +export enum SdkApiType{ + HarmonyOS, + OpenHarmony +} /** * bundle info @@ -89,7 +95,7 @@ export interface CheckValidCallbackInterface { } export interface CheckJsDocSpecialValidCallbackInterface { - (jsDocTags: readonly ts.JSDocTag[], config: ts.JsDocNodeCheckConfigItem): boolean; + (jsDocTags: readonly ts.JSDocTag[], config: ts.JsDocNodeCheckConfigItem, node?: ts.Node): boolean; } export interface checkConditionValidCallbackInterface { @@ -549,9 +555,10 @@ function collectExternalSyscapInfos( * * @param jsDocTags * @param config + * @param node * @returns */ -function checkSinceValue(jsDocTags: readonly ts.JSDocTag[], config: ts.JsDocNodeCheckConfigItem): boolean { +function checkSinceValue(jsDocTags: readonly ts.JSDocTag[], config: ts.JsDocNodeCheckConfigItem, node?: ts.Node): boolean { if (!jsDocTags[0]?.parent?.parent || !projectConfig.compatibleSdkVersion) { return false; } @@ -567,6 +574,12 @@ function checkSinceValue(jsDocTags: readonly ts.JSDocTag[], config: ts.JsDocNode return false; } if (hasSince && comparePointVersion(compatibleSdkVersion.toString(), minSince) === -1) { + const checker: ts.TypeChecker | undefined = CurrentProcessFile.getChecker(); + const sdkValidator = new SdkApiVersionValidator(compatibleSdkVersion, checker); + + if (sdkValidator.isSdkApiVersionHandled(node, SdkApiType.HarmonyOS)) { + return false; + } config.message = SINCE_TAG_CHECK_ERROER.replace('$SINCE1', minSince).replace('$SINCE2', compatibleSdkVersion); return true; } diff --git a/compiler/src/fast_build/system_api/sdk_api_version_validator.ts b/compiler/src/fast_build/system_api/sdk_api_version_validator.ts new file mode 100644 index 000000000..2206cb59d --- /dev/null +++ b/compiler/src/fast_build/system_api/sdk_api_version_validator.ts @@ -0,0 +1,278 @@ +/* + * 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 ts from 'typescript'; +import { SdkApiType } from './api_check_utils'; + +/** + * The node is considered **valid** if it satisfies **at least one** of the following: + * 1. It is wrapped in a `try/catch` block. + * 2. It is wrapped in an `undefined` check. + * 3. It is wrapped in an SDK version comparison. + */ +export class SdkApiVersionValidator { + private readonly sdkOpenHarmonyPackageMap: Map = new Map([ + ['sdkApiVersion', ['@ohos.deviceInfo.d.ts']] + ]); + private readonly sdkHarmonOSPackageMap: Map = new Map([ + ['distributionOSApiVersion', ['@ohos.deviceInfo.d.ts']] + ]); + private readonly compatibleSdkVersion: string; + private readonly typeChecker?: ts.TypeChecker; + + constructor(projectCompatibleSdkVersion: string, typeChecker?: ts.TypeChecker) { + this.compatibleSdkVersion = projectCompatibleSdkVersion; + this.typeChecker = typeChecker; + } + + /** + * Checks whether a given node is valid for at least one condition. + * @param node - The AST node to check. + * @param sdkType - Calling API's type (HarmonyOS or OpenHarmony) + * @returns `true` if the node meets any of the handling rules, otherwise `false`. + */ + public isSdkApiVersionHandled(node: ts.Node, sdkType: SdkApiType): boolean { + if (!node) { + return false; + } + + return ( + this.isNodeWrappedInTryCatch(node) || + this.isNodeWrappedInUndefinedCheck(node) || + this.isNodeWrappedInSdkComparison(node, sdkType) + ); + } + + private isNodeWrappedInTryCatch(node: ts.Node): boolean { + return this.findParentNode(node, (parent) => { + if (ts.isTryStatement(parent)) { + return node.getStart() >= parent.tryBlock.getStart(); + } + return false; + }) !== null; + } + + private isNodeWrappedInUndefinedCheck(node: ts.Node): boolean { + const targetName = this.getPrimaryNameFromNode(node); + if (!targetName) { + return false; + } + + return this.findParentNode(node, (parent) => { + if (ts.isIfStatement(parent)) { + return this.isUndefinedCheckHelper(parent.expression, targetName); + } + return false; + }) !== null; + } + + private isUndefinedCheckHelper(expression: ts.Expression, name: string): boolean { + if (!ts.isBinaryExpression(expression)) { + return false; + } + + // Check if the operator is a "not equal" comparison (!== or !=) + const isNotEqualOperator = [ + ts.SyntaxKind.ExclamationEqualsEqualsToken, + ts.SyntaxKind.ExclamationEqualsToken + ].includes(expression.operatorToken.kind); + + if (!isNotEqualOperator) { + return false; + } + + const { left, right } = expression; + + // Determine if either side is the literal "undefined" + const isLeftUndefined = this.isUndefinedNode(left); + const isRightUndefined = this.isUndefinedNode(right); + + const isLeftTarget = this.isTargetNode(left, name); + const isRightTarget = this.isTargetNode(right, name); + + return (isLeftTarget && isRightUndefined) || (isLeftUndefined && isRightTarget); + } + + private isUndefinedNode(node: ts.Node): boolean { + return ts.isIdentifier(node) && node.text === 'undefined'; + } + + private isNodeWrappedInSdkComparison(node: ts.Node, sdkType: SdkApiType): boolean { + if (this.compatibleSdkVersion === '' || !this.typeChecker) { + return false; + } + + return this.findParentNode(node, (parent) => { + if (ts.isIfStatement(parent)) { + try { + return this.isSdkComparisonHelper(parent.expression, sdkType); + } catch { + return false; + } + } + return false; + }) !== null; + } + + private isSdkComparisonHelper(expression: ts.Expression, sdkType: SdkApiType): boolean { + const expressionText = expression.getText(); + let sdkApiToPackageMap: Map; + try { + sdkApiToPackageMap = this.getSdkPackageMap(sdkType); + } catch (err) { + return false; + } + + // Find matching SDK API entry based on whether the expression text contains the API name + const matchedEntry = Array.from(sdkApiToPackageMap.entries()) + .find(([api]) => expressionText.includes(api)); + + if (!matchedEntry) { + return false; + } + + const [matchedApi, validPackagePaths] = matchedEntry; + // Try to resolve the actual identifier used for this API in the expression + const apiIdentifier = this.findValidImportApiIdentifier(expression, matchedApi); + + // Validate that the identifier comes from one of the allowed SDK package paths + return apiIdentifier ? + this.isValidSdkDeclaration(apiIdentifier, validPackagePaths) : + false; + } + + private findValidImportApiIdentifier(expression: ts.Expression, api: string): ts.Identifier | undefined { + if (ts.isBinaryExpression(expression)) { + return this.extractApiIdentifierFromExpression(expression.left, api) || + this.extractApiIdentifierFromExpression(expression.right, api); + } + return this.extractApiIdentifierFromExpression(expression, api); + } + + private extractApiIdentifierFromExpression( + expression: ts.Expression, + targetProperty: string + ): ts.Identifier | undefined { + if (!ts.isPropertyAccessExpression(expression)) { + return undefined; + } + if (expression.name.text !== targetProperty) { + return undefined; + } + return this.getRootIdentifier(expression.expression); + } + + private getRootIdentifier(expression: ts.Expression): ts.Identifier | undefined { + let current: ts.Expression = expression; + while (ts.isPropertyAccessExpression(current)) { + current = current.expression; + } + return ts.isIdentifier(current) ? current : undefined; + } + + private isValidSdkDeclaration(identifier: ts.Identifier, validPackagePaths: string[]): boolean { + if (!this.typeChecker) { + return false; + } + const symbol = this.typeChecker.getSymbolAtLocation(identifier); + if (!symbol) { + return false; + } + const declarationFile = this.getActualDeclarationFile(symbol); + return declarationFile ? + this.isValidSdkDeclarationPath(declarationFile, validPackagePaths) : + false; + } + + private getActualDeclarationFile(symbol: ts.Symbol): string | undefined { + if (!this.typeChecker) { + return undefined; + } + const targetSymbol = this.typeChecker.getAliasedSymbol(symbol); + const actualSymbol = targetSymbol !== symbol ? targetSymbol : symbol; + if (!actualSymbol.declarations?.length) { + return undefined; + } + const declarationFile = actualSymbol.declarations[0].getSourceFile().fileName; + + // This regex removes the leading path up to and including the last "sdk//" or "sdk\\" + // before the "@", leaving only the part starting from the package name. + return declarationFile.replace(/^.*sdk[\\/].*[\\/](?=@)/, ''); + } + + private isValidSdkDeclarationPath(filePath: string, validPackagePaths: string[]): boolean { + const normalizedPath = this.normalizePath(filePath); + return validPackagePaths.some(validPath => + normalizedPath.includes(this.normalizePath(validPath)) + ); + } + + private normalizePath(path: string): string { + return path.replace(/\\/g, '/').toLowerCase(); + } + + private getSdkPackageMap(sdkType: SdkApiType): Map { + switch (sdkType) { + case SdkApiType.HarmonyOS: + return this.sdkHarmonOSPackageMap; + case SdkApiType.OpenHarmony: + return this.sdkOpenHarmonyPackageMap; + default: + throw new Error(`Unsupported SDK type: ${sdkType}`); + } + } + + /** + * Traverses upward in the AST from the given node to find the first parent + * that satisfies the provided predicate function. + * + * @param node - The starting AST node. + * @param predicate - A function that returns `true` for the desired parent node. + * @returns The first matching parent node, or `null` if none is found. + */ + private findParentNode( + node: ts.Node, + predicate: (parent: ts.Node) => boolean + ): ts.Node | null { + let currentNode = node.parent; + + // Walk up the AST until we reach the root or find a match + while (currentNode) { + if (predicate(currentNode)) { + return currentNode; + } + currentNode = currentNode.parent; + } + return null; + } + + private getPrimaryNameFromNode(node: ts.Node): string | undefined { + if (ts.isIdentifier(node)) { + return node.text; + } + if (ts.isCallExpression(node)) { + return this.getPrimaryNameFromNode(node.expression); + } + if (ts.isPropertyAccessExpression(node)) { + return node.name.text; + } + return undefined; + } + + private isTargetNode(node: ts.Node, name: string): boolean { + const nodePrimaryName = this.getPrimaryNameFromNode(node); + return nodePrimaryName === name; + } +} -- Gitee From a997d1c8c98b06d681eb91e064c379a7b5de4595 Mon Sep 17 00:00:00 2001 From: bulutcangocer Date: Fri, 29 Aug 2025 15:19:34 +0300 Subject: [PATCH 2/2] feat: added close source device check with close source api Signed-off-by: bulutcangocer --- compiler/main.js | 118 ++++++----- .../fast_build/system_api/api_check_utils.ts | 197 +++++++++++------- .../system_api/sdk_api_version_validator.ts | 151 +++++++++----- 3 files changed, 291 insertions(+), 175 deletions(-) diff --git a/compiler/main.js b/compiler/main.js index c1e72c927..0e35c2456 100644 --- a/compiler/main.js +++ b/compiler/main.js @@ -72,6 +72,8 @@ let sdkConfigPrefix = 'ohos|system|kit|arkts'; let ohosSystemModulePaths = []; let ohosSystemModuleSubDirPaths = []; let allModulesPaths = []; +let externalApiCheckPlugin = new Map(); + function initProjectConfig(projectConfig) { initProjectPathConfig(projectConfig); @@ -107,7 +109,7 @@ function initProjectConfig(projectConfig) { projectConfig.optTryCatchFunc = true; // All files which dependent on bytecode har, and should be added to compilation entries. projectConfig.otherCompileFiles = {}; - // Packages which need to update version in bytecode har + // Packages which need to update version in bytecode har projectConfig.updateVersionInfo = undefined; projectConfig.allowEmptyBundleName = false; projectConfig.uiTransformOptimization = false; @@ -116,20 +118,20 @@ function initProjectConfig(projectConfig) { function initProjectPathConfig(projectConfig) { projectConfig.projectPath = projectConfig.projectPath || process.env.aceModuleRoot || - path.join(process.cwd(), 'sample'); + path.join(process.cwd(), 'sample'); projectConfig.buildPath = projectConfig.buildPath || process.env.aceModuleBuild || - path.resolve(projectConfig.projectPath, 'build'); + path.resolve(projectConfig.projectPath, 'build'); projectConfig.aceModuleBuild = projectConfig.buildPath; // To be compatible with both webpack and rollup projectConfig.manifestFilePath = projectConfig.manifestFilePath || process.env.aceManifestPath || - path.join(projectConfig.projectPath, 'manifest.json'); + path.join(projectConfig.projectPath, 'manifest.json'); projectConfig.aceProfilePath = projectConfig.aceProfilePath || process.env.aceProfilePath; projectConfig.aceModuleJsonPath = projectConfig.aceModuleJsonPath || process.env.aceModuleJsonPath; projectConfig.aceSuperVisualPath = projectConfig.aceSuperVisualPath || - process.env.aceSuperVisualPath; + process.env.aceSuperVisualPath; projectConfig.hashProjectPath = projectConfig.hashProjectPath || - hashProjectPath(projectConfig.projectPath); + hashProjectPath(projectConfig.projectPath); projectConfig.cachePath = projectConfig.cachePath || process.env.cachePath || - path.resolve(__dirname, 'node_modules/.cache'); + path.resolve(__dirname, 'node_modules/.cache'); projectConfig.aceSoPath = projectConfig.aceSoPath || process.env.aceSoPath; projectConfig.localPropertiesPath = projectConfig.localPropertiesPath || process.env.localPropertiesPath; projectConfig.projectProfilePath = projectConfig.projectProfilePath || process.env.projectProfilePath; @@ -140,7 +142,7 @@ function loadMemoryTrackingConfig(projectConfig) { projectConfig.memoryDottingPath = path.resolve(projectConfig.buildPath, '../', '../', 'dottingfile'); // recordInterval config, unit is ms projectConfig.memoryDottingRecordInterval = process.env.memoryDottingRecordInterval || 100; - // records the config interval for writing files, unit is ms. + // records the config interval for writing files, unit is ms. projectConfig.memoryDottingWriteFileInterval = process.env.memoryDottingWriteFileInterval || 1000; } @@ -163,7 +165,7 @@ function loadEntryObj(projectConfig) { if (staticPreviewPage) { projectConfig.entryObj['./' + staticPreviewPage] = projectConfig.projectPath + path.sep + - staticPreviewPage + '.ets?entry'; + staticPreviewPage + '.ets?entry'; setEntryArrayForObf(staticPreviewPage); } else if (abilityConfig.abilityType === 'page') { if (fs.existsSync(projectConfig.manifestFilePath)) { @@ -179,7 +181,7 @@ function loadEntryObj(projectConfig) { buildManifest(manifest, projectConfig.aceModuleJsonPath); } else { throw Error('\u001b[31m ERROR: the manifest file ' + projectConfig.manifestFilePath.replace(/\\/g, '/') + - ' or module.json is lost or format is invalid. \u001b[39m').message; + ' or module.json is lost or format is invalid. \u001b[39m').message; } if (!projectConfig.compileHar) { if (manifest.pages) { @@ -193,14 +195,14 @@ function loadEntryObj(projectConfig) { setEntryArrayForObf(sourcePath); } else { throw Error('\u001b[31m10906403 ArkTS Compiler Error' + '\n' + - 'Error Message: ' + `Page '${fileName.replace(/\\/g, '/')}' does not exist. \u001b[39m`) - .message; + 'Error Message: ' + `Page '${fileName.replace(/\\/g, '/')}' does not exist. \u001b[39m`) + .message; } }); } else { throw Error('\u001b[31m ERROR: missing pages attribute in ' + - projectConfig.manifestFilePath.replace(/\\/g, '/') + - '. \u001b[39m').message; + projectConfig.manifestFilePath.replace(/\\/g, '/') + + '. \u001b[39m').message; } } } @@ -229,7 +231,7 @@ function buildManifest(manifest, aceConfigPath) { if (moduleConfigJson && moduleConfigJson.app && moduleConfigJson.app.minAPIVersion) { if (moduleConfigJson.module && moduleConfigJson.module.metadata) { partialUpdateController(moduleConfigJson.app.minAPIVersion, moduleConfigJson.module.metadata, - moduleConfigJson.module.type); + moduleConfigJson.module.type); stageOptimization(moduleConfigJson.module.metadata); } else { partialUpdateController(moduleConfigJson.app.minAPIVersion); @@ -252,15 +254,15 @@ function buildManifest(manifest, aceConfigPath) { } } else { throw Error('\u001b[31m' + - 'BUIDERROR: the config.json file miss key word module || module[abilities].' + - '\u001b[39m').message; + 'BUIDERROR: the config.json file miss key word module || module[abilities].' + + '\u001b[39m').message; } } catch (e) { if (/BUIDERROR/.test(e)) { throw Error(e.replace('BUIDERROR', 'ERROR')).message; } else { throw Error('\x1B[31m' + 'ERROR: the module.json file is lost or format is invalid.' + - '\x1B[39m').message; + '\x1B[39m').message; } } } @@ -271,7 +273,7 @@ function getPackageJsonEntryPath() { let rootPackageJsonContent; try { rootPackageJsonContent = (projectConfig.packageManagerType === 'npm' ? - JSON : JSON5).parse(fs.readFileSync(rootPackageJsonPath, 'utf-8')); + JSON : JSON5).parse(fs.readFileSync(rootPackageJsonPath, 'utf-8')); } catch (e) { throw Error('\u001b[31m' + 'BUIDERROR: ' + rootPackageJsonPath + ' format is invalid.' + '\u001b[39m').message; } @@ -285,7 +287,7 @@ function getPackageJsonEntryPath() { } } else if (projectConfig.compileHar) { throw Error('\u001b[31m' + 'BUIDERROR: lack message in ' + projectConfig.packageJson + '.' + - '\u001b[39m').message; + '\u001b[39m').message; } } } @@ -299,7 +301,7 @@ function supportSuffix(mainEntryPath) { mainEntryPath = path.join(mainEntryPath, 'index.js'); } else if (projectConfig.compileHar) { throw Error('\u001b[31m' + 'BUIDERROR: not find entry file in ' + projectConfig.packageJson + - '.' + '\u001b[39m').message; + '.' + '\u001b[39m').message; } return mainEntryPath; } @@ -338,7 +340,7 @@ function stageOptimization(metadata) { if (Array.isArray(metadata) && metadata.length) { metadata.some(item => { if (item.name && item.name === 'USE_COMMON_CHUNK' && - item.value && item.value === 'true') { + item.value && item.value === 'true') { projectConfig.splitCommon = true; return true; } @@ -365,7 +367,7 @@ function getPages(configJson) { } } catch (e) { throw Error('\x1B[31m' + `BUIDERROR: the ${modulePagePath} file format is invalid.` + - '\x1B[39m').message; + '\x1B[39m').message; } } return pages; @@ -440,8 +442,8 @@ function setEntryArrayForObf(...entryPath) { function folderExistsCaseSensitive(folderPath) { try { const folders = fs.readdirSync(path.dirname(folderPath), { withFileTypes: true }) - .filter(entry => entry.isDirectory()) - .map(entry => entry.name); + .filter(entry => entry.isDirectory()) + .map(entry => entry.name); const targetFolderName = path.basename(folderPath); return folders.includes(targetFolderName); } catch (error) { @@ -479,7 +481,7 @@ function setAbilityFile(projectConfig, abilityPages) { setEntryArrayForObf(entryPageKey); } else { throw Error( - `\u001b[31m ERROR: srcEntry file '${projectAbilityPath.replace(/\\/g, '/')}' does not exist. \u001b[39m` + `\u001b[31m ERROR: srcEntry file '${projectAbilityPath.replace(/\\/g, '/')}' does not exist. \u001b[39m` ).message; } }); @@ -551,7 +553,7 @@ function readCardResource(resource) { function readCardForm(form) { if ((form.type && form.type === 'eTS') || - (form.uiSyntax && form.uiSyntax === 'arkts')) { + (form.uiSyntax && form.uiSyntax === 'arkts')) { const sourcePath = form.src.replace(/\.ets$/, ''); const cardPath = path.resolve(projectConfig.projectPath, '..', sourcePath + '.ets'); if (cardPath && fs.existsSync(cardPath)) { @@ -587,7 +589,7 @@ function loadWorker(projectConfig, workerFileEntry) { workerFiles.forEach((item) => { if (/\.(ts|js|ets)$/.test(item)) { const relativePath = path.relative(workerPath, item) - .replace(/\.(ts|js|ets)$/, '').replace(/\\/g, '/'); + .replace(/\.(ts|js|ets)$/, '').replace(/\\/g, '/'); projectConfig.entryObj[`./${WORKERS_DIR}/` + relativePath] = item; setEntryArrayForObf(WORKERS_DIR, relativePath); abilityPagesFullPath.add(path.resolve(item).toLowerCase()); @@ -629,7 +631,7 @@ function loadBuildJson() { function initBuildInfo() { projectConfig.projectRootPath = aceBuildJson.projectRootPath; if (projectConfig.compileHar && aceBuildJson.moduleName && - aceBuildJson.modulePathMap[aceBuildJson.moduleName]) { + aceBuildJson.modulePathMap[aceBuildJson.moduleName]) { projectConfig.moduleRootPath = aceBuildJson.modulePathMap[aceBuildJson.moduleName]; } } @@ -640,9 +642,9 @@ function readWorkerFile() { aceBuildJson.workers.forEach(worker => { if (!/\.(ts|js|ets)$/.test(worker)) { throw Error( - '\u001b[31m10906402 ArkTS Compiler Error' + '\n' + - 'Error Message: ' + 'File: ' + worker + '.' + '\n' + - "The worker file can only be an '.ets', '.ts', or '.js' file.\u001b[39m" + '\u001b[31m10906402 ArkTS Compiler Error' + '\n' + + 'Error Message: ' + 'File: ' + worker + '.' + '\n' + + "The worker file can only be an '.ets', '.ts', or '.js' file.\u001b[39m" ).message; } const relativePath = path.relative(projectConfig.projectPath, worker); @@ -650,9 +652,9 @@ function readWorkerFile() { const workerKey = relativePath.replace(/\.(ts|js)$/, '').replace(/\\/g, '/'); if (workerFileEntry[workerKey]) { throw Error( - '\u001b[31m10905407 ArkTS Compiler Error' + '\n' + - 'Error Message: ' + 'The worker file cannot use the same file name: \n' + - workerFileEntry[workerKey] + '\n' + worker + '\u001b[39m' + '\u001b[31m10905407 ArkTS Compiler Error' + '\n' + + 'Error Message: ' + 'The worker file cannot use the same file name: \n' + + workerFileEntry[workerKey] + '\n' + worker + '\u001b[39m' ).message; } else { workerFileEntry[workerKey] = worker; @@ -668,14 +670,14 @@ function readWorkerFile() { function readPatchConfig() { if (aceBuildJson.patchConfig) { projectConfig.hotReload = (process.env.watchMode === 'true' && !projectConfig.isPreview) || - aceBuildJson.patchConfig.mode === 'hotReload'; + aceBuildJson.patchConfig.mode === 'hotReload'; projectConfig.coldReload = aceBuildJson.patchConfig.mode === COLD_RELOAD_MODE ? true : false; // The "isFirstBuild" field indicates whether it is the first compilation of cold reload mode // It is determined by hvigor and passed via env projectConfig.isFirstBuild = process.env.isFirstBuild === 'false' ? false : true; projectConfig.patchAbcPath = aceBuildJson.patchConfig.patchAbcPath; projectConfig.changedFileList = aceBuildJson.patchConfig.changedFileList ? - aceBuildJson.patchConfig.changedFileList : path.join(projectConfig.cachePath, 'changedFileList.json'); + aceBuildJson.patchConfig.changedFileList : path.join(projectConfig.cachePath, 'changedFileList.json'); projectConfig.removeChangedFileListInSdk = aceBuildJson.patchConfig.removeChangedFileListInSdk === 'true' || false; if (!projectConfig.removeChangedFileListInSdk && projectConfig.hotReload) { writeFileSync(projectConfig.changedFileList, JSON.stringify({ @@ -732,11 +734,11 @@ function filterWorker(workerPath) { return !(dirName === apiDirPath || dirName === arktsDirPath || dirName === kitsDirPath); }).map(filePath => { return filePath - .replace(apiDirPath, '') - .replace(arktsDirPath, '') - .replace(kitsDirPath, '') - .replace(/\\/g, '/') - .replace(/(^\/)|(.d.e?ts$)/g, ''); + .replace(apiDirPath, '') + .replace(arktsDirPath, '') + .replace(kitsDirPath, '') + .replace(/\\/g, '/') + .replace(/(^\/)|(.d.e?ts$)/g, ''); }); ohosSystemModuleSubDirPaths.push(...moduleSubdir); allModulesPaths.push(...modulePaths); @@ -760,6 +762,19 @@ function filterWorker(workerPath) { sdkConfigs = [...defaultSdkConfigs, ...extendSdkConfigs]; })(); +function collectExternalApiCheckPlugin(sdkConfig, sdkPath) { + const pluginConfigs = sdkConfig.apiCheckPlugin; + pluginConfigs.forEach(config => { + const pluginPrefix = sdkConfig.prefix + '/' + config.tag; + config.path = path.resolve(sdkPath, config.path); + if (externalApiCheckPlugin.get(pluginPrefix)) { + externalApiCheckPlugin.set(pluginPrefix, externalApiCheckPlugin.get(pluginPrefix).push(...pluginConfigs)); + } else { + externalApiCheckPlugin.set(pluginPrefix, [...pluginConfigs]); + } + }); +} + function collectExternalModules(sdkPaths) { for (let i = 0; i < sdkPaths.length; i++) { const sdkPath = sdkPaths[i]; @@ -771,6 +786,10 @@ function collectExternalModules(sdkPaths) { if (!sdkConfig.apiPath) { continue; } + + if (sdkConfig.apiCheckPlugin && sdkConfig.apiCheckPlugin.length > 0) { + collectExternalApiCheckPlugin(sdkConfig, sdkPath); + } let externalApiPathArray = []; if (Array.isArray(sdkConfig.apiPath)) { externalApiPathArray = sdkConfig.apiPath; @@ -908,7 +927,7 @@ function loadModuleInfo(projectConfig, envArgs) { projectConfig.harNameOhmMap = buildJsonInfo.harNameOhmMap; } if (projectConfig.compileHar && buildJsonInfo.moduleName && - buildJsonInfo.modulePathMap[buildJsonInfo.moduleName]) { + buildJsonInfo.modulePathMap[buildJsonInfo.moduleName]) { if (projectConfig.useTsHar) { projectConfig.processTs = true; } @@ -950,11 +969,11 @@ function saveAppResourcePath(appResourcePath, appResourcePathSavePath) { function addSDKBuildDependencies(config) { if (projectConfig.localPropertiesPath && - fs.existsSync(projectConfig.localPropertiesPath) && config.cache) { + fs.existsSync(projectConfig.localPropertiesPath) && config.cache) { config.cache.buildDependencies.config.push(projectConfig.localPropertiesPath); } if (projectConfig.projectProfilePath && - fs.existsSync(projectConfig.projectProfilePath) && config.cache) { + fs.existsSync(projectConfig.projectProfilePath) && config.cache) { config.cache.buildDependencies.config.push(projectConfig.projectProfilePath); } } @@ -984,7 +1003,7 @@ function isPartialUpdate(metadata, moduleType) { partialUpdateConfig.partialUpdateMode = false; if (projectConfig.aceModuleJsonPath) { logger.warn('\u001b[33m ArkTS:WARN File: ' + projectConfig.aceModuleJsonPath + '.' + '\n' + - " The 'ArkTSPartialUpdate' field will no longer be supported in the future. \u001b[39m"); + " The 'ArkTSPartialUpdate' field will no longer be supported in the future. \u001b[39m"); } } if (item.name === 'ArkTSBuilderCheck' && item.value === 'false') { @@ -1019,9 +1038,9 @@ function isPartialUpdate(metadata, moduleType) { } } return !partialUpdateConfig.partialUpdateMode && !partialUpdateConfig.builderCheck && - !partialUpdateConfig.executeArkTSLinter && !partialUpdateConfig.standardArkTSLinter && - partialUpdateConfig.arkTSVersion !== undefined && projectConfig.optLazyForEach && - partialUpdateConfig.skipTscOhModuleCheck && partialUpdateConfig.skipArkTSStaticBlocksCheck; + !partialUpdateConfig.executeArkTSLinter && !partialUpdateConfig.standardArkTSLinter && + partialUpdateConfig.arkTSVersion !== undefined && projectConfig.optLazyForEach && + partialUpdateConfig.skipTscOhModuleCheck && partialUpdateConfig.skipArkTSStaticBlocksCheck; }); } @@ -1186,3 +1205,4 @@ exports.resetGlobalProgram = resetGlobalProgram; exports.setEntryArrayForObf = setEntryArrayForObf; exports.getPackageJsonEntryPath = getPackageJsonEntryPath; exports.setIntentEntryPages = setIntentEntryPages; +exports.externalApiCheckPlugin = externalApiCheckPlugin; diff --git a/compiler/src/fast_build/system_api/api_check_utils.ts b/compiler/src/fast_build/system_api/api_check_utils.ts index 5da0d3e1c..c360766ec 100644 --- a/compiler/src/fast_build/system_api/api_check_utils.ts +++ b/compiler/src/fast_build/system_api/api_check_utils.ts @@ -24,13 +24,15 @@ import { ohosSystemModulePaths, systemModules, allModulesPaths, - ohosSystemModuleSubDirPaths + ohosSystemModuleSubDirPaths, + externalApiCheckPlugin } from '../../../main'; import { LogType, LogInfo, IFileLog, - CurrentProcessFile + CurrentProcessFile, + compilerOptions } from '../../utils'; import { type ResolveModuleInfo } from '../../ets_checker'; import { @@ -75,11 +77,6 @@ import { import { JsDocCheckService } from './api_check_permission'; import { SdkApiVersionValidator } from './sdk_api_version_validator'; -export enum SdkApiType{ - HarmonyOS, - OpenHarmony -} - /** * bundle info * @@ -151,7 +148,7 @@ function checkBundleVersion(bundleVersion: string): boolean { bundleVersionNumber = Number(bundleVersion); } if (bundleVersion && bundleVersion !== '' && !isNaN(bundleVersionNumber) && - !isNaN(Number(compatibleSdkVersion)) && Number(compatibleSdkVersion) >= bundleVersionNumber) { + !isNaN(Number(compatibleSdkVersion)) && Number(compatibleSdkVersion) >= bundleVersionNumber) { return true; } return false; @@ -198,7 +195,7 @@ export function getRealModulePath(apiDirs: string[], moduleName: string, exts: s * @returns {string} */ export function moduleRequestCallback(moduleRequest: string, _: string, - moduleType: string, systemKey: string): string { + moduleType: string, systemKey: string): string { for (const config of extendSdkConfigs.values()) { if (config.prefix === '@arkui-x') { continue; @@ -206,7 +203,7 @@ export function moduleRequestCallback(moduleRequest: string, _: string, if (moduleRequest.startsWith(config.prefix + '.')) { let compileRequest: string = `${config.prefix}:${systemKey}`; const resolveModuleInfo: ResolveModuleInfo = getRealModulePath(config.apiPath, moduleRequest, - ['.d.ts', '.d.ets']); + ['.d.ts', '.d.ets']); const modulePath: string = resolveModuleInfo.modulePath; if (!fs.existsSync(modulePath)) { return compileRequest; @@ -248,8 +245,8 @@ export function checkTypeReference(node: ts.TypeReferenceNode, transformLog: IFi } const sourceBaseName: string = path.basename(sourceFile.fileName); if (isArkuiDependence(sourceFile.fileName) && - sourceBaseName !== 'common_ts_ets_api.d.ts' && - sourceBaseName !== 'global.d.ts' + sourceBaseName !== 'common_ts_ets_api.d.ts' && + sourceBaseName !== 'global.d.ts' ) { // TODO: change to error transformLog.errors.push({ @@ -258,7 +255,7 @@ export function checkTypeReference(node: ts.TypeReferenceNode, transformLog: IFi pos: node.getStart() }); } else if (GLOBAL_DECLARE_WHITE_LIST.has(currentTypeName) && - ohosSystemModulePaths.includes(sourceFile.fileName.replace(/\//g, '\\'))) { + ohosSystemModulePaths.includes(sourceFile.fileName.replace(/\//g, '\\'))) { transformLog.errors.push({ type: LogType.WARN, message: `Cannot find name '${currentTypeName}'.`, @@ -281,10 +278,10 @@ export function checkTypeReference(node: ts.TypeReferenceNode, transformLog: IFi * @returns {ts.JsDocNodeCheckConfigItem} */ function getJsDocNodeCheckConfigItem(tagName: string[], message: string, needConditionCheck: boolean, - type: ts.DiagnosticCategory, specifyCheckConditionFuncName: string, - tagNameShouldExisted: boolean, checkValidCallback?: CheckValidCallbackInterface, - checkJsDocSpecialValidCallback?: CheckJsDocSpecialValidCallbackInterface, - checkConditionValidCallback?: checkConditionValidCallbackInterface): ts.JsDocNodeCheckConfigItem { + type: ts.DiagnosticCategory, specifyCheckConditionFuncName: string, + tagNameShouldExisted: boolean, checkValidCallback?: CheckValidCallbackInterface, + checkJsDocSpecialValidCallback?: CheckJsDocSpecialValidCallbackInterface, + checkConditionValidCallback?: checkConditionValidCallbackInterface): ts.JsDocNodeCheckConfigItem { return { tagName: tagName, message: message, @@ -337,27 +334,27 @@ export function getJsDocNodeCheckConfig(fileName: string, sourceFileName: string const apiName: string = path.basename(fileName); const sourceBaseName: string = path.basename(sourceFileName); if (/(? 0) { const fileContent: string = fs.readFileSync(fileName, { encoding: 'utf-8' }); const needCanIUseCheck: boolean = /canIUse\(.*\)/.test(fileContent); checkConfigArray.push(getJsDocNodeCheckConfigItem([SYSCAP_TAG_CHECK_NAME], - SYSCAP_TAG_CHECK_WARNING, needCanIUseCheck, ts.DiagnosticCategory.Warning, CANIUSE_FUNCTION_NAME, false, undefined, - checkSyscapAbility, checkSyscapConditionValidCallback)); + SYSCAP_TAG_CHECK_WARNING, needCanIUseCheck, ts.DiagnosticCategory.Warning, CANIUSE_FUNCTION_NAME, false, undefined, + checkSyscapAbility, checkSyscapConditionValidCallback)); } if (projectConfig.projectRootPath) { const ohosTestDir = ts.sys.resolvePath(path.join(projectConfig.projectRootPath, 'entry', 'src', 'ohosTest')); @@ -365,38 +362,38 @@ export function getJsDocNodeCheckConfig(fileName: string, sourceFileName: string if (!ts.sys.resolvePath(fileName).startsWith(ohosTestDir)) { permissionsArray = projectConfig.requestPermissions; checkConfigArray.push(getJsDocNodeCheckConfigItem([TEST_TAG_CHECK_NAME], TEST_TAG_CHECK_ERROR, false, - ts.DiagnosticCategory.Warning, '', false)); + ts.DiagnosticCategory.Warning, '', false)); } } checkConfigArray.push(getJsDocNodeCheckConfigItem([PERMISSION_TAG_CHECK_NAME], PERMISSION_TAG_CHECK_ERROR, false, - ts.DiagnosticCategory.Warning, '', false, undefined, checkPermissionValue)); + ts.DiagnosticCategory.Warning, '', false, undefined, checkPermissionValue)); if (isCardFile(fileName)) { needCheckResult = true; checkConfigArray.push(getJsDocNodeCheckConfigItem([FORM_TAG_CHECK_NAME], FORM_TAG_CHECK_ERROR, false, - ts.DiagnosticCategory.Error, '', true)); + ts.DiagnosticCategory.Error, '', true)); } if (projectConfig.isCrossplatform) { needCheckResult = true; const logType: ts.DiagnosticCategory = projectConfig.ignoreCrossplatformCheck !== true ? ts.DiagnosticCategory.Error : - ts.DiagnosticCategory.Warning; + ts.DiagnosticCategory.Warning; checkConfigArray.push(getJsDocNodeCheckConfigItem([CROSSPLATFORM_TAG_CHECK_NAME], CROSSPLATFORM_TAG_CHECK_ERROER, - false, logType, '', true)); + false, logType, '', true)); } if (process.env.compileMode === STAGE_COMPILE_MODE) { needCheckResult = true; checkConfigArray.push(getJsDocNodeCheckConfigItem([FA_TAG_CHECK_NAME, FA_TAG_HUMP_CHECK_NAME], - FA_TAG_CHECK_ERROR, false, ts.DiagnosticCategory.Error, '', false)); + FA_TAG_CHECK_ERROR, false, ts.DiagnosticCategory.Error, '', false)); } else if (process.env.compileMode !== '') { needCheckResult = true; checkConfigArray.push(getJsDocNodeCheckConfigItem([STAGE_TAG_CHECK_NAME, STAGE_TAG_HUMP_CHECK_NAME], - STAGE_TAG_CHECK_ERROR, false, - ts.DiagnosticCategory.Error, '', false)); + STAGE_TAG_CHECK_ERROR, false, + ts.DiagnosticCategory.Error, '', false)); } if (projectConfig.bundleType === ATOMICSERVICE_BUNDLE_TYPE && - projectConfig.compileSdkVersion >= ATOMICSERVICE_TAG_CHECK_VERSION) { + projectConfig.compileSdkVersion >= ATOMICSERVICE_TAG_CHECK_VERSION) { needCheckResult = true; checkConfigArray.push(getJsDocNodeCheckConfigItem([ATOMICSERVICE_TAG_CHECK_NAME], ATOMICSERVICE_TAG_CHECK_ERROER, - false, ts.DiagnosticCategory.Error, '', true)); + false, ts.DiagnosticCategory.Error, '', true)); } } result = { @@ -518,9 +515,9 @@ function collectOhSyscapInfos(deviceType: string, deviceDir: string, deviceInfoM } function collectExternalSyscapInfos( - externalApiPaths: string[], - deviceTypes: string[], - deviceInfoMap: Map + externalApiPaths: string[], + deviceTypes: string[], + deviceInfoMap: Map ) { const externalDeviceDirs: string[] = []; externalApiPaths.forEach((externalApiPath: string) => { @@ -552,11 +549,11 @@ function collectExternalSyscapInfos( /** * Determine the necessity of since check. - * - * @param jsDocTags - * @param config + * + * @param jsDocTags + * @param config * @param node - * @returns + * @returns */ function checkSinceValue(jsDocTags: readonly ts.JSDocTag[], config: ts.JsDocNodeCheckConfigItem, node?: ts.Node): boolean { if (!jsDocTags[0]?.parent?.parent || !projectConfig.compatibleSdkVersion) { @@ -573,19 +570,73 @@ function checkSinceValue(jsDocTags: readonly ts.JSDocTag[], config: ts.JsDocNode if (!isCompliantSince(minSince) || !isCompliantSince(compatibleSdkVersion)) { return false; } + + const sourceFile: ts.SourceFile = currentNode.getSourceFile(); + if (!sourceFile) { + return false; + } + const apiFilePath: string = sourceFile.fileName; + const pluginKey: string = getPluginKey(apiFilePath, SINCE_TAG_NAME); + const needExtrenalApiCheck: boolean = isExternalApiCheck(pluginKey); + if (hasSince && comparePointVersion(compatibleSdkVersion.toString(), minSince) === -1) { const checker: ts.TypeChecker | undefined = CurrentProcessFile.getChecker(); - const sdkValidator = new SdkApiVersionValidator(compatibleSdkVersion, checker); + const sdkValidator = new SdkApiVersionValidator(compatibleSdkVersion, minSince, checker); - if (sdkValidator.isSdkApiVersionHandled(node, SdkApiType.HarmonyOS)) { + if (sdkValidator.isSdkApiVersionHandled(node)) { return false; } - config.message = SINCE_TAG_CHECK_ERROER.replace('$SINCE1', minSince).replace('$SINCE2', compatibleSdkVersion); + config.message = SINCE_TAG_CHECK_ERROER.replace('$SINCE1', getVersionNumber(minSince)).replace('$SINCE2', compatibleSdkVersion); + return true; + } + return false; +} + +/** + * 是否需要使用拓展SDK自定义校验接口 + * @param { string } pluginKey prefix/tagname + * @returns { boolean } + */ +function isExternalApiCheck(pluginKey: string): boolean { + if (externalApiCheckPlugin.get(pluginKey)) { return true; } return false; } +/** + * 获取externalApiCheckPlugin的key + * @param { string } apiFilePath + * @param { string } tagName + * @returns { string } + */ +function getPluginKey(apiFilePath: string, tagName: string): string { + const apiFileName: string = path.basename(apiFilePath); + const apiPrefix: string = apiFileName.split('.')[0]; + const pluginKey: string = apiPrefix + '/' + tagName; + return pluginKey; +} + +/** + * 调用拓展SDK自定义校验接口 + * @param { string } pluginKey + * @param { string } tagValue + * @param { string } targetValue + * @returns { boolean } + */ +function externalApiCheck(pluginKey: string, tagValue: string, targetValue: string): boolean { + const extrenalPlugins = externalApiCheckPlugin.get(pluginKey); + for (let i = 0; i < extrenalPlugins.length; i++) { + const extrenalPlugin = extrenalPlugins[i]; + const extrenalMethod = require(extrenalPlugin.path)[extrenalPlugin.functionName]; + const checkResult: boolean = extrenalMethod(tagValue, targetValue); + if (checkResult) { + return true; + } + } + return false; +} + /** * Confirm compliance since * Only major version can be passed in, such as "19"; @@ -594,19 +645,19 @@ function checkSinceValue(jsDocTags: readonly ts.JSDocTag[], config: ts.JsDocNode * 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); + return /^(?!0\d)[1-9]\d{0,2}(?:\.[1-9]\d{0,2}|\.0){0,2}(?:\(\d{1,3}\))?$/.test(since); } /** * Determine the necessity of syscap check. - * @param jsDocTags - * @param config - * @returns + * @param jsDocTags + * @param config + * @returns */ export function checkSyscapAbility(jsDocTags: readonly ts.JSDocTag[], config: ts.JsDocNodeCheckConfigItem): boolean { let currentSyscapValue: string = ''; @@ -668,8 +719,8 @@ export function checkPermissionValue(jsDocTags: readonly ts.JSDocTag[], config: return false; } const comment: string = typeof jsDocTag.comment === 'string' ? - jsDocTag.comment : - ts.getTextOfJSDocComment(jsDocTag.comment); + jsDocTag.comment : + ts.getTextOfJSDocComment(jsDocTag.comment); config.message = PERMISSION_TAG_CHECK_ERROR.replace('$DT', comment); return comment !== '' && !JsDocCheckService.validPermission(comment, permissionsArray); } @@ -682,7 +733,7 @@ export function checkPermissionValue(jsDocTags: readonly ts.JSDocTag[], config: * @returns */ export function getJsDocNodeConditionCheckResult(jsDocFileCheckedInfo: ts.FileCheckModuleInfo, jsDocTagInfos: ts.JsDocTagInfo[], jsDocs?: ts.JSDoc[]): - ts.ConditionCheckResult { + ts.ConditionCheckResult { let result: ts.ConditionCheckResult = { valid: true }; @@ -787,8 +838,8 @@ function checkSyscapConditionValidCallback(node: ts.CallExpression, specifyFuncN const typeChecker: ts.TypeChecker = globalProgram.program.getTypeChecker(); const arguSymbol: ts.Symbol | undefined = typeChecker.getSymbolAtLocation(expression); return arguSymbol && arguSymbol.valueDeclaration && ts.isVariableDeclaration(arguSymbol.valueDeclaration) && - arguSymbol.valueDeclaration.initializer && ts.isStringLiteral(arguSymbol.valueDeclaration.initializer) && - arguSymbol.valueDeclaration.initializer.text === tagValue; + arguSymbol.valueDeclaration.initializer && ts.isStringLiteral(arguSymbol.valueDeclaration.initializer) && + arguSymbol.valueDeclaration.initializer.text === tagValue; } } return false; @@ -796,7 +847,7 @@ function checkSyscapConditionValidCallback(node: ts.CallExpression, specifyFuncN /** * get minversion - * @param { ts.JSDoc[] } jsDocs + * @param { ts.JSDoc[] } jsDocs * @returns string */ function getMinVersion(jsDocs: ts.JSDoc[]): string { @@ -816,25 +867,17 @@ function getMinVersion(jsDocs: ts.JSDoc[]): string { return minVersion; } -/** - * compare point version - * @param { string } firstVersion - * @param { string } secondVersion - * @returns { number } - */ +function getVersionNumber(version: string): number { + const parenMatch = version.match(/\((\d+)\)/); + if (parenMatch) return parseInt(parenMatch[1], 10); + + const parts = version.split('.'); + return parseInt(parts[parts.length - 1], 10); +} + 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; + const firstNum = getVersionNumber(firstVersion); + const secondNum = getVersionNumber(secondVersion); + + return firstNum > secondNum ? 1 : -1; } \ No newline at end of file diff --git a/compiler/src/fast_build/system_api/sdk_api_version_validator.ts b/compiler/src/fast_build/system_api/sdk_api_version_validator.ts index 2206cb59d..ef0c463b5 100644 --- a/compiler/src/fast_build/system_api/sdk_api_version_validator.ts +++ b/compiler/src/fast_build/system_api/sdk_api_version_validator.ts @@ -14,7 +14,6 @@ */ import ts from 'typescript'; -import { SdkApiType } from './api_check_utils'; /** * The node is considered **valid** if it satisfies **at least one** of the following: @@ -23,35 +22,36 @@ import { SdkApiType } from './api_check_utils'; * 3. It is wrapped in an SDK version comparison. */ export class SdkApiVersionValidator { - private readonly sdkOpenHarmonyPackageMap: Map = new Map([ + private readonly deviceInfoChecker: Map = new Map([ + ['distributionOSApiVersion', ['@ohos.deviceInfo.d.ts']], ['sdkApiVersion', ['@ohos.deviceInfo.d.ts']] ]); - private readonly sdkHarmonOSPackageMap: Map = new Map([ - ['distributionOSApiVersion', ['@ohos.deviceInfo.d.ts']] - ]); private readonly compatibleSdkVersion: string; + private readonly minSinceVersion: string; private readonly typeChecker?: ts.TypeChecker; + private readonly closeSourceDeviceInfo: string = "distributionOSApiVersion" + private readonly openSourceDeviceInfo: "sdkApiVersion" - constructor(projectCompatibleSdkVersion: string, typeChecker?: ts.TypeChecker) { + constructor(projectCompatibleSdkVersion: string, minSinceValue: string, typeChecker?: ts.TypeChecker) { this.compatibleSdkVersion = projectCompatibleSdkVersion; + this.minSinceVersion = minSinceValue; this.typeChecker = typeChecker; } /** * Checks whether a given node is valid for at least one condition. * @param node - The AST node to check. - * @param sdkType - Calling API's type (HarmonyOS or OpenHarmony) * @returns `true` if the node meets any of the handling rules, otherwise `false`. */ - public isSdkApiVersionHandled(node: ts.Node, sdkType: SdkApiType): boolean { + public isSdkApiVersionHandled(node: ts.Node): boolean { if (!node) { return false; } return ( - this.isNodeWrappedInTryCatch(node) || - this.isNodeWrappedInUndefinedCheck(node) || - this.isNodeWrappedInSdkComparison(node, sdkType) + this.isNodeWrappedInTryCatch(node) || + this.isNodeWrappedInUndefinedCheck(node) || + this.isNodeWrappedInSdkComparison(node) ); } @@ -109,7 +109,7 @@ export class SdkApiVersionValidator { return ts.isIdentifier(node) && node.text === 'undefined'; } - private isNodeWrappedInSdkComparison(node: ts.Node, sdkType: SdkApiType): boolean { + private isNodeWrappedInSdkComparison(node: ts.Node): boolean { if (this.compatibleSdkVersion === '' || !this.typeChecker) { return false; } @@ -117,7 +117,7 @@ export class SdkApiVersionValidator { return this.findParentNode(node, (parent) => { if (ts.isIfStatement(parent)) { try { - return this.isSdkComparisonHelper(parent.expression, sdkType); + return this.isSdkComparisonHelper(parent.expression); } catch { return false; } @@ -126,44 +126,66 @@ export class SdkApiVersionValidator { }) !== null; } - private isSdkComparisonHelper(expression: ts.Expression, sdkType: SdkApiType): boolean { + private isSdkComparisonHelper(expression: ts.Expression): boolean { const expressionText = expression.getText(); - let sdkApiToPackageMap: Map; - try { - sdkApiToPackageMap = this.getSdkPackageMap(sdkType); - } catch (err) { - return false; - } - - // Find matching SDK API entry based on whether the expression text contains the API name - const matchedEntry = Array.from(sdkApiToPackageMap.entries()) - .find(([api]) => expressionText.includes(api)); + const matchedEntry = Array.from(this.deviceInfoChecker.entries()) + .find(([api]) => expressionText.includes(api)); if (!matchedEntry) { return false; } const [matchedApi, validPackagePaths] = matchedEntry; + const parts = this.extractComparisonParts(expression, matchedApi); + if (!parts) { + return false + } + + if (!this.sdkComparison(parts.operator, parts.value, matchedApi)){ + return false + } + // Try to resolve the actual identifier used for this API in the expression const apiIdentifier = this.findValidImportApiIdentifier(expression, matchedApi); // Validate that the identifier comes from one of the allowed SDK package paths - return apiIdentifier ? - this.isValidSdkDeclaration(apiIdentifier, validPackagePaths) : - false; + return apiIdentifier + ? this.isValidSdkDeclaration(apiIdentifier, validPackagePaths) + : false; + } + + private extractComparisonParts( + expression: ts.Expression, + matchedApi: string + ): { operator: string; value: string } | undefined { + if (!ts.isBinaryExpression(expression)) { + return undefined; + } + + const operator = expression.operatorToken.getText(); + const left = expression.left.getText(); + const right = expression.right.getText(); + + if (left.includes(matchedApi)) { + return { operator, value: right }; + } else if (right.includes(matchedApi)) { + return { operator, value: left }; + } + + return undefined; } private findValidImportApiIdentifier(expression: ts.Expression, api: string): ts.Identifier | undefined { if (ts.isBinaryExpression(expression)) { return this.extractApiIdentifierFromExpression(expression.left, api) || - this.extractApiIdentifierFromExpression(expression.right, api); + this.extractApiIdentifierFromExpression(expression.right, api); } return this.extractApiIdentifierFromExpression(expression, api); } private extractApiIdentifierFromExpression( - expression: ts.Expression, - targetProperty: string + expression: ts.Expression, + targetProperty: string ): ts.Identifier | undefined { if (!ts.isPropertyAccessExpression(expression)) { return undefined; @@ -192,8 +214,8 @@ export class SdkApiVersionValidator { } const declarationFile = this.getActualDeclarationFile(symbol); return declarationFile ? - this.isValidSdkDeclarationPath(declarationFile, validPackagePaths) : - false; + this.isValidSdkDeclarationPath(declarationFile, validPackagePaths) : + false; } private getActualDeclarationFile(symbol: ts.Symbol): string | undefined { @@ -215,7 +237,7 @@ export class SdkApiVersionValidator { private isValidSdkDeclarationPath(filePath: string, validPackagePaths: string[]): boolean { const normalizedPath = this.normalizePath(filePath); return validPackagePaths.some(validPath => - normalizedPath.includes(this.normalizePath(validPath)) + normalizedPath.includes(this.normalizePath(validPath)) ); } @@ -223,28 +245,59 @@ export class SdkApiVersionValidator { return path.replace(/\\/g, '/').toLowerCase(); } - private getSdkPackageMap(sdkType: SdkApiType): Map { - switch (sdkType) { - case SdkApiType.HarmonyOS: - return this.sdkHarmonOSPackageMap; - case SdkApiType.OpenHarmony: - return this.sdkOpenHarmonyPackageMap; + private sdkComparison(operator: string, value: string, matchedApi: string): boolean { + let minVal: number; + let cmpVal: number; + + if (matchedApi === this.openSourceDeviceInfo) { + return true + //TODO: we will implement close source checkSince + minVal = Number(this.minSinceVersion); + cmpVal = Number(value); + } else if (matchedApi === this.closeSourceDeviceInfo) { + return true + //TODO: we will implement close source checkSince + minVal = this.parseDistributionOSVersion(this.minSinceVersion); + cmpVal = Number(value); + } else { + throw new Error(`Unknown API type: ${matchedApi}`); + } + + switch (operator) { + case ">": return cmpVal > minVal; + case "<": return cmpVal < minVal; + case "==": + case "===": + case ">=": + case "<=": + return false; + case "!=": + case "!==": return cmpVal !== minVal; default: - throw new Error(`Unsupported SDK type: ${sdkType}`); + throw new Error(`Unsupported operator: ${operator}`); } } + private parseDistributionOSVersion(version: string): number { + const match = version.match(/^(\d+\.\d+\.\d+)/); + if (!match) { + throw new Error(`Invalid distributionOSApiVersion format: ${version}`); + } + const [x, y, z] = match[1].split('.').map(Number); + return x * 10000 + y * 100 + z; + } + /** - * Traverses upward in the AST from the given node to find the first parent - * that satisfies the provided predicate function. - * - * @param node - The starting AST node. - * @param predicate - A function that returns `true` for the desired parent node. - * @returns The first matching parent node, or `null` if none is found. - */ + * Traverses upward in the AST from the given node to find the first parent + * that satisfies the provided predicate function. + * + * @param node - The starting AST node. + * @param predicate - A function that returns `true` for the desired parent node. + * @returns The first matching parent node, or `null` if none is found. + */ private findParentNode( - node: ts.Node, - predicate: (parent: ts.Node) => boolean + node: ts.Node, + predicate: (parent: ts.Node) => boolean ): ts.Node | null { let currentNode = node.parent; -- Gitee