/* * Copyright (c) 2023 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 ts = require('typescript'); const { ApiDigestInfo } = require('./api_data'); const { DiffReporter } = require('./reporter'); const { StatusCode } = require('./reporter'); const { getCheckApiVersion } = require('../../api_check_plugin/src/utils') class TagItem { constructor() { } addSinceVersion(version) { this.setProperty('sinceVersion', version); } getSinceVersion() { return this.sinceVersion; } addSyscap(sysCap) { this.setProperty('sysCap', sysCap); } getSyscap() { return this.sysCap; } addAppModel(model) { this.setProperty('appModel', model); } getAppModel() { return this.appModel; } addDeprecated(deprecated) { this.setProperty('deprecated', deprecated); } getDeprecated() { return this.deprecated; } addErrorCode(errorCode) { this.setProperty('errorCode', errorCode); } getErrorCode() { return this.errorCode; } addApiLevel(level) { this.setProperty('apiLevel', level); } getApiLevel() { return this.apiLevel; } addTypeTag(type) { this.setProperty('type', type); } getTypeTag() { return this.type; } addUseInstead(useinstead) { this.setProperty('useinstead', useinstead); } getUseInstead() { return this.useinstead; } addPermission(permission) { this.setProperty('permission', permission); } getPermission() { return this.permission; } addForm(form) { this.setProperty('form', form); } getForm() { return this.form; } addCrossplatform(crossplatform) { this.setProperty('crossplatform', crossplatform); } getCrossplatform() { return this.crossplatform; } setProperty(name, value) { if (this[name]) { this[name].push(value); } else { this[name] = [value]; } } } function isArrayEquals(first, second) { if (!first && !second) { return true; } const ret = first && second && first.length === second.length; if (ret) { first.sort(); second.sort(); for (let index = 0; index < first.length; index++) { if (first[index] !== second[index]) { return false; } } } return ret; } function arrayToString(array) { if (!array || array.length === 0) { return ''; } return array.join(); } /** * 获å–API @deprecated æ ‡ç¾ä¿¡æ¯, 继承父类 * * @param {ApiDigestInfo} api * @returns {Array} */ function getApiDeprecated(api) { let curApi = api; while (curApi) { const jsdocTagItem = getTagItemFromJSDoc(curApi); if (jsdocTagItem.getDeprecated()) { return jsdocTagItem.getDeprecated(); } curApi = curApi.getParent(); } return []; } function matchSyscapInFile(api) { let syscap = getApiSyscap(api)[0]; let curApi = api; while (!syscap && !ts.isSourceFile(curApi.node)) { curApi = curApi.getParent(); } if (!syscap && ts.isSourceFile(curApi.node)) { const fileContent = curApi.node.getFullText(); if (/\@syscap\s*((\w|\.|\/|\{|\@|\}|\s)+)/g.test(fileContent)) { fileContent.replace(/\@syscap\s*((\w|\.|\/|\{|\@|\}|\s)+)/g, sysCapInfo => { syscap = sysCapInfo.replace(/\@syscap/g, '').trim(); }); } } return syscap; } /** * èŽ·å– API @syscap æ ‡ç¾ä¿¡æ¯ï¼Œç»§æ‰¿çˆ¶ç±» * * @param {ApiDigestInfo} api * @returns {Array} */ function getApiSyscap(api) { let curApi = api; while (curApi && !ts.isSourceFile(curApi.node)) { const jsdocTagItem = getTagItemFromJSDoc(curApi); if (jsdocTagItem.getSyscap()) { return jsdocTagItem.getSyscap(); } curApi = curApi.getParent(); } return []; } /** * 获å–API @useinstead,继承父类 * * @param {ApiDigestInfo} api * @returns {Array} */ function getApiUseInstead(api) { let curApi = api; while (curApi) { const jsdocTagItem = getTagItemFromJSDoc(curApi); if (jsdocTagItem.getUseInstead()) { return jsdocTagItem.getUseInstead(); } curApi = curApi.getParent(); } return []; } /** * 从 JSDoc 对象获å–所有Tagæ ‡ç¾ä¿¡æ¯ * * @param {ApiDigestInfo} api * @returns {Object} */ function getTagItemFromJSDoc(api) { let jsdocTagItem = api.getJSDocTagItem(); if (!jsdocTagItem) { jsdocTagItem = createTagItemFromJSDoc(api.jsdoc); api.setJSDocTagItem(jsdocTagItem); } return jsdocTagItem; } function wrapApiChanges(api, statusCode, oldMessage, newMessage, hint, oldNode, newNode, syscap) { return { api: api, statusCode: statusCode, oldMessage: oldMessage, newMessage: newMessage, hint: hint, oldNode: oldNode, newNode: newNode, syscap: syscap, }; } /** * 比较JSDoc的差异 * * @param {ApiDigestInfo} oldApi * @param {ApiDigestInfo} newApi * @param {DiffReporter} diffReporter */ function compareJSDocs(oldApi, newApi, diffReporter) { const oldTagItem = getTagItemFromJSDoc(oldApi); const newTagItem = getTagItemFromJSDoc(newApi); const useinstead = getApiUseInstead(newApi); const hint = useinstead.length > 0 ? `useinstead: ${useinstead[0]}` : ''; diffHistoricalJsDoc(oldApi, newApi, diffReporter, hint); diffErrorCode(diffReporter, oldTagItem, newTagItem, oldApi, newApi, hint); diffPermission(diffReporter, oldTagItem, newTagItem, oldApi, newApi, hint); diffForm(diffReporter, oldTagItem, newTagItem, oldApi, newApi, hint); diffCrossplatform(diffReporter, oldTagItem, newTagItem, oldApi, newApi, hint); diffDeprecated(diffReporter, oldApi, newApi, hint); diffApiLevel(diffReporter, oldTagItem, newTagItem, oldApi, newApi, hint); diffAppModel(diffReporter, oldTagItem, newTagItem, oldApi, newApi, hint); diffType(diffReporter, oldTagItem, newTagItem, oldApi, newApi, hint); } function getLatestVersion(jsdocs) { if (!jsdocs || jsdocs.length === 0) { return; } const jsdoc = jsdocs[jsdocs.length - 1]; let apiLatestVersion = ''; jsdoc.tags?.forEach(tagObject => { if (tagObject.tag === 'since') { apiLatestVersion = tagObject.name; } }) return apiLatestVersion; } function diffHistoricalJsDoc(oldApi, newApi, diffReporter, hint) { const currentVersion = getCheckApiVersion(); const oldJsDocTextArr = oldApi.getAstNode().getFullText().replace(oldApi.getAstNode().getText(), '').split('*/'); const newJsDocTextArr = newApi.getAstNode().getFullText().replace(oldApi.getAstNode().getText(), '').split('*/'); if (!oldApi.jsdoc) { return; } const oldLatestVersion = getLatestVersion(oldApi.jsdoc); const newLatestVersion = getLatestVersion(newApi.jsdoc); if (oldLatestVersion === currentVersion) { oldJsDocTextArr.splice(-2); } else { oldJsDocTextArr.splice(-1); } if (newLatestVersion === currentVersion) { newJsDocTextArr.splice(-2); } else { newJsDocTextArr.splice(-1); } if (oldJsDocTextArr.length !== newJsDocTextArr.length) { diffReporter.addDiffInfo(wrapApiChanges(newApi, StatusCode.HOSTORICAL_JSDOC_CHANGE, '', '', hint, oldApi.node, newApi.node )); return; } for (let i = 0; i < oldJsDocTextArr.length; i++) { if (oldJsDocTextArr[i].replace(/\r|\n|\s+/g, '') !== newJsDocTextArr[i].replace(/\r|\n|\s+/g, '')) { diffReporter.addDiffInfo(wrapApiChanges(newApi, StatusCode.HOSTORICAL_JSDOC_CHANGE, '', '', hint, oldApi.node, newApi.node )); } } } /** * 比较@type * * @param {DiffReporter} diffReporter * @param {TagItem} oldTagItem * @param {TagItem} newTagItem * @param {ApiDigestInfo} oldApi * @param {ApiDigestInfo} newApi * @param {string} hint */ function diffType(diffReporter, oldTagItem, newTagItem, oldApi, newApi, hint) { const oldType = oldTagItem.getTypeTag(); const newType = newTagItem.getTypeTag(); if (!isArrayEquals(oldType, newType)) { diffReporter.addChangedApi(wrapApiChanges(newApi, StatusCode.TYPE_CHNAGES, arrayToString(oldType), arrayToString(newType), hint, '', '', matchSyscapInFile(newApi) )); diffReporter.addDiffInfo(wrapApiChanges(newApi, StatusCode.TYPE_CHNAGES, arrayToString(oldType), arrayToString(newType), hint, oldApi.node, newApi.node )); } } /** * 比较@FAModelOnly @StageModelOnly * * @param {DiffReporter} diffReporter * @param {TagItem} oldTagItem * @param {TagItem} newTagItem * @param {ApiDigestInfo} oldApi * @param {ApiDigestInfo} newApi * @param {string} hint */ function diffAppModel(diffReporter, oldTagItem, newTagItem, oldApi, newApi, hint) { const oldAppModel = oldTagItem.getAppModel(); const newAppModel = newTagItem.getAppModel(); if (!isArrayEquals(oldAppModel, newTagItem.getAppModel())) { diffReporter.addChangedApi(wrapApiChanges(newApi, StatusCode.MODEL_CHNAGES, arrayToString(oldAppModel), arrayToString(newAppModel), hint, '', '', matchSyscapInFile(newApi) )); diffReporter.addDiffInfo(wrapApiChanges(newApi, StatusCode.MODEL_CHNAGES, arrayToString(oldAppModel), arrayToString(newAppModel), hint, oldApi.node, newApi.node )); } } /** * 比较@systemapi * * @param {DiffReporter} diffReporter * @param {TagItem} oldTagItem * @param {TagItem} newTagItem * @param {ApiDigestInfo} oldApi * @param {ApiDigestInfo} newApi * @param {string} hint */ function diffApiLevel(diffReporter, oldTagItem, newTagItem, oldApi, newApi, hint) { const oldApiLevel = oldTagItem.getApiLevel(); const newApiLevel = newTagItem.getApiLevel(); if (!isArrayEquals(oldApiLevel, newApiLevel)) { diffReporter.addChangedApi(wrapApiChanges( newApi, StatusCode.SYSTEM_API_CHNAGES, arrayToString(oldApiLevel), arrayToString(newApiLevel), hint, '', '', matchSyscapInFile(newApi) )); diffReporter.addDiffInfo(wrapApiChanges(newApi, StatusCode.SYSTEM_API_CHNAGES, arrayToString(oldApiLevel), arrayToString(newApiLevel), hint, oldApi.node, newApi.node )); } } /** * 比较@crossplatform * * @param {DiffReporter} diffReporter * @param {TagItem} oldTagItem * @param {TagItem} newTagItem * @param {ApiDigestInfo} oldApi * @param {ApiDigestInfo} newApi * @param {string} hint */ function diffCrossplatform(diffReporter, oldTagItem, newTagItem, oldApi, newApi, hint) { if (!isArrayEquals(oldTagItem.getCrossplatform(), newTagItem.getCrossplatform())) { diffReporter.addChangedApi(wrapApiChanges( newApi, StatusCode.CROSSPLATFORM_CHANGED, arrayToString(oldTagItem.getCrossplatform()), arrayToString(newTagItem.getCrossplatform()), hint, '', '', matchSyscapInFile(newApi) )); diffReporter.addDiffInfo(wrapApiChanges(newApi, StatusCode.CROSSPLATFORM_CHANGED, arrayToString(oldTagItem.getCrossplatform()), arrayToString(newTagItem.getCrossplatform()), hint, oldApi.node, newApi.node )); } } /** * 比较@form * * @param {DiffReporter} diffReporter * @param {TagItem} oldTagItem * @param {TagItem} newTagItem * @param {ApiDigestInfo} oldApi * @param {ApiDigestInfo} newApi * @param {string} hint */ function diffForm(diffReporter, oldTagItem, newTagItem, oldApi, newApi, hint) { if (!isArrayEquals(oldTagItem.getForm(), newTagItem.getForm())) { diffReporter.addChangedApi(wrapApiChanges( newApi, StatusCode.FORM_CHANGED, arrayToString(oldTagItem.getForm()), arrayToString(newTagItem.getForm()), hint, '', '', matchSyscapInFile(newApi) )); diffReporter.addDiffInfo(wrapApiChanges(newApi, StatusCode.FORM_CHANGED, arrayToString(oldTagItem.getForm()), arrayToString(newTagItem.getForm()), hint, oldApi.node, newApi.node )); } } /** * 比较@syscap * * @param {DiffReporter} diffReporter * @param {ApiDigestInfo} oldApi * @param {ApiDigestInfo} newApi * @param {string} hint */ function diffSyscap(diffReporter, oldApi, newApi, hint) { const oldSyscap = getApiSyscap(oldApi); const newSyscap = getApiSyscap(newApi); if (!isArrayEquals(oldSyscap, newSyscap)) { diffReporter.addChangedApi(wrapApiChanges( newApi, StatusCode.SYSCAP_CHANGES, arrayToString(oldSyscap), arrayToString(newSyscap), hint, '', '', matchSyscapInFile(newApi) )); diffReporter.addDiffInfo(wrapApiChanges( newApi, StatusCode.SYSCAP_CHANGES, arrayToString(oldSyscap), arrayToString(newSyscap), hint, oldApi.node, newApi.node )); } } /** * 比较@since * * @param {DiffReporter} diffReporter * @param {TagItem} oldTagItem * @param {TagItem} newTagItem * @param {ApiDigestInfo} oldApi * @param {ApiDigestInfo} newApi * @param {string} hint */ function diffSinceVersion(diffReporter, oldTagItem, newTagItem, oldApi, newApi, hint) { const oldVersion = oldTagItem.getSinceVersion(); const newVersion = newTagItem.getSinceVersion(); if (!isArrayEquals(oldVersion, newVersion)) { diffReporter.addChangedApi(wrapApiChanges( newApi, StatusCode.VERSION_CHNAGES, arrayToString(oldVersion), arrayToString(newVersion), hint, '', '', matchSyscapInFile(newApi) )); diffReporter.addDiffInfo(wrapApiChanges( newApi, StatusCode.VERSION_CHNAGES, arrayToString(oldVersion), arrayToString(newVersion), hint, oldApi.node, newApi.node )); } } /** * 比较@deprecated * * @param {DiffReporter} diffReporter * @param {ApiDigestInfo} oldApi * @param {ApiDigestInfo} newApi * @param {string} hint */ function diffDeprecated(diffReporter, oldApi, newApi, hint) { const oldDeprecated = getApiDeprecated(oldApi); const newDeprecated = getApiDeprecated(newApi); if (!isArrayEquals(oldDeprecated, newDeprecated)) { diffReporter.addChangedApi(wrapApiChanges( newApi, StatusCode.DEPRECATED_CHNAGES, arrayToString(oldDeprecated), arrayToString(newDeprecated), hint, '', '', matchSyscapInFile(newApi) )); diffReporter.addDiffInfo(wrapApiChanges( newApi, StatusCode.DEPRECATED_CHNAGES, arrayToString(oldDeprecated), arrayToString(newDeprecated), hint, oldApi.node, newApi.node )); } } /** * 比较æƒé™çš„差异@permission * * @param {DiffReporter} diffReporter * @param {TagItem} oldTagItem * @param {TagItem} newTagItem * @param {ApiDigestInfo} oldApi * @param {ApiDigestInfo} newApi * @param {string} hint */ function diffPermission(diffReporter, oldTagItem, newTagItem, oldApi, newApi, hint) { if (!isArrayEquals(oldTagItem.getPermission(), newTagItem.getPermission())) { diffReporter.addChangedApi(wrapApiChanges(newApi, StatusCode.PERMISSION_CHANGES, arrayToString(oldTagItem.getPermission()), arrayToString(newTagItem.getPermission()), hint, oldApi.node, newApi.node, matchSyscapInFile(newApi) )); diffReporter.addDiffInfo(wrapApiChanges(newApi, StatusCode.PERMISSION_CHANGES, arrayToString(oldTagItem.getPermission()), arrayToString(newTagItem.getPermission()), hint, oldApi.node, newApi.node )); } } /** * 比较错误ç 的差异@throws * * @param {DiffReporter} diffReporter * @param {TagItem} oldTagItem * @param {TagItem} newTagItem * @param {ApiDigestInfo} oldApi * @param {ApiDigestInfo} newApi * @param {string} hint */ function diffErrorCode(diffReporter, oldTagItem, newTagItem, oldApi, newApi, hint) { if (!isArrayEquals(oldTagItem.getErrorCode(), newTagItem.getErrorCode())) { let statusCode = ''; if (oldTagItem.getErrorCode() === undefined) { statusCode = StatusCode.NEW_ERRORCODE; } else { statusCode = StatusCode.ERRORCODE_CHANGES; } diffReporter.addChangedApi(wrapApiChanges( newApi, statusCode, arrayToString(oldTagItem.getErrorCode()), arrayToString(newTagItem.getErrorCode()), hint, oldApi.node, newApi.node, matchSyscapInFile(newApi) )); diffReporter.addDiffInfo(wrapApiChanges( newApi, statusCode, arrayToString(oldTagItem.getErrorCode()), arrayToString(newTagItem.getErrorCode()), hint, oldApi.node, newApi.node )); } } /** * è§£æž @since 7 * * @param {Object} tagObject * @param {TagItem} tagItem */ function appendSinceTag(tagObject, tagItem) { tagItem.addSinceVersion(tagObject.name ? tagObject.name : ''); } /** * è§£æž @syscap * * @param {Object} tagObject * @param {TagItem} tagItem */ function appendSyscapTag(tagObject, tagItem) { tagItem.addSyscap(tagObject.name ? tagObject.name : ''); } /** * è§£æž @permission * * @param {Object} tagObject * @param {TagItem} tagItem */ function appendPermissionTag(tagObject, tagItem) { const permissionRegExp = RegExp(/ohos\.permission\.\w+/g); let sourceText = ''; tagObject.source.forEach((src) => { sourceText += src.source; }); const permissionArray = sourceText.match(permissionRegExp); if (permissionArray) { permissionArray.forEach((permission) => { tagItem.addPermission(permission); }); } } /** * è§£æž @deprecated æ ‡ç¾ * * @param {Object} tagObject * @param {TagItem} tagItem */ function appendDeprecatedTag(tagObject, tagItem) { tagItem.addDeprecated(tagObject.description); } /** * è§£æž @FAModelOnly @StageModelOnly * * @param {Object} tagObject * @param {TagItem} tagItem */ function appendModelTag(tagObject, tagItem) { tagItem.addAppModel(tagObject.tag); } /** * è§£æž @systemapi * * @param {Object} tagObject * @param {TagItem} tagItem */ function appendApiLevelTag(tagObject, tagItem) { tagItem.addApiLevel(tagObject.tag); } /** * è§£æž @type {string} * * @param {Object} tagObject * @param {TagItem} tagItem */ function appendTypeTag(tagObject, tagItem) { tagItem.addTypeTag(tagObject.type); } /** * è§£æž @useinstedad æ ‡ç¾ * * @param {Object} tagObject * @param {TagItem} tagItem */ function appendUseInsteadTag(tagObject, tagItem) { tagItem.addUseInstead(tagObject.name); } /** * è§£æž @throws { BusinessError } 201 - æ ‡ç¾ * * @param {Object} tagObject * @param {TagItem} tagItem */ function appendErrorCodeTag(tagObject, tagItem) { tagItem.addErrorCode(tagObject.name); } /** * è§£æž @form - æ ‡ç¾ * * @param {Object} tagObject * @param {TagItem} tagItem */ function appendForm(tagObject, tagItem) { tagItem.addForm(tagObject.tag); } function appendCrossplatform(tagObject, tagItem) { tagItem.addCrossplatform(tagObject.tag); } const tagHandlerMap = new Map([ ['syscap', appendSyscapTag], ['permission', appendPermissionTag], ['deprecated', appendDeprecatedTag], ['famodelonly', appendModelTag], ['stagemodelonly', appendModelTag], ['systemapi', appendApiLevelTag], ['type', appendTypeTag], ['useinstead', appendUseInsteadTag], ['throws', appendErrorCodeTag], ['form', appendForm], ['crossplatform', appendCrossplatform] ]); /** * 从 comment-parser 库解æžçš„JSDoc对象ä¸æå–æ ‡ç¾ä¿¡æ¯ * * @param {Object} jsdocs */ function createTagItemFromJSDoc(jsdocs) { const tagItem = new TagItem(); if (!jsdocs || jsdocs.length === 0) { return tagItem; } const singleJSDoc = jsdocs[jsdocs.length - 1]; const firstJsDoc = jsdocs[0]; if (singleJSDoc.tags) { singleJSDoc.tags.forEach((tagObject) => { const handler = tagHandlerMap.get(tagObject.tag.toLowerCase()); if (handler) { handler(tagObject, tagItem); } }); } if (firstJsDoc.tags) { firstJsDoc.tags.forEach((tagObject) => { if (tagObject.tag.toLowerCase() === 'since') { appendSinceTag(tagObject, tagItem); } }); } return tagItem; } exports.JSDocDiffer = { collectJSDocDiffs: compareJSDocs, };