1/* 2 * Copyright (c) 2021-2022 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16const fs = require('fs'); 17const path = require('path'); 18const { exportDiffInfo } = require('../../api_diff/src/api_diff'); 19const { StatusCode } = require('../../api_diff/src/reporter'); 20const { 21 parseJsDoc, 22 requireTypescriptModule, 23 ErrorType, 24 LogType, 25 ErrorLevel, 26 ErrorValueInfo, 27 getCheckApiVersion, 28 FUNCTION_TYPES, 29 DIFF_INFO, 30 createErrorInfo 31} = require('./utils'); 32const ts = requireTypescriptModule(); 33const { addAPICheckErrorLogs } = require('./compile_info'); 34 35const changeErrors = []; 36 37/** 38 * 检查历史JSDoc是否一致 39 * @param {array} newNodeJSDocs 修改后API节点JSDoc数组 40 * @param {array} oldNodeJSDocs 修改前API节点JSDoc数组 41 * @returns {boolean} 42 */ 43function checkHistoryJSDoc(newNodeJSDocs, oldNodeJSDocs) { 44 let checkEndJSDocIndex = isNewApi(oldNodeJSDocs) ? 1 : 0; 45 for (let i = 0; i < oldNodeJSDocs.length - checkEndJSDocIndex; i++) { 46 const oldDescription = oldNodeJSDocs[i].description; 47 const oldTags = oldNodeJSDocs[i].tags; 48 const newDescription = newNodeJSDocs[i].description; 49 const newTags = newNodeJSDocs[i].tags; 50 if (oldDescription !== newDescription || oldTags.length !== newTags.length) { 51 return false; 52 } 53 for (let j = 0; j < oldTags.length; j++) { 54 const oldTag = oldTags[j]; 55 const newTag = newTags[j]; 56 if ( 57 oldTag.tag !== newTag.tag || 58 oldTag.name !== newTag.name || 59 oldTag.type !== newTag.type || 60 oldTag.optional !== newTag.optional || 61 oldTag.description !== newTag.description 62 ) { 63 return false; 64 } 65 } 66 } 67 return true; 68} 69 70/** 71 * 根据JSDoc获取版本号 72 * @param {JSDoc} JSDoc 73 * @returns {number} 74 */ 75function getJSDocVersion(JSDoc) { 76 if (JSDoc) { 77 for (let i = 0; i < JSDoc.tags.length; i++) { 78 if (JSDoc.tags[i].tag === 'since') { 79 return JSDoc.tags[i].name; 80 } 81 } 82 } 83 return NaN; 84} 85 86/** 87 * 检查API变更版本是否正确 88 * @param {JSDoc} currentJSDoc 修改后API节点JSDoc 89 * @param {JSDoc} lastJSDoc 修改前API节点JSDoc 90 * @param {ts.Node} node 修改后API节点 91 */ 92function checkApiChangeVersion(currentJSDoc, lastJSDoc, node) { 93 const currentVersion = getJSDocVersion(currentJSDoc); 94 const lastVersion = getJSDocVersion(lastJSDoc); 95 const checkApiVersion = getCheckApiVersion(); 96 if (lastVersion === 0 || currentVersion !== checkApiVersion) { 97 changeErrors.push({ 98 node: node, 99 errorInfo: createErrorInfo(ErrorValueInfo.ERROR_CHANGES_VERSION, [checkApiVersion]), 100 LogType: LogType.LOG_JSDOC, 101 }); 102 } 103} 104 105/** 106 * 检查JSDoc变更 107 * @param {string} tagName 标签名称 108 * @param {JSDoc} currentJSDoc 修改后API节点JSDoc 109 * @param {JSDoc} lastJSDoc 修改前API节点JSDoc 110 * @param {function} customCheckCallback 自定义检查规则 111 */ 112function checkJSDocChange(tagName, currentJSDoc, lastJSDoc, customCheckCallback) { 113 const newTagValue = []; 114 const oldTagValue = []; 115 const addTags = []; 116 if (currentJSDoc) { 117 currentJSDoc.tags.forEach(tag => { 118 if (tag.tag === tagName) { 119 newTagValue.push(tag.name); 120 } 121 }); 122 } 123 if (lastJSDoc) { 124 lastJSDoc.tags.forEach(tag => { 125 if (tag.tag === tagName) { 126 oldTagValue.push(tag.name); 127 } 128 }); 129 } 130 newTagValue.forEach(newValue => { 131 if (!new Set(oldTagValue).has(newValue)) { 132 addTags.push(newValue); 133 } 134 }); 135 136 customCheckCallback(newTagValue, oldTagValue, addTags); 137} 138 139/** 140 * 检查权限变化 141 * @param { string } newPermission 142 * @param { string } oldPermission 143 * @returns { boolean } 144 */ 145function checkPermissionChange(newPermission, oldPermission) { 146 const permissionChange = newPermission.replace(oldPermission, ''); 147 return !newPermission.includes(oldPermission) || /\band\b/.test(permissionChange); 148} 149 150/** 151 * 检查JSDoc变更 152 * @param {array} newNodeJSDocs 修改后API节点JSDoc列表 153 * @param {enum} statusCode api_diff工具返回的变更状态 154 * @param {ts.Node} node 修改后API节点 155 */ 156function checkCurrentJSDocChange(newNodeJSDocs, statusCode, node) { 157 const currentJSDoc = newNodeJSDocs[newNodeJSDocs.length - 1]; 158 const lastJSDoc = 159 newNodeJSDocs.length === DIFF_INFO.NEW_JSDOCS_LENGTH 160 ? null 161 : newNodeJSDocs[newNodeJSDocs.length - DIFF_INFO.NEW_JSDOC_INDEX]; 162 163 checkApiChangeVersion(currentJSDoc, lastJSDoc, node); 164 165 if (statusCode === StatusCode.ERRORCODE_CHANGES || statusCode === StatusCode.NEW_ERRORCODE) { 166 checkJSDocChange('throws', currentJSDoc, lastJSDoc, (newTagValue, oldTagValue, addTags) => { 167 if (addTags.length !== 0 && oldTagValue.length === 0) { 168 changeErrors.push({ 169 node: node, 170 errorInfo: ErrorValueInfo.ERROR_CHANGES_JSDOC_TRROWS, 171 LogType: LogType.LOG_JSDOC, 172 }); 173 } 174 }); 175 } else if (statusCode === StatusCode.PERMISSION_CHANGES) { 176 checkJSDocChange('permission', currentJSDoc, lastJSDoc, (newTagValue, oldTagValue, addTags) => { 177 let checkResult = true; 178 // 从无到有新增权限 179 checkResult = !(newTagValue.length === 1 && oldTagValue.length === 0); 180 // 权限值变更 181 if (newTagValue.length === 1 && oldTagValue.length === 1) { 182 const newPermission = newTagValue[0]; 183 const oldPermission = oldTagValue[0]; 184 checkResult = checkResult && 185 !(newPermission !== oldPermission && checkPermissionChange(newPermission, oldPermission)); 186 } 187 188 if (!checkResult) { 189 changeErrors.push({ 190 node: node, 191 errorInfo: ErrorValueInfo.ERROR_CHANGES_JSDOC_PERMISSION, 192 LogType: LogType.LOG_JSDOC, 193 }); 194 } 195 }); 196 } 197} 198 199/** 200 * 检查API历史版本JSDoc是否包含废弃标签 201 * 202 * @param {array} historyJSDocs 历史接口JSDoc信息 203 * @returns {boolean} 204 */ 205function checkApiDeprecatedStatus(historyJSDocs) { 206 for (let i = 0; i < historyJSDocs.length; i++) { 207 const doc = historyJSDocs[i]; 208 for (let j = 0; j < doc.tags.length; j++) { 209 const tag = doc.tags[j]; 210 if (tag === 'deprecated') { 211 return true; 212 } 213 } 214 } 215 return false; 216} 217 218/** 219 * 检查JSDoc变更内容 220 * @param {array} newNodeJSDocs 修改后API节点JSDoc数组 221 * @param {array} oldNodeJSDocs 修改前API节点JSDoc数组 222 * @param {object} change api_diff获取的变更数据 223 */ 224function checkJSDocChangeInfo(newNodeJSDocs, oldNodeJSDocs, change) { 225 if (checkApiDeprecatedStatus(oldNodeJSDocs)) { 226 changeErrors.push({ 227 node: change.newNode, 228 errorInfo: ErrorValueInfo.ERROR_CHANGES_DEPRECATED, 229 LogType: LogType.LOG_JSDOC, 230 }); 231 } 232 if (newNodeJSDocs.length !== oldNodeJSDocs.length + 1 && !isNewApi(oldNodeJSDocs)) { 233 changeErrors.push({ 234 node: change.newNode, 235 errorInfo: ErrorValueInfo.ERROR_CHANGES_JSDOC_NUMBER, 236 LogType: LogType.LOG_JSDOC, 237 }); 238 } else if (!checkHistoryJSDoc(newNodeJSDocs, oldNodeJSDocs)) { 239 changeErrors.push({ 240 node: change.newNode, 241 errorInfo: ErrorValueInfo.ERROR_CHANGES_JSDOC_CHANGE, 242 LogType: LogType.LOG_JSDOC, 243 }); 244 } else { 245 checkCurrentJSDocChange(newNodeJSDocs, change.statusCode, change.newNode); 246 } 247} 248 249/** 250 * 检查JSDoc变更 251 * @param {object} change api_diff获取的变更数据 252 */ 253function checkJSDocChangeEntry(change) { 254 const newNodeJSDocs = parseJsDoc(change.newNode); 255 const oldNodeJSDocs = parseJsDoc(change.oldNode); 256 257 checkJSDocChangeInfo(newNodeJSDocs, oldNodeJSDocs, change); 258} 259 260/** 261 * 解析接口参数类型数据 262 * @param {array} paramType 接口参数类型 263 * @returns {array} 264 */ 265function analysisParamType(paramType) { 266 const types = []; 267 if (paramType.kind === ts.SyntaxKind.UnionType) { 268 paramType.types.forEach(type => { 269 types.push(type.getText()); 270 }); 271 } else { 272 types.push(paramType.getText()); 273 } 274 return types; 275} 276 277/** 278 * 检查API接口历史参数 279 * @param {array} currentParameters 修改后API节点参数 280 * @param {array} lastParameters 修改前API节点参数 281 */ 282function checkHistoryParameters(currentParameters, lastParameters, change) { 283 for (let i = currentParameters.length - 1; i >= 0; i--) { 284 const historyParamType = analysisParamType(lastParameters[i].type); 285 const currentParamType = analysisParamType(currentParameters[i].type); 286 287 // 拦截可选变必选 288 if (currentParameters[i].isRequired && !lastParameters[i].isRequired) { 289 changeErrors.push({ 290 node: change.newNode, 291 errorInfo: ErrorValueInfo.ERROR_CHANGES_API_HISTORY_PARAM_REQUIRED_CHANGE, 292 LogType: LogType.LOG_API, 293 }); 294 } 295 296 // 变更后参数无类型定义 297 if (currentParamType.length === 0) { 298 changeErrors.push({ 299 node: change.newNode, 300 errorInfo: ErrorValueInfo.ERROR_CHANGES_API_HISTORY_PARAM_WITHOUT_TYPE_CHANGE, 301 LogType: LogType.LOG_API, 302 }); 303 // 变更后参数范围大于等于变更前 304 } else if (currentParamType.length >= historyParamType.length) { 305 checkHistoryParametersType(historyParamType, currentParamType, changeErrors, change); 306 // 变更后参数范围小于变更前 307 } else { 308 changeErrors.push({ 309 node: change.newNode, 310 errorInfo: ErrorValueInfo.ERROR_CHANGES_API_HISTORY_PARAM_RANGE_CHANGE, 311 LogType: LogType.LOG_API, 312 }); 313 } 314 } 315 316 // 拦截参数位置变更 317 for (let i = 0; i < currentParameters.length; i++) { 318 for (let j = 0; j < lastParameters.length; j++) { 319 if ( 320 currentParameters[i].paramName === lastParameters[j].paramName && 321 currentParameters[i].order !== lastParameters[j].order 322 ) { 323 changeErrors.push({ 324 node: change.newNode, 325 errorInfo: ErrorValueInfo.ERROR_CHANGES_API_HISTORY_PARAM_POSITION_CHANGE, 326 LogType: LogType.LOG_API, 327 }); 328 } 329 } 330 } 331} 332 333function checkHistoryParametersType(historyParamType, currentParamType, changeErrors, change) { 334 for (let j = 0; j < historyParamType.length; j++) { 335 if (!new Set(currentParamType).has(historyParamType[j])) { 336 changeErrors.push({ 337 node: change.newNode, 338 errorInfo: ErrorValueInfo.ERROR_CHANGES_API_HISTORY_PARAM_TYPE_CHANGE, 339 LogType: LogType.LOG_API, 340 }); 341 } 342 } 343} 344 345/** 346 * 检查API接口新增参数 347 * @param {array} parameters 新增参数列表 348 */ 349function checkCurrentParameters(parameters, change) { 350 for (let i = 0; i < parameters.length; i++) { 351 if (parameters[i].isRequired) { 352 changeErrors.push({ 353 node: change.newNode, 354 errorInfo: ErrorValueInfo.ERROR_CHANGES_API_NEW_REQUIRED_PARAM, 355 LogType: LogType.LOG_API, 356 }); 357 break; 358 } 359 } 360} 361 362/** 363 * 解析接口参数数据 364 * @param {array} params tsNode参数列表 365 * @returns {array} 366 */ 367function analysisParameters(params) { 368 const paramInfoData = []; 369 params.forEach((param, index) => { 370 const data = { 371 paramName: param.name ? param.name.getText() : '', 372 order: index, 373 isRequired: param.questionToken && param.questionToken.kind === ts.SyntaxKind.QuestionToken ? false : true, 374 type: param.type, 375 }; 376 paramInfoData.push(data); 377 }); 378 return paramInfoData; 379} 380 381/** 382 * 判断是否为新增接口或已变更为最新版本接口 383 * @param {array} oldNodeJSDocs 修改前API节点JSDoc数组 384 */ 385function isNewApi(oldNodeJSDocs) { 386 const checkApiVersion = getCheckApiVersion(); 387 const oldNodeVersion = getJSDocVersion(oldNodeJSDocs[oldNodeJSDocs.length - 1]); 388 389 if (oldNodeVersion === checkApiVersion) { 390 return true; 391 } 392 return false; 393} 394 395/** 396 * 检查API变更 397 * @param {object} change api_diff获取的变更数据 398 */ 399function checkApiChangeEntry(change) { 400 // 检查JSDoc 401 const newNodeJSDocs = parseJsDoc(change.newNode); 402 const oldNodeJSDocs = parseJsDoc(change.oldNode); 403 404 checkJSDocChangeInfo(newNodeJSDocs, oldNodeJSDocs, change); 405 406 // 新增接口不检查接口变更 407 if (isNewApi(oldNodeJSDocs) && oldNodeJSDocs.length === 1) { 408 return; 409 } 410 const currentParameters = analysisParameters(change.newNode.parameters); 411 const lastParameters = analysisParameters(change.oldNode.parameters); 412 413 if (currentParameters.length === lastParameters.length) { 414 checkHistoryParameters(currentParameters, lastParameters, change); 415 } else if (currentParameters.length > lastParameters.length) { 416 if (lastParameters.length !== 0) { 417 checkHistoryParameters(currentParameters.slice(0, lastParameters.length), lastParameters, change); 418 } 419 checkCurrentParameters(currentParameters.slice(lastParameters.length, currentParameters.length), change); 420 } else { 421 changeErrors.push({ 422 node: change.newNode, 423 errorInfo: ErrorValueInfo.ERROR_CHANGES_API_DELETE_PARAM, 424 LogType: LogType.LOG_API, 425 }); 426 } 427} 428 429/** 430 * 分析变更内容 431 * @param {array} changes api_diff获取的变更数据列表 432 */ 433function analyseChanges(changes) { 434 const functionTypeSet = new Set(FUNCTION_TYPES); 435 changes.forEach(change => { 436 if ( 437 change.statusCode === StatusCode.ERRORCODE_CHANGES || 438 change.statusCode === StatusCode.NEW_ERRORCODE || 439 change.statusCode === StatusCode.PERMISSION_CHANGES 440 ) { 441 checkJSDocChangeEntry(change); 442 } else if ( 443 change.statusCode === StatusCode.FUNCTION_CHANGES && 444 functionTypeSet.has(change.oldNode.kind) && 445 functionTypeSet.has(change.newNode.kind) 446 ) { 447 checkApiChangeEntry(change); 448 } 449 }); 450} 451 452/** 453 * 封装错误信息 454 */ 455function logChangeErrors() { 456 changeErrors.forEach(error => { 457 const sourceFileNode = ts.getSourceFileOfNode(error.node); 458 addAPICheckErrorLogs( 459 error.node, 460 sourceFileNode, 461 sourceFileNode.fileName, 462 ErrorType.API_CHANGE_ERRORS, 463 error.errorInfo, 464 error.LogType, 465 ErrorLevel.MIDDLE 466 ); 467 }); 468} 469 470/** 471 * API变更检查入口 472 */ 473function checkApiChanges(prId) { 474 let isApiChanged = false; 475 const oldFiles = []; 476 // 编译流水线根目录 477 const rootDir = path.resolve(__dirname, `../../../../../Archive/patch_info/openharmony_interface_sdk-js_${prId}`); 478 if (!fs.existsSync(rootDir)) { 479 return; 480 } 481 const oldApiPath = path.resolve(rootDir, './old'); 482 const newFiles = []; 483 const newApiPath = path.resolve(rootDir, './new'); 484 const fileNames = fs.readdirSync(rootDir); 485 let patchConfigPath = ''; 486 for (let i = 0; i < fileNames.length; i++) { 487 if (/\.json$/.test(fileNames[i])) { 488 patchConfigPath = path.resolve(rootDir, fileNames[i]); 489 break; 490 } 491 } 492 const patchConfig = JSON.parse(fs.readFileSync(patchConfigPath)); 493 for (const file in patchConfig) { 494 // 判断为文件修改 495 if (patchConfig[file] === 'M' && /\.d\.ts$/.test(file)) { 496 const oldMdFilePath = path.resolve(oldApiPath, file); 497 const newMdFilePath = path.resolve(newApiPath, file); 498 499 if (fs.existsSync(oldMdFilePath) && fs.existsSync(newMdFilePath)) { 500 oldFiles.push(oldMdFilePath); 501 newFiles.push(newMdFilePath); 502 isApiChanged = true; 503 } 504 } 505 } 506 507 if (isApiChanged) { 508 const diffResult = exportDiffInfo(newFiles, oldFiles, newApiPath, oldApiPath); 509 analyseChanges(diffResult); 510 logChangeErrors(); 511 } 512} 513exports.checkApiChanges = checkApiChanges; 514