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 71be695cd3d84c08684a8ec0b880aead0e20a94a..3dc86b2a401f4e6f9e9e5f80c7dd2763067d4a4e 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,7 @@ import { VERSION_CHECK_FUNCTION_NAME } from './api_check_define'; import { JsDocCheckService } from './api_check_permission'; +import {NodeValidator} from "./node_validator"; /** * bundle info @@ -89,7 +90,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 { @@ -546,12 +547,13 @@ function collectExternalSyscapInfos( /** * Determine the necessity of since check. - * - * @param jsDocTags - * @param config - * @returns + * + * @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 +569,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 nodeValidator = new NodeValidator(compatibleSdkVersion, checker); + + if (nodeValidator.isNodeHandled(node)) { + return false; + } config.message = SINCE_TAG_CHECK_ERROER.replace('$SINCE1', minSince).replace('$SINCE2', compatibleSdkVersion); return true; } @@ -581,7 +589,7 @@ 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} */ @@ -591,9 +599,9 @@ function isCompliantSince(since: string): boolean { /** * 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 = ''; @@ -783,7 +791,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 { @@ -805,8 +813,8 @@ function getMinVersion(jsDocs: ts.JSDoc[]): string { /** * compare point version - * @param { string } firstVersion - * @param { string } secondVersion + * @param { string } firstVersion + * @param { string } secondVersion * @returns { number } */ function comparePointVersion(firstVersion: string, secondVersion: string): number { @@ -824,4 +832,4 @@ function comparePointVersion(firstVersion: string, secondVersion: string): numbe } } return 0; -} \ No newline at end of file +} diff --git a/compiler/src/fast_build/system_api/node_validator.ts b/compiler/src/fast_build/system_api/node_validator.ts new file mode 100644 index 0000000000000000000000000000000000000000..3fafa3abb2a2018274607e11fa1157ce9f3c7784 --- /dev/null +++ b/compiler/src/fast_build/system_api/node_validator.ts @@ -0,0 +1,222 @@ +import ts from 'typescript'; + +export class NodeValidator { + private readonly sdkApiToPackageMap: Map = new Map([ + ['sdkApiVersion', ['@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. + */ + public isNodeHandled(node: ts.Node): boolean { + return ( + this.isNodeWrappedInTryCatch(node) || + this.isNodeWrappedInUndefinedCheck(node) || + this.isNodeWrappedInSdkComparison(node) + ); + } + + // ─────────────────────────────────────────────────────────────── + // TRY-CATCH CHECK BLOCK + // ─────────────────────────────────────────────────────────────── + public isNodeWrappedInTryCatch(node: ts.Node): boolean { + return this.findParentNode(node, (parent) => { + if (ts.isTryStatement(parent)) { + return node.getStart() >= parent.tryBlock.getStart(); + } + return false; + }) !== null; + } + + // ─────────────────────────────────────────────────────────────── + // UNDEFINED CHECK BLOCK + // ─────────────────────────────────────────────────────────────── + public 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; + } + + const isNotEqualOperator = [ + ts.SyntaxKind.ExclamationEqualsEqualsToken, + ts.SyntaxKind.ExclamationEqualsToken + ].includes(expression.operatorToken.kind); + + if (!isNotEqualOperator) { + return false; + } + + const { left, right } = expression; + 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'; + } + + // ─────────────────────────────────────────────────────────────── + // SDK COMPARISON CHECK BLOCK + // ─────────────────────────────────────────────────────────────── + public isNodeWrappedInSdkComparison(node: ts.Node): boolean { + if (this.compatibleSdkVersion === '' || !this.typeChecker) { + return false; + } + + return this.findParentNode(node, (parent) => { + if (ts.isIfStatement(parent)) { + try { + return this.isSdkComparisonHelper(parent.expression); + } catch { + return false; + } + } + return false; + }) !== null; + } + + private isSdkComparisonHelper(expression: ts.Expression): boolean { + const expressionText = expression.getText(); + + const matchedEntry = Array.from(this.sdkApiToPackageMap.entries()) + .find(([api]) => expressionText.includes(api)); + + if (!matchedEntry) { + return false; + } + + const [matchedApi, validPackagePaths] = matchedEntry; + const apiIdentifier = this.findApiIdentifier(expression, matchedApi); + + return apiIdentifier + ? this.isValidSdkDeclaration(apiIdentifier, validPackagePaths) + : false; + } + + private findApiIdentifier(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; + 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(); + } + + // ─────────────────────────────────────────────────────────────── + // COMMON HELPERS (USED ACROSS MULTIPLE BLOCKS) + // ─────────────────────────────────────────────────────────────── + private findParentNode( + node: ts.Node, + predicate: (parent: ts.Node) => boolean + ): ts.Node | null { + let currentNode = node.parent; + 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; + } +}