1/* 2 * Copyright (c) 2023 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 16import ts from 'typescript'; 17import { Code } from '../utils/constant'; 18import type { 19 Context, ISourceCodeProcessor, JsDocCheckResult, JsDocModificationInterface, ProcessResult, 20 LogReporter, IllegalTagsInfo, rawInfo, CheckLogResult, ModifyLogResult, comment 21} from './typedef'; 22import { JSDocModifyType, ErrorInfo, JSDocCheckErrorType } from './typedef'; 23import { CommentHelper, LogResult } from './coreImpls'; 24const apiChecker = require('api-checker'); 25 26/** 27 * JSDoc整改入口 28 */ 29export class CommentModificationProcessor implements ISourceCodeProcessor { 30 31 logReporter?: LogReporter; 32 context?: Context; 33 rawSourceCodeInfo?: rawInfo.RawSourceCodeInfo; 34 35 async process(context: Context, content: string): Promise<ProcessResult> { 36 this.context = context; 37 const newParser = context.getSourceParser(content); 38 this.logReporter = context.getLogReporter(); 39 this.rawSourceCodeInfo = context.getRawSourceCodeInfo(); 40 await apiChecker.initEnv(context.getOptions().workingBranch); 41 const newSourceFile = newParser.visitEachNodeComment(this, false); 42 return { 43 code: Code.OK, 44 content: newSourceFile ? newParser.printSourceFile(newSourceFile) : content 45 }; 46 } 47 48 onVisitNode(node: comment.CommentNode): void { 49 if (node.astNode) { 50 const curNode: ts.Node = node.astNode; 51 // 获取诊断信息 52 const checkResults = apiChecker.checkJSDoc(node.astNode, node.astNode?.getSourceFile(), this.context?.getInputFile()); 53 const newCommentIndexs: number[] = []; 54 const newCommentInfos: comment.CommentInfo[] = node.commentInfos ? [...node.commentInfos] : []; 55 // 获取需要整改的JSDoc数组 56 newCommentInfos?.forEach((commentInfo: comment.CommentInfo, index: number) => { 57 if (commentInfo.isApiComment) { 58 newCommentIndexs.push(index); 59 // 添加继承接口 60 this.addInheritTags(node, curNode, commentInfo, newCommentInfos, index === newCommentInfos.length - 1, 61 index + 1); 62 } 63 }); 64 if (newCommentIndexs.length > 0 && node.commentInfos) { 65 // JSDoc校验&整改 66 if (newCommentIndexs.length === checkResults.length) { 67 newCommentIndexs.forEach((item: number, index: number) => { 68 // 匹配处理校验结果与JSDoc数组 69 const checkResult: JsDocCheckResult = checkResults[index]; 70 const curCommentInfo: comment.CommentInfo = newCommentInfos[item]; 71 // 添加缺失标签 72 this.addMissingTags(node, curNode, curCommentInfo, newCommentInfos, checkResult.missingTags, index + 1); 73 // 输出诊断日志 74 this.exportIllegalInfo(curNode, newCommentInfos, checkResult.illegalTags, index + 1); 75 // 调整标签顺序 76 this.modifyTagsOrder(curNode, curCommentInfo, newCommentInfos, checkResult.orderResult.checkResult, 77 index + 1); 78 }); 79 } else { 80 // 捕获异常 81 const apiName: string = JSDocModificationManager.getApiName(curNode); 82 const checkLogResult: CheckLogResult = LogResult.createCheckResult(curNode, newCommentInfos, 83 JSDocModificationManager.createErrorInfo(ErrorInfo.JSDOC_FORMAT_ERROR, []), this.context, apiName, 84 JSDocCheckErrorType.API_FORMAT_ERROR); 85 this.logReporter?.addCheckResult(checkLogResult); 86 } 87 } 88 CommentHelper.setComment(node.astNode, newCommentInfos); 89 } 90 } 91 addInheritTags(node: comment.CommentNode, curNode: ts.Node, commentInfo: comment.CommentInfo, 92 newCommentInfos: comment.CommentInfo[], deprecatedCheckResult: boolean, jsdocNumber: number): void { 93 const apiName: string = JSDocModificationManager.getApiName(curNode); 94 INHERIT_TAGS_ARRAY.forEach((tagName: string) => { 95 if (tagName !== 'deprecated' || (tagName === 'deprecated' && deprecatedCheckResult)) { 96 const modifyResult: boolean = JSDocModificationManager.addTagFrommParentNode(node, commentInfo, tagName, 97 this.context); 98 if (modifyResult) { 99 if (tagName === 'permission') { 100 const checkLogResult: CheckLogResult = LogResult.createCheckResult(curNode, newCommentInfos, 101 JSDocModificationManager.createErrorInfo(ErrorInfo.COMPLETE_INHERIT_PERMISSION_TAG_ERROR, [`${jsdocNumber}`, `${tagName}`]), 102 this.context, apiName, JSDocCheckErrorType.INCOMPLETE_TAG); 103 this.logReporter?.addCheckResult(checkLogResult); 104 } else { 105 const modifyLogResult: ModifyLogResult = LogResult.createModifyResult(curNode, newCommentInfos, 106 JSDocModificationManager.createErrorInfo(ErrorInfo.COMPLETE_INHERIT_TAG_INFORMATION, [`${jsdocNumber}`, `${tagName}`]), 107 this.context, apiName, JSDocModifyType.MISSING_TAG_COMPLETION); 108 this.logReporter?.addModifyResult(modifyLogResult); 109 } 110 } 111 } 112 }); 113 } 114 addMissingTags(node: comment.CommentNode, curNode: ts.Node, curCommentInfo: comment.CommentInfo, 115 newCommentInfos: comment.CommentInfo[], missingTags: string[], jsdocNumber: number): void { 116 const apiName: string = JSDocModificationManager.getApiName(curNode); 117 missingTags.forEach((tagName: string) => { 118 const modifier = jsDocModifier.get(tagName); 119 let modifyResult: boolean = false; 120 if (modifier) { 121 modifyResult = modifier(node, curCommentInfo, tagName, this.context); 122 if (modifyResult) { 123 const modifyLogResult: ModifyLogResult = LogResult.createModifyResult(curNode, newCommentInfos, 124 JSDocModificationManager.createErrorInfo(ErrorInfo.COMPLETE_TAG_INFORMATION, [`${jsdocNumber}`, `${tagName}`]), 125 this.context, apiName, JSDocModifyType.MISSING_TAG_COMPLETION); 126 this.logReporter?.addModifyResult(modifyLogResult); 127 } 128 } 129 if (!modifier || !modifyResult) { 130 let modifyErrorInfo: string = ErrorInfo.COMPLETE_TAG_ERROR; 131 let insteadInfo: string[] = [`${jsdocNumber}`, `${tagName}`]; 132 if (tagName === 'interface') { 133 modifyErrorInfo = ErrorInfo.COMPLETE_INTERFACE_TAG_ERROR; 134 insteadInfo = [`${jsdocNumber}`]; 135 } 136 const checkLogResult: CheckLogResult = LogResult.createCheckResult(curNode, newCommentInfos, 137 JSDocModificationManager.createErrorInfo(modifyErrorInfo, insteadInfo), 138 this.context, apiName, JSDocCheckErrorType.INCOMPLETE_TAG); 139 this.logReporter?.addCheckResult(checkLogResult); 140 } 141 }); 142 } 143 modifyTagsOrder(curNode: ts.Node, curCommentInfo: comment.CommentInfo, newCommentInfos: comment.CommentInfo[], 144 orderResult: boolean, jsdocNumber: number): void { 145 const apiName: string = JSDocModificationManager.getApiName(curNode); 146 curCommentInfo.commentTags = JSDocModificationManager.modifyJsDocTagsOrder(curCommentInfo.commentTags); 147 if (!orderResult) { 148 const modifyLogResult: ModifyLogResult = LogResult.createModifyResult(curNode, newCommentInfos, 149 JSDocModificationManager.createErrorInfo(ErrorInfo.MODIFY_TAG_ORDER_INFORMATION, [`${jsdocNumber}`]), 150 this.context, apiName, JSDocModifyType.TAG_ORDER_AJUSTMENT); 151 this.logReporter?.addModifyResult(modifyLogResult); 152 } 153 } 154 exportIllegalInfo(curNode: ts.Node, newCommentInfos: comment.CommentInfo[], 155 illegalTags: IllegalTagsInfo[], jsdocNumber: number): void { 156 const apiName: string = JSDocModificationManager.getApiName(curNode); 157 illegalTags.forEach((illegalTag: IllegalTagsInfo) => { 158 if (!illegalTag.checkResult) { 159 const checkLogResult: CheckLogResult = LogResult.createCheckResult(curNode, newCommentInfos, 160 JSDocModificationManager.createErrorInfo(ErrorInfo.JSDOC_ILLEGAL_ERROR + illegalTag.errorInfo, [`${jsdocNumber}`]), 161 this.context, apiName, JSDocCheckErrorType.TAG_VALUE_ERROR); 162 this.logReporter?.addCheckResult(checkLogResult); 163 } 164 }); 165 } 166} 167 168/** 169 * JSDoc整改工具类 170 */ 171class JSDocModificationManager { 172 173 /** 174 * 获取commentInfo初始值 175 */ 176 static getNewCommentInfoObj(commentInfo: comment.CommentInfo): comment.CommentTag { 177 return { 178 tag: '', 179 name: '', 180 type: '', 181 optional: false, 182 description: '', 183 source: '', 184 lineNumber: commentInfo.commentTags.length > 0 ? 185 commentInfo.commentTags[commentInfo.commentTags.length - 1].lineNumber + 1 : 0, 186 tokenSource: [] 187 }; 188 } 189 190 /** 191 * 添加无值标签 192 */ 193 static addTagWithoutValue(node: comment.CommentNode, commentInfo: comment.CommentInfo, tagName: string, 194 context: Context | undefined): boolean { 195 const newCommentTag: comment.CommentTag = JSDocModificationManager.getNewCommentInfoObj(commentInfo); 196 newCommentTag.tag = tagName; 197 commentInfo.commentTags.push(newCommentTag); 198 return true; 199 } 200 201 /** 202 * 添加有值标签 203 */ 204 static addTagWithValue(node: comment.CommentNode, commentInfo: comment.CommentInfo, tagName: string, 205 context: Context | undefined): boolean { 206 const newCommentTag: comment.CommentTag = JSDocModificationManager.getNewCommentInfoObj(commentInfo); 207 newCommentTag.tag = tagName; 208 let tagValue = ''; 209 let tagType = ''; 210 if (node.astNode) { 211 if (tagName === 'returns' && (ts.isMethodDeclaration(node.astNode) || ts.isMethodSignature(node.astNode) || 212 ts.isFunctionDeclaration(node.astNode) || ts.isCallSignatureDeclaration(node.astNode)) && 213 node.astNode.type) { 214 if (ts.isTypeLiteralNode(node.astNode.type)) { 215 tagType = 'object'; 216 } else if (ts.isFunctionTypeNode(node.astNode.type)) { 217 tagType = 'function'; 218 } else { 219 tagType = node.astNode.type.getText(); 220 } 221 } else if ((ts.isModuleDeclaration(node.astNode) || ts.isEnumDeclaration(node.astNode)) && node.astNode.name && 222 ts.isIdentifier(node.astNode.name)) { 223 tagValue = node.astNode.name.escapedText.toString(); 224 } else if (ts.isClassDeclaration(node.astNode) && node.astNode.heritageClauses) { 225 const clauses = node.astNode.heritageClauses; 226 const extendClasses: string[] = []; 227 clauses.forEach((clause: ts.HeritageClause) => { 228 if (/^extends /.test(clause.getText()) && clause.types) { 229 clause.types.forEach((type: ts.ExpressionWithTypeArguments) => { 230 extendClasses.push(type.getText()); 231 }); 232 } 233 }); 234 tagValue = extendClasses.join(', '); 235 } 236 } 237 newCommentTag.name = tagValue; 238 newCommentTag.type = tagType; 239 commentInfo.commentTags.push(newCommentTag); 240 return true; 241 } 242 243 /** 244 * 添加继承标签 245 */ 246 static addTagFrommParentNode(node: comment.CommentNode, commentInfo: comment.CommentInfo, tagName: string, 247 context: Context | undefined): boolean { 248 let checkResult: boolean = false; 249 commentInfo.commentTags.forEach((tag: comment.CommentTag) => { 250 if (tag.tag === tagName) { 251 checkResult = true; 252 } 253 }); 254 if (checkResult) { 255 return false; 256 } 257 if (node.parentNode) { 258 const pTag: comment.CommentTag | undefined = getParentTag(node.parentNode, tagName); 259 if (pTag) { 260 if (tagName !== 'permission') { 261 commentInfo.commentTags.push(pTag); 262 } 263 return true; 264 } 265 } 266 return false; 267 function getParentTag(pNode: comment.CommentNode, tagName: string): comment.CommentTag | undefined { 268 if (pNode.commentInfos && pNode.commentInfos[pNode.commentInfos.length - 1] && 269 pNode.commentInfos[pNode.commentInfos.length - 1].commentTags) { 270 const pTags: comment.CommentTag[] = pNode.commentInfos[pNode.commentInfos.length - 1].commentTags; 271 for (let i = 0; i < pTags.length; i++) { 272 if (pTags[i].tag === tagName) { 273 return pTags[i]; 274 } 275 } 276 } 277 if (pNode.parentNode) { 278 return getParentTag(pNode.parentNode, tagName); 279 } 280 281 return undefined; 282 } 283 } 284 285 /** 286 * 添加param标签 287 */ 288 static addParamTag(node: comment.CommentNode, commentInfo: comment.CommentInfo, tagName: string, 289 context: Context | undefined): boolean { 290 if (node.astNode && (ts.isMethodDeclaration(node.astNode) || ts.isMethodSignature(node.astNode) || 291 ts.isFunctionDeclaration(node.astNode) || ts.isCallSignatureDeclaration(node.astNode) || 292 ts.isConstructorDeclaration(node.astNode))) { 293 let paramTagNum: number = 0; 294 commentInfo.commentTags.forEach((tag: comment.CommentTag) => { 295 if (tag.tag === 'param') { 296 paramTagNum++; 297 } 298 }); 299 const parameters = node.astNode.parameters; 300 for (let i = 0; i < parameters.length; i++) { 301 const newCommentTag: comment.CommentTag = JSDocModificationManager.getNewCommentInfoObj(commentInfo); 302 const curIndex: number = paramTagNum + i; 303 newCommentTag.tag = tagName; 304 const curParameter: ts.ParameterDeclaration = parameters[curIndex]; 305 if (curParameter) { 306 const apiName: string = node.astNode.name ? node.astNode.name.getText() : ''; 307 const commentInfos: comment.CommentInfo[] = node.commentInfos ? node.commentInfos : []; 308 let paramType: string = ''; 309 if (curParameter.type) { 310 if (ts.isTypeLiteralNode(curParameter.type)) { 311 paramType = 'object'; 312 } else if (ts.isFunctionTypeNode(curParameter.type)) { 313 paramType = 'function'; 314 } else { 315 paramType = curParameter.type.getText(); 316 } 317 } 318 newCommentTag.type = paramType; 319 newCommentTag.name = curParameter.name ? curParameter.name.getText() : ''; 320 commentInfo.commentTags.push(newCommentTag); 321 // 提示description缺失信息 322 const checkLogResult: CheckLogResult = LogResult.createCheckResult(node.astNode, commentInfos, 323 JSDocModificationManager.createErrorInfo(ErrorInfo.PARAM_FORAMT_DESCRIPTION_ERROR, [`${curIndex + 1}`]), context, apiName, 324 JSDocCheckErrorType.PARAM_DESCRIPTION_WARNING); 325 context?.getLogReporter().addCheckResult(checkLogResult); 326 } 327 } 328 } 329 return true; 330 } 331 332 /** 333 * 调整标签顺序 334 */ 335 static modifyJsDocTagsOrder(commentTags: comment.CommentTag[]): comment.CommentTag[] { 336 const orderSet = new Set(JSDOC_ORDER_TAGS_ARRAY); 337 const newTags: comment.CommentTag[] = []; 338 JSDOC_ORDER_TAGS_ARRAY.forEach((tagName: string) => { 339 commentTags.forEach((curTag: comment.CommentTag) => { 340 if (tagName === curTag.tag) { 341 newTags.push(curTag); 342 } 343 }); 344 }); 345 commentTags.forEach((tag: comment.CommentTag) => { 346 if (!orderSet.has(tag.tag)) { 347 newTags.push(tag); 348 } 349 }); 350 return newTags; 351 } 352 353 /** 354 * 组装错误信息 355 */ 356 static createErrorInfo(errorInfo: string, params: string[]): string { 357 params.forEach((param: string) => { 358 errorInfo = errorInfo.replace('$$', param); 359 }); 360 return errorInfo; 361 } 362 363 /** 364 * 获取apiName 365 */ 366 static getApiName(node: ts.Node): string { 367 let apiName: string = ''; 368 if ((ts.isVariableDeclaration(node) || ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node) || 369 ts.isInterfaceDeclaration(node) || ts.isEnumDeclaration(node) || ts.isModuleDeclaration(node) || 370 ts.isNamespaceExportDeclaration(node) || ts.isPropertySignature(node) || ts.isEnumMember(node) || 371 ts.isMethodSignature(node) || ts.isMethodDeclaration(node) || ts.isPropertyDeclaration(node) || 372 ts.isCallSignatureDeclaration(node) || ts.isTypeAliasDeclaration(node)) && node.name) { 373 apiName = node.name.getText(); 374 } else if (ts.isConstructorDeclaration(node)) { 375 apiName = 'constructor'; 376 } else if (ts.isVariableStatement(node)) { 377 apiName = node.declarationList.declarations[0].name.getText(); 378 } else if (ts.isVariableDeclarationList(node)) { 379 apiName = node.declarations[0].name.getText(); 380 } 381 return apiName; 382 } 383} 384 385const jsDocModifier: Map<string, JsDocModificationInterface> = new Map([ 386 ['constant', JSDocModificationManager.addTagWithoutValue], 387 ['deprecated', JSDocModificationManager.addTagFrommParentNode], 388 ['extends', JSDocModificationManager.addTagWithValue], 389 ['famodelonly', JSDocModificationManager.addTagFrommParentNode], 390 ['namespace', JSDocModificationManager.addTagWithValue], 391 ['param', JSDocModificationManager.addParamTag], 392 ['returns', JSDocModificationManager.addTagWithValue], 393 ['stagemodelonly', JSDocModificationManager.addTagFrommParentNode], 394 ['syscap', JSDocModificationManager.addTagFrommParentNode], 395 ['systemapi', JSDocModificationManager.addTagFrommParentNode], 396 ['test', JSDocModificationManager.addTagFrommParentNode], 397]); 398 399/** 400 * 标签顺序白名单 401 */ 402const JSDOC_ORDER_TAGS_ARRAY = [ 403 'namespace', 'extends', 'typedef', 'interface', 'permission', 'enum', 'constant', 'type', 'param', 'default', 404 'returns', 'readonly', 'throws', 'static', 'fires', 'syscap', 'systemapi', 'famodelonly', 'FAModelOnly', 405 'stagemodelonly', 'StageModelOnly', 'crossplatform', 'since', 'deprecated', 'useinstead', 'test', 'form', 'example' 406]; 407 408/** 409 * 继承标签白名单 410 */ 411const INHERIT_TAGS_ARRAY = ['deprecated', 'famodelonly', 'stagemodelonly', 'systemapi', 'test', 'permission']; 412