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'; 17 18import { Comment } from '../../typedef/parser/Comment'; 19import { LogUtil } from '../../utils/logUtil'; 20import { StringUtils } from '../../utils/StringUtils'; 21import { ApiType } from '../../typedef/parser/ApiInfoDefination'; 22 23export class CommentHelper { 24 static licenseKeyword: string = 'Copyright'; 25 static referenceRegexp: RegExp = /\/\/\/\s*<reference\s*path/g; 26 static referenceCommentRegexp: RegExp = /\/\s*<reference\s*path/g; 27 static mutiCommentDelimiter: string = '/**'; 28 static fileTag: RegExp = /\@file|\@kit/g; 29 30 /** 31 * 获取指定AST节点上的注释,若无注释返回空数组。 32 * 33 * @param { ts.Node } node 当前节点 34 * @param { ts.SourceFile } sourceFile sourceFile节点 35 * @returns { Comment.CommentInfo[] } 处理后的JsDoc数组 36 */ 37 static getNodeLeadingComments(node: ts.Node, sourceFile: ts.SourceFile): Comment.CommentInfo[] { 38 try { 39 const leadingCommentRange: ts.CommentRange[] | undefined = ts.getLeadingCommentRanges( 40 sourceFile.getFullText(), 41 node.getFullStart() 42 ); 43 if (leadingCommentRange && leadingCommentRange.length) { 44 const parsedCommentInfos: Array<Comment.CommentInfo> = []; 45 leadingCommentRange.forEach((range: ts.CommentRange) => { 46 const comment: string = sourceFile.getFullText().slice(range.pos, range.end); 47 const commentInfo: Comment.CommentInfo = CommentHelper.parseComment(comment, range.kind, true); 48 commentInfo.pos = range.pos; 49 commentInfo.end = range.end; 50 parsedCommentInfos.push(commentInfo); 51 }); 52 return parsedCommentInfos; 53 } 54 return []; 55 } catch (error) { 56 LogUtil.d('CommentHelper', `node(kind=${node.kind}) is created in memory.`); 57 return []; 58 } 59 } 60 61 /** 62 * 将多段注释文本解析成注释对象。 63 * 64 * @param { string } comment 多端JsDoc字符串 65 * @param { ts.CommentKind } commentKind 注释的类型 66 * @param { boolean } isLeading 是否是头注释 67 * @returns { Comment.CommentInfo } 返回处理后的JsDoc对象 68 */ 69 static parseComment(comment: string, commentKind: ts.CommentKind, isLeading: boolean): Comment.CommentInfo { 70 const { parse } = require('comment-parser'); 71 const commentInfo: Comment.CommentInfo = { 72 text: comment, 73 isMultiLine: commentKind === ts.SyntaxKind.MultiLineCommentTrivia, 74 isLeading: isLeading, 75 description: '', 76 commentTags: [], 77 parsedComment: undefined, 78 pos: -1, 79 end: -1, 80 ignore: false, 81 isApiComment: false, 82 isInstruct: false, 83 isFileJsDoc: false, 84 }; 85 let commentString: string = comment; 86 let parsedComments = parse(commentString); 87 // 无法被解析的注释,可能以 /* 开头或是单行注释 88 if (parsedComments.length === 0) { 89 // 注释是 /// <reference path="" /> 或 单行注释 90 if ( 91 StringUtils.hasSubstring(commentString, this.referenceRegexp) || 92 commentKind === ts.SyntaxKind.SingleLineCommentTrivia 93 ) { 94 commentInfo.isMultiLine = false; 95 // 注释内容需丢弃 "//" 96 const startIndex: number = 2; 97 commentInfo.text = commentString.substring(startIndex, commentString.length); 98 } 99 return commentInfo; 100 } 101 commentInfo.parsedComment = parsedComments[0]; 102 commentInfo.description = parsedComments[0].description; 103 for (let i = 0; i < parsedComments[0].tags.length; i++) { 104 const tag = parsedComments[0].tags[i]; 105 commentInfo.commentTags.push({ 106 tag: tag.tag, 107 name: tag.name, 108 type: tag.type, 109 optional: tag.optional, 110 description: tag.description, 111 source: tag.source[0].source, 112 lineNumber: tag.source[0].number, 113 tokenSource: tag.source, 114 defaultValue: tag.default ? tag.default : undefined, 115 }); 116 } 117 if (StringUtils.hasSubstring(commentString, this.fileTag)) { 118 commentInfo.isFileJsDoc = true; 119 } 120 commentInfo.isApiComment = true; 121 return commentInfo; 122 } 123} 124 125export class JsDocProcessorHelper { 126 static setSyscap(jsDocInfo: Comment.JsDocInfo, commentTag: Comment.CommentTag): void { 127 jsDocInfo.setSyscap(commentTag.name); 128 } 129 130 static setSince(jsDocInfo: Comment.JsDocInfo, commentTag: Comment.CommentTag): void { 131 jsDocInfo.setSince(commentTag.name); 132 } 133 134 static setIsForm(jsDocInfo: Comment.JsDocInfo): void { 135 jsDocInfo.setIsForm(true); 136 } 137 138 static setIsCrossPlatForm(jsDocInfo: Comment.JsDocInfo): void { 139 jsDocInfo.setIsCrossPlatForm(true); 140 } 141 142 static setIsSystemApi(jsDocInfo: Comment.JsDocInfo): void { 143 jsDocInfo.setIsSystemApi(true); 144 } 145 146 static setIsAtomicService(jsDocInfo: Comment.JsDocInfo): void { 147 jsDocInfo.setIsAtomicService(true); 148 } 149 150 static setDeprecatedVersion(jsDocInfo: Comment.JsDocInfo, commentTag: Comment.CommentTag): void { 151 jsDocInfo.setDeprecatedVersion(commentTag.description); 152 } 153 154 static setUseinstead(jsDocInfo: Comment.JsDocInfo, commentTag: Comment.CommentTag): void { 155 jsDocInfo.setUseinstead(commentTag.name); 156 } 157 158 static setPermission(jsDocInfo: Comment.JsDocInfo, commentTag: Comment.CommentTag): void { 159 const description: string = commentTag.description; 160 const name: string = commentTag.name; 161 const permissions: string = description ? `${name} ${description}` : `${name}`; 162 jsDocInfo.setPermission(permissions); 163 } 164 165 static addErrorCode(jsDocInfo: Comment.JsDocInfo, commentTag: Comment.CommentTag): void { 166 if (!commentTag || isNaN(Number(commentTag.name))) { 167 return; 168 } 169 jsDocInfo.addErrorCode(Number(commentTag.name)); 170 } 171 172 static setTypeInfo(jsDocInfo: Comment.JsDocInfo, commentTag: Comment.CommentTag): void { 173 jsDocInfo.setTypeInfo(commentTag.type); 174 } 175 176 static setIsConstant(jsDocInfo: Comment.JsDocInfo): void { 177 jsDocInfo.setIsConstant(true); 178 } 179 180 static setModelLimitation(jsDocInfo: Comment.JsDocInfo, commentTag: Comment.CommentTag): void { 181 jsDocInfo.setModelLimitation(commentTag.tag); 182 } 183 184 static setKitContent(jsDocInfo: Comment.JsDocInfo, commentTag: Comment.CommentTag): void { 185 jsDocInfo.setKit(commentTag.source.replace(/\* @kit|\r|\n/g, '').trim()); 186 } 187 188 static setIsFile(jsDocInfo: Comment.JsDocInfo, commentTag: Comment.CommentTag): void { 189 jsDocInfo.setFileTagContent(commentTag.source.replace(/\* @file|\r|\n/g, '').trim()); 190 } 191 192 /** 193 * 基于comment-parser解析的注释对象得到JsDocInfo对象 194 * 195 * @param { Comment.CommentInfo } jsDoc 一段JsDoc的信息解析的注释对象 196 * @returns 返回JsDoc得到的JsDocInfo对象 197 */ 198 static processJsDoc(jsDoc: Comment.CommentInfo, parentKitInfo: string, parentFileTag: string): Comment.JsDocInfo { 199 const jsDocInfo: Comment.JsDocInfo = new Comment.JsDocInfo(); 200 jsDocInfo.setDescription(jsDoc.description); 201 jsDocInfo.setKit(parentKitInfo); 202 jsDocInfo.setFileTagContent(parentFileTag); 203 for (let i = 0; i < jsDoc.commentTags.length; i++) { 204 const commentTag: Comment.CommentTag = jsDoc.commentTags[i]; 205 jsDocInfo.addTag(commentTag); 206 const jsDocProcessor = jsDocProcessorMap.get(commentTag.tag.toLowerCase()); 207 if (!jsDocProcessor) { 208 continue; 209 } 210 jsDocProcessor(jsDocInfo, commentTag); 211 } 212 return jsDocInfo; 213 } 214 215 /** 216 * 基于当前node节点,返回节点上多段JsDoc解析后的对象数组 217 * 218 * @param { ts.Node } node node节点 219 * @param { ts.SourceFile } sourceFile node节点的sourceFile 220 * @returns 返回解析后的多段JsDoc的信息数组 221 */ 222 static processJsDocInfos(node: ts.Node, apiType: string, parentKitInfo: string, parentFileTag: string): Comment.JsDocInfo[] { 223 const sourceFile = node.getSourceFile(); 224 const allCommentInfos: Comment.CommentInfo[] = CommentHelper.getNodeLeadingComments(node, sourceFile); 225 const commentInfos: Comment.CommentInfo[] = allCommentInfos.filter((commentInfo: Comment.CommentInfo) => { 226 return ( 227 (commentInfo.isApiComment && !commentInfo.isFileJsDoc && apiType !== ApiType.SOURCE_FILE) || 228 (commentInfo.isApiComment && commentInfo.isFileJsDoc && apiType === ApiType.SOURCE_FILE) 229 ); 230 }); 231 232 const jsDocInfos: Comment.JsDocInfo[] = []; 233 if (commentInfos.length === 0 && parentKitInfo !== '') { 234 const jsDocInfo: Comment.JsDocInfo = new Comment.JsDocInfo(); 235 jsDocInfo.setKit(parentKitInfo); 236 jsDocInfo.setFileTagContent(parentFileTag); 237 jsDocInfos.push(jsDocInfo); 238 } 239 for (let i = 0; i < commentInfos.length; i++) { 240 const commentInfo: Comment.CommentInfo = commentInfos[i]; 241 const jsDocInfo: Comment.JsDocInfo = JsDocProcessorHelper.processJsDoc(commentInfo, parentKitInfo, parentFileTag); 242 jsDocInfos.push(jsDocInfo); 243 } 244 return jsDocInfos; 245 } 246} 247 248const jsDocProcessorMap: Map<string, Comment.JsDocProcessorInterface> = new Map([ 249 [Comment.JsDocTag.SYSCAP, JsDocProcessorHelper.setSyscap], 250 [Comment.JsDocTag.SINCE, JsDocProcessorHelper.setSince], 251 [Comment.JsDocTag.FORM, JsDocProcessorHelper.setIsForm], 252 [Comment.JsDocTag.CROSS_PLAT_FORM, JsDocProcessorHelper.setIsCrossPlatForm], 253 [Comment.JsDocTag.SYSTEM_API, JsDocProcessorHelper.setIsSystemApi], 254 [Comment.JsDocTag.STAGE_MODEL_ONLY, JsDocProcessorHelper.setModelLimitation], 255 [Comment.JsDocTag.FA_MODEL_ONLY, JsDocProcessorHelper.setModelLimitation], 256 [Comment.JsDocTag.DEPRECATED, JsDocProcessorHelper.setDeprecatedVersion], 257 [Comment.JsDocTag.USEINSTEAD, JsDocProcessorHelper.setUseinstead], 258 [Comment.JsDocTag.TYPE, JsDocProcessorHelper.setTypeInfo], 259 [Comment.JsDocTag.PERMISSION, JsDocProcessorHelper.setPermission], 260 [Comment.JsDocTag.THROWS, JsDocProcessorHelper.addErrorCode], 261 [Comment.JsDocTag.CONSTANT, JsDocProcessorHelper.setIsConstant], 262 [Comment.JsDocTag.ATOMIC_SERVICE, JsDocProcessorHelper.setIsAtomicService], 263 [Comment.JsDocTag.KIT, JsDocProcessorHelper.setKitContent], 264 [Comment.JsDocTag.FILE, JsDocProcessorHelper.setIsFile], 265]); 266