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