/* * Copyright (c) 2021-2022 Huawei Device Co., Ltd. * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ const fs = require('fs'); const path = require('path'); const { exportDiffInfo } = require('../../api_diff/src/api_diff'); const { StatusCode } = require('../../api_diff/src/reporter'); const { parseJsDoc, requireTypescriptModule, ErrorType, LogType, ErrorLevel, ErrorValueInfo, getCheckApiVersion, FUNCTION_TYPES, DIFF_INFO, createErrorInfo } = require('./utils'); const ts = requireTypescriptModule(); const { addAPICheckErrorLogs } = require('./compile_info'); const changeErrors = []; /** * 检查历史JSDoc是否一致 * @param {array} newNodeJSDocs 修改后API节点JSDoc数组 * @param {array} oldNodeJSDocs 修改前API节点JSDoc数组 * @returns {boolean} */ function checkHistoryJSDoc(newNodeJSDocs, oldNodeJSDocs) { let checkEndJSDocIndex = isNewApi(oldNodeJSDocs) ? 1 : 0; for (let i = 0; i < oldNodeJSDocs.length - checkEndJSDocIndex; i++) { const oldDescription = oldNodeJSDocs[i].description; const oldTags = oldNodeJSDocs[i].tags; const newDescription = newNodeJSDocs[i].description; const newTags = newNodeJSDocs[i].tags; if (oldDescription !== newDescription || oldTags.length !== newTags.length) { return false; } for (let j = 0; j < oldTags.length; j++) { const oldTag = oldTags[j]; const newTag = newTags[j]; if ( oldTag.tag !== newTag.tag || oldTag.name !== newTag.name || oldTag.type !== newTag.type || oldTag.optional !== newTag.optional || oldTag.description !== newTag.description ) { return false; } } } return true; } /** * 根据JSDoc获取版本号 * @param {JSDoc} JSDoc * @returns {number} */ function getJSDocVersion(JSDoc) { if (JSDoc) { for (let i = 0; i < JSDoc.tags.length; i++) { if (JSDoc.tags[i].tag === 'since') { return JSDoc.tags[i].name; } } } return NaN; } /** * 检查API变更版本是否正确 * @param {JSDoc} currentJSDoc 修改后API节点JSDoc * @param {JSDoc} lastJSDoc 修改前API节点JSDoc * @param {ts.Node} node 修改后API节点 */ function checkApiChangeVersion(currentJSDoc, lastJSDoc, node) { const currentVersion = getJSDocVersion(currentJSDoc); const lastVersion = getJSDocVersion(lastJSDoc); const checkApiVersion = getCheckApiVersion(); if (lastVersion === 0 || currentVersion !== checkApiVersion) { changeErrors.push({ node: node, errorInfo: createErrorInfo(ErrorValueInfo.ERROR_CHANGES_VERSION, [checkApiVersion]), LogType: LogType.LOG_JSDOC, }); } } /** * 检查JSDoc变更 * @param {string} tagName 标签名称 * @param {JSDoc} currentJSDoc 修改后API节点JSDoc * @param {JSDoc} lastJSDoc 修改前API节点JSDoc * @param {function} customCheckCallback 自定义检查规则 */ function checkJSDocChange(tagName, currentJSDoc, lastJSDoc, customCheckCallback) { const newTagValue = []; const oldTagValue = []; const addTags = []; if (currentJSDoc) { currentJSDoc.tags.forEach(tag => { if (tag.tag === tagName) { newTagValue.push(tag.name); } }); } if (lastJSDoc) { lastJSDoc.tags.forEach(tag => { if (tag.tag === tagName) { oldTagValue.push(tag.name); } }); } newTagValue.forEach(newValue => { if (!new Set(oldTagValue).has(newValue)) { addTags.push(newValue); } }); customCheckCallback(newTagValue, oldTagValue, addTags); } /** * 检查权限变化 * @param { string } newPermission * @param { string } oldPermission * @returns { boolean } */ function checkPermissionChange(newPermission, oldPermission) { const permissionChange = newPermission.replace(oldPermission, ''); return !newPermission.includes(oldPermission) || /\band\b/.test(permissionChange); } /** * 检查JSDoc变更 * @param {array} newNodeJSDocs 修改后API节点JSDoc列表 * @param {enum} statusCode api_diff工具返回的变更状态 * @param {ts.Node} node 修改后API节点 */ function checkCurrentJSDocChange(newNodeJSDocs, statusCode, node) { const currentJSDoc = newNodeJSDocs[newNodeJSDocs.length - 1]; const lastJSDoc = newNodeJSDocs.length === DIFF_INFO.NEW_JSDOCS_LENGTH ? null : newNodeJSDocs[newNodeJSDocs.length - DIFF_INFO.NEW_JSDOC_INDEX]; checkApiChangeVersion(currentJSDoc, lastJSDoc, node); if (statusCode === StatusCode.ERRORCODE_CHANGES || statusCode === StatusCode.NEW_ERRORCODE) { checkJSDocChange('throws', currentJSDoc, lastJSDoc, (newTagValue, oldTagValue, addTags) => { if (addTags.length !== 0 && oldTagValue.length === 0) { changeErrors.push({ node: node, errorInfo: ErrorValueInfo.ERROR_CHANGES_JSDOC_TRROWS, LogType: LogType.LOG_JSDOC, }); } }); } else if (statusCode === StatusCode.PERMISSION_CHANGES) { checkJSDocChange('permission', currentJSDoc, lastJSDoc, (newTagValue, oldTagValue, addTags) => { let checkResult = true; // 从无到有新增权限 checkResult = !(newTagValue.length === 1 && oldTagValue.length === 0); // 权限值变更 if (newTagValue.length === 1 && oldTagValue.length === 1) { const newPermission = newTagValue[0]; const oldPermission = oldTagValue[0]; checkResult = checkResult && !(newPermission !== oldPermission && checkPermissionChange(newPermission, oldPermission)); } if (!checkResult) { changeErrors.push({ node: node, errorInfo: ErrorValueInfo.ERROR_CHANGES_JSDOC_PERMISSION, LogType: LogType.LOG_JSDOC, }); } }); } } /** * 检查API历史版本JSDoc是否包含废弃标签 * * @param {array} historyJSDocs 历史接口JSDoc信息 * @returns {boolean} */ function checkApiDeprecatedStatus(historyJSDocs) { for (let i = 0; i < historyJSDocs.length; i++) { const doc = historyJSDocs[i]; for (let j = 0; j < doc.tags.length; j++) { const tag = doc.tags[j]; if (tag === 'deprecated') { return true; } } } return false; } /** * 检查JSDoc变更内容 * @param {array} newNodeJSDocs 修改后API节点JSDoc数组 * @param {array} oldNodeJSDocs 修改前API节点JSDoc数组 * @param {object} change api_diff获取的变更数据 */ function checkJSDocChangeInfo(newNodeJSDocs, oldNodeJSDocs, change) { if (checkApiDeprecatedStatus(oldNodeJSDocs)) { changeErrors.push({ node: change.newNode, errorInfo: ErrorValueInfo.ERROR_CHANGES_DEPRECATED, LogType: LogType.LOG_JSDOC, }); } if (newNodeJSDocs.length !== oldNodeJSDocs.length + 1 && !isNewApi(oldNodeJSDocs)) { changeErrors.push({ node: change.newNode, errorInfo: ErrorValueInfo.ERROR_CHANGES_JSDOC_NUMBER, LogType: LogType.LOG_JSDOC, }); } else if (!checkHistoryJSDoc(newNodeJSDocs, oldNodeJSDocs)) { changeErrors.push({ node: change.newNode, errorInfo: ErrorValueInfo.ERROR_CHANGES_JSDOC_CHANGE, LogType: LogType.LOG_JSDOC, }); } else { checkCurrentJSDocChange(newNodeJSDocs, change.statusCode, change.newNode); } } /** * 检查JSDoc变更 * @param {object} change api_diff获取的变更数据 */ function checkJSDocChangeEntry(change) { const newNodeJSDocs = parseJsDoc(change.newNode); const oldNodeJSDocs = parseJsDoc(change.oldNode); checkJSDocChangeInfo(newNodeJSDocs, oldNodeJSDocs, change); } /** * 解析接口参数类型数据 * @param {array} paramType 接口参数类型 * @returns {array} */ function analysisParamType(paramType) { const types = []; if (paramType.kind === ts.SyntaxKind.UnionType) { paramType.types.forEach(type => { types.push(type.getText()); }); } else { types.push(paramType.getText()); } return types; } /** * 检查API接口历史参数 * @param {array} currentParameters 修改后API节点参数 * @param {array} lastParameters 修改前API节点参数 */ function checkHistoryParameters(currentParameters, lastParameters, change) { for (let i = currentParameters.length - 1; i >= 0; i--) { const historyParamType = analysisParamType(lastParameters[i].type); const currentParamType = analysisParamType(currentParameters[i].type); // 拦截可选变必选 if (currentParameters[i].isRequired && !lastParameters[i].isRequired) { changeErrors.push({ node: change.newNode, errorInfo: ErrorValueInfo.ERROR_CHANGES_API_HISTORY_PARAM_REQUIRED_CHANGE, LogType: LogType.LOG_API, }); } // 变更后参数无类型定义 if (currentParamType.length === 0) { changeErrors.push({ node: change.newNode, errorInfo: ErrorValueInfo.ERROR_CHANGES_API_HISTORY_PARAM_WITHOUT_TYPE_CHANGE, LogType: LogType.LOG_API, }); // 变更后参数范围大于等于变更前 } else if (currentParamType.length >= historyParamType.length) { checkHistoryParametersType(historyParamType, currentParamType, changeErrors, change); // 变更后参数范围小于变更前 } else { changeErrors.push({ node: change.newNode, errorInfo: ErrorValueInfo.ERROR_CHANGES_API_HISTORY_PARAM_RANGE_CHANGE, LogType: LogType.LOG_API, }); } } // 拦截参数位置变更 for (let i = 0; i < currentParameters.length; i++) { for (let j = 0; j < lastParameters.length; j++) { if ( currentParameters[i].paramName === lastParameters[j].paramName && currentParameters[i].order !== lastParameters[j].order ) { changeErrors.push({ node: change.newNode, errorInfo: ErrorValueInfo.ERROR_CHANGES_API_HISTORY_PARAM_POSITION_CHANGE, LogType: LogType.LOG_API, }); } } } } function checkHistoryParametersType(historyParamType, currentParamType, changeErrors, change) { for (let j = 0; j < historyParamType.length; j++) { if (!new Set(currentParamType).has(historyParamType[j])) { changeErrors.push({ node: change.newNode, errorInfo: ErrorValueInfo.ERROR_CHANGES_API_HISTORY_PARAM_TYPE_CHANGE, LogType: LogType.LOG_API, }); } } } /** * 检查API接口新增参数 * @param {array} parameters 新增参数列表 */ function checkCurrentParameters(parameters, change) { for (let i = 0; i < parameters.length; i++) { if (parameters[i].isRequired) { changeErrors.push({ node: change.newNode, errorInfo: ErrorValueInfo.ERROR_CHANGES_API_NEW_REQUIRED_PARAM, LogType: LogType.LOG_API, }); break; } } } /** * 解析接口参数数据 * @param {array} params tsNode参数列表 * @returns {array} */ function analysisParameters(params) { const paramInfoData = []; params.forEach((param, index) => { const data = { paramName: param.name ? param.name.getText() : '', order: index, isRequired: param.questionToken && param.questionToken.kind === ts.SyntaxKind.QuestionToken ? false : true, type: param.type, }; paramInfoData.push(data); }); return paramInfoData; } /** * 判断是否为新增接口或已变更为最新版本接口 * @param {array} oldNodeJSDocs 修改前API节点JSDoc数组 */ function isNewApi(oldNodeJSDocs) { const checkApiVersion = getCheckApiVersion(); const oldNodeVersion = getJSDocVersion(oldNodeJSDocs[oldNodeJSDocs.length - 1]); if (oldNodeVersion === checkApiVersion) { return true; } return false; } /** * 检查API变更 * @param {object} change api_diff获取的变更数据 */ function checkApiChangeEntry(change) { // 检查JSDoc const newNodeJSDocs = parseJsDoc(change.newNode); const oldNodeJSDocs = parseJsDoc(change.oldNode); checkJSDocChangeInfo(newNodeJSDocs, oldNodeJSDocs, change); // 新增接口不检查接口变更 if (isNewApi(oldNodeJSDocs) && oldNodeJSDocs.length === 1) { return; } const currentParameters = analysisParameters(change.newNode.parameters); const lastParameters = analysisParameters(change.oldNode.parameters); if (currentParameters.length === lastParameters.length) { checkHistoryParameters(currentParameters, lastParameters, change); } else if (currentParameters.length > lastParameters.length) { if (lastParameters.length !== 0) { checkHistoryParameters(currentParameters.slice(0, lastParameters.length), lastParameters, change); } checkCurrentParameters(currentParameters.slice(lastParameters.length, currentParameters.length), change); } else { changeErrors.push({ node: change.newNode, errorInfo: ErrorValueInfo.ERROR_CHANGES_API_DELETE_PARAM, LogType: LogType.LOG_API, }); } } /** * 分析变更内容 * @param {array} changes api_diff获取的变更数据列表 */ function analyseChanges(changes) { const functionTypeSet = new Set(FUNCTION_TYPES); changes.forEach(change => { if ( change.statusCode === StatusCode.ERRORCODE_CHANGES || change.statusCode === StatusCode.NEW_ERRORCODE || change.statusCode === StatusCode.PERMISSION_CHANGES ) { checkJSDocChangeEntry(change); } else if ( change.statusCode === StatusCode.FUNCTION_CHANGES && functionTypeSet.has(change.oldNode.kind) && functionTypeSet.has(change.newNode.kind) ) { checkApiChangeEntry(change); } }); } /** * 封装错误信息 */ function logChangeErrors() { changeErrors.forEach(error => { const sourceFileNode = ts.getSourceFileOfNode(error.node); addAPICheckErrorLogs( error.node, sourceFileNode, sourceFileNode.fileName, ErrorType.API_CHANGE_ERRORS, error.errorInfo, error.LogType, ErrorLevel.MIDDLE ); }); } /** * API变更检查入口 */ function checkApiChanges(prId) { let isApiChanged = false; const oldFiles = []; // 编译流水线根目录 const rootDir = path.resolve(__dirname, `../../../../../Archive/patch_info/openharmony_interface_sdk-js_${prId}`); if (!fs.existsSync(rootDir)) { return; } const oldApiPath = path.resolve(rootDir, './old'); const newFiles = []; const newApiPath = path.resolve(rootDir, './new'); const fileNames = fs.readdirSync(rootDir); let patchConfigPath = ''; for (let i = 0; i < fileNames.length; i++) { if (/\.json$/.test(fileNames[i])) { patchConfigPath = path.resolve(rootDir, fileNames[i]); break; } } const patchConfig = JSON.parse(fs.readFileSync(patchConfigPath)); for (const file in patchConfig) { // 判断为文件修改 if (patchConfig[file] === 'M' && /\.d\.ts$/.test(file)) { const oldMdFilePath = path.resolve(oldApiPath, file); const newMdFilePath = path.resolve(newApiPath, file); if (fs.existsSync(oldMdFilePath) && fs.existsSync(newMdFilePath)) { oldFiles.push(oldMdFilePath); newFiles.push(newMdFilePath); isApiChanged = true; } } } if (isApiChanged) { const diffResult = exportDiffInfo(newFiles, oldFiles, newApiPath, oldApiPath); analyseChanges(diffResult); logChangeErrors(); } } exports.checkApiChanges = checkApiChanges;