• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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