• 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 path from 'path';
17import excelJs from 'exceljs';
18import fs from 'fs';
19import { Command } from 'commander';
20import { ConstantValue, Instruct, StringResourceId } from '../utils/constant';
21import { FileUtils } from '../utils/fileUtils';
22import { LogUtil } from '../utils/logUtil';
23import { StringResource, StringUtils } from '../utils/stringUtils';
24import type {
25  Context, LogReporter, CheckLogResult, ModifyLogResult, LogWriter, JSDocModifyType,
26  JSDocCheckErrorType, CommentData
27} from './typedef';
28import { comment, Options, sourceParser, rawInfo } from './typedef';
29import ts from 'typescript';
30import type { CommentRange, TransformationContext, TransformationResult } from 'typescript';
31
32export class ContextImpl implements Context {
33  options: Options;
34  inputFile: string;
35  outputFile: string;
36  rawSourceCodeInfo?: rawInfo.RawSourceCodeInfo;
37  logReporter?: LogReporter;
38
39  constructor(inputFile: string, outputFile: string, options?: Options) {
40    this.inputFile = inputFile;
41    this.outputFile = outputFile;
42    this.options = options ? options : new Options();
43  }
44
45  setLogReporter(logReporter: LogReporter): void {
46    this.logReporter = logReporter;
47  }
48
49  getOptions(): Options {
50    return this.options;
51  }
52
53  setRawSourceInfo(rawInfo: rawInfo.RawSourceCodeInfo): void {
54    this.rawSourceCodeInfo = rawInfo;
55  }
56
57  getRawSourceCodeInfo(): rawInfo.RawSourceCodeInfo {
58    return this.rawSourceCodeInfo || new RawSourceCodeInfoImpl('');
59  }
60
61  getLogReporter(): LogReporter {
62    if (this.logReporter === undefined) {
63      this.logReporter = new LogReporterImpl();
64      let writer: LogWriter;
65      if (this.getOptions().isTest) {
66        writer = new logWriter.JsonWriter();
67      } else {
68        writer = new logWriter.ExcelWriter(this.logReporter);
69      }
70      this.logReporter.setWriter(writer);
71    }
72    return this.logReporter;
73  }
74
75  getOutputFile(): string {
76    return this.outputFile;
77  }
78
79  getInputFile(): string {
80    return this.inputFile;
81  }
82
83  getSourceParser(code: string): sourceParser.SourceCodeParser {
84    return new SourceCodeParserImpl(code, this.options);
85  }
86}
87
88
89export class OutputFileHelper {
90
91  static getOutputFilePath(inputParam: InputParameter, sourceFile: string): string {
92    return inputParam.isHandleMultiFiles() ? OutputFileHelper.getMultiOutputFilePath(inputParam, sourceFile) :
93      OutputFileHelper.getSingleOutputFilePath(inputParam);
94  }
95
96  static getMultiOutputFilePath(inputParam: InputParameter, sourceFile: string): string {
97    // 计算原文件与输入目录的相对路径
98    const relativePath = path.relative(inputParam.inputFilePath!, sourceFile);
99    // 如果未设置 outputPath, 同级目录创建新目录
100    return path.resolve(this.getMultiOutputDir(inputParam), relativePath);
101  }
102
103  static getMultiOutputDir(inputParam: InputParameter): string {
104    if (inputParam.outputFilePath) {
105      return inputParam.outputFilePath;
106    }
107    const fileName = path.basename(inputParam.inputFilePath!);
108    const dirname = path.dirname(inputParam.inputFilePath!);
109    return path.resolve(dirname, `${fileName}_new`);
110  }
111
112  static getSingleOutputFilePath(inputParam: InputParameter): string {
113    if (inputParam.outputFilePath) {
114      return inputParam.outputFilePath;
115    }
116    // 同级目录创建新文件
117    const fileName = path.basename(inputParam.inputFilePath!, ConstantValue.DTS_EXTENSION);
118    return path.join(inputParam.inputFilePath!, '..', `${fileName}_new${ConstantValue.DTS_EXTENSION}`);
119  }
120
121  // 获取报告输出路径
122  static getLogReportFilePath(inputParam: InputParameter): string {
123    const fileName = path.basename(inputParam.inputFilePath, '.d.ts');
124    if (inputParam.outputFilePath) {
125      const dirName = path.dirname(inputParam.outputFilePath);
126      return path.join(dirName, `${fileName}`);
127    } else {
128      const dirName = path.dirname(inputParam.inputFilePath);
129      return path.join(dirName, `${fileName}`);
130    }
131  }
132}
133
134function getCommentNode(node: ts.Node, parentNode: comment.CommentNode | undefined, sourceFile: ts.SourceFile): comment.CommentNode {
135  const leadingComments: comment.CommentInfo[] = CommentHelper.getNodeLeadingComments(node, sourceFile);
136  const currentCommentNode: comment.CommentNode = {
137    astNode: node,
138    parentNode: parentNode,
139    commentInfos: []
140  };
141  currentCommentNode.commentInfos?.push(...leadingComments);
142  return currentCommentNode;
143}
144
145export class SourceCodeParserImpl extends sourceParser.SourceCodeParser {
146  options: Options;
147
148  /**
149   * 可能存在注释的节点类型。
150   */
151  commentNodeWhiteList: ts.SyntaxKind[] = [
152    ts.SyntaxKind.VariableDeclaration, ts.SyntaxKind.VariableDeclarationList, ts.SyntaxKind.FunctionDeclaration,
153    ts.SyntaxKind.ClassDeclaration, ts.SyntaxKind.InterfaceDeclaration, ts.SyntaxKind.TypeAliasDeclaration,
154    ts.SyntaxKind.EnumDeclaration, ts.SyntaxKind.ModuleDeclaration, ts.SyntaxKind.NamespaceExportDeclaration,
155    ts.SyntaxKind.PropertySignature, ts.SyntaxKind.CallSignature, ts.SyntaxKind.MethodSignature, ts.SyntaxKind.MethodDeclaration,
156    ts.SyntaxKind.EnumMember, ts.SyntaxKind.VariableStatement, ts.SyntaxKind.PropertyDeclaration, ts.SyntaxKind.Constructor,
157    ts.SyntaxKind.TypeLiteral, ts.SyntaxKind.ImportDeclaration, ts.SyntaxKind.LabeledStatement
158  ];
159  visitNodeChildrenWhilteList: ts.SyntaxKind[] = [
160    ts.SyntaxKind.ModuleDeclaration, ts.SyntaxKind.ModuleBlock, ts.SyntaxKind.NamespaceExportDeclaration, ts.SyntaxKind.ClassDeclaration,
161    ts.SyntaxKind.InterfaceDeclaration, ts.SyntaxKind.EnumDeclaration, ts.SyntaxKind.MethodDeclaration, ts.SyntaxKind.Parameter,
162    ts.SyntaxKind.TypeLiteral, ts.SyntaxKind.PropertySignature, ts.SyntaxKind.TypeAliasDeclaration
163  ];
164
165  constructor(code: string, options: Options) {
166    super(code);
167    this.options = options;
168  }
169
170  private shouldNotify(node: ts.Node): boolean {
171    // if kind = ImportDeclaration, comments may be license declarations
172    return this.commentNodeWhiteList.includes(node.kind);
173  }
174
175  private shouldForEachChildren(node: ts.Node): boolean {
176    return true;
177  }
178
179  createSourceFile(content: string, name?: string | undefined): ts.SourceFile | undefined {
180    const sourceFile = ts.createSourceFile(name ? name : 'memory', content, this.options.scriptTarget, true);
181    // 没有解析成AST树,非代码文件
182    if (sourceFile.statements.length === 0) {
183      return undefined;
184    }
185    return sourceFile;
186  }
187
188  transform(callback: sourceParser.ITransformCallback): ts.SourceFile | undefined {
189    let transformSourceFile = this.createSourceFile(this.content, 'transform');
190    if (!transformSourceFile) {
191      return undefined;
192    }
193    function transformCallback(context: TransformationContext) {
194      return (rootNode: ts.Node) => {
195        function visitor(node: ts.Node): ts.Node {
196          const commentNode = getCommentNode(node, undefined, transformSourceFile!);
197          const newNode: ts.Node | undefined = callback.onTransformNode(commentNode);
198          return ts.visitEachChild(newNode ? newNode : node, visitor, context);
199        }
200        return ts.visitEachChild(rootNode, visitor, context);
201      };
202    };
203    const rootNode = callback.onTransformNode({ astNode: transformSourceFile });
204    if (rootNode && ts.isSourceFile(rootNode) && rootNode !== transformSourceFile) {
205      transformSourceFile = rootNode;
206    }
207    const transformResult: TransformationResult<ts.Node> = ts.transform(transformSourceFile, [transformCallback]);
208    if (transformResult.transformed.length > 0) {
209      return transformResult.transformed[0] as ts.SourceFile;
210    }
211    return transformSourceFile;
212  }
213
214  visitEachNodeComment(callback: sourceParser.INodeVisitorCallback, onlyVisitHasComment?: boolean): ts.SourceFile | undefined {
215    const forEachSourceFile = this.createSourceFile(this.content, 'forEach');
216    if (!forEachSourceFile) {
217      return undefined;
218    }
219    const thiz = this;
220    const handledComments: Set<string> = new Set();
221    function nodeVisitor(node: ts.Node, parentNode: comment.CommentNode | undefined, sourceFile: ts.SourceFile): void {
222      const currentCommentNode = getCommentNode(node, parentNode, sourceFile);
223      const NOTE_LENGTH = 0;
224      thiz.skipHandledComments(handledComments, currentCommentNode);
225      const hasComment: boolean = currentCommentNode.commentInfos ?
226        currentCommentNode.commentInfos.length > NOTE_LENGTH : false;
227      const { line, character } = node.getSourceFile().getLineAndCharacterOfPosition(node.getStart());
228      if (thiz.shouldNotifyCallback(node, hasComment, onlyVisitHasComment)) {
229        LogUtil.d('SourceCodeParserImpl', `kind: ${node.kind}, line: ${line + 1}, ${JSON.stringify(currentCommentNode.commentInfos)}`);
230        callback.onVisitNode(currentCommentNode);
231      } else {
232        LogUtil.d('SourceCodeParserImpl',
233          `skip, [ ${node.getText()} ] kind: ${node.kind}, line: ${line + 1}, character: ${character}, comment size: ${currentCommentNode.commentInfos?.length}`);
234      }
235      if (thiz.shouldForEachChildren(node)) {
236        node.forEachChild((child) => {
237          nodeVisitor(child, currentCommentNode, sourceFile);
238        });
239      }
240    }
241    forEachSourceFile.forEachChild((child) => {
242      nodeVisitor(child, undefined, forEachSourceFile);
243    });
244    return forEachSourceFile;
245  }
246
247  private skipHandledComments(handledComments: Set<string>, commentNode: comment.CommentNode): void {
248    const unHandledComments: Array<comment.CommentInfo> = [];
249    commentNode.commentInfos?.forEach((info) => {
250      const key = `${info.pos}:${info.end}`;
251      if (info.isInstruct || !handledComments.has(key)) {
252        unHandledComments.push(info);
253        handledComments.add(key);
254      }
255    });
256    if (unHandledComments.length !== commentNode.commentInfos?.length) {
257      commentNode.commentInfos = unHandledComments;
258    }
259  }
260
261  private shouldNotifyCallback(node: ts.Node, hasComment: boolean, onlyVisitHasComment?: boolean): boolean {
262    if (!this.shouldNotify(node)) {
263      return false;
264    }
265    return !(onlyVisitHasComment && onlyVisitHasComment === true && !hasComment);
266  }
267
268  printSourceFile(sourceFile: ts.SourceFile): string {
269    const printer = ts.createPrinter(this.options.printerOptions);
270    let codeString = printer.printFile(sourceFile);
271    // replace empty line instruct
272    codeString = codeString.replaceAll(/\s*\/\/Instruct_new_line/g, '\n');
273    return codeString;
274  }
275}
276
277
278export class CommentHelper {
279  static LICENSE_KEYWORD = 'Copyright';
280  static REFERENCE_REGEXP = /\/\/\/\s*<reference\s*path/g;
281  static REFERENCE_COMMENT_REGEXP = /\/\s*<reference\s*path/g;
282  static MULTI_COMMENT_DELIMITER = '/**';
283
284  /**
285   * 给节点追加注释(注意:无法在原始注释中追加,只能将原始注释完全复制一份并修改再做完整替换)
286   *
287   * @param node ast 节点对象
288   * @param commentInfos 注释列表
289   */
290  static addComment(node: ts.Node, commentInfos: Array<comment.CommentInfo>): void {
291    if (commentInfos.length === 0) {
292      return;
293    }
294    CommentHelper.ignoreOriginalComment(node);
295    commentInfos.forEach((info) => {
296      // might be a license
297      if (info.ignore) {
298        return;
299      }
300      const assembledComment = CommentHelper.assembleComment(info);
301      const kind = info.isMultiLine ? ts.SyntaxKind.MultiLineCommentTrivia : ts.SyntaxKind.SingleLineCommentTrivia;
302      if (info.isLeading) {
303        ts.addSyntheticLeadingComment(node, kind, assembledComment, true);
304      } else {
305        ts.addSyntheticTrailingComment(node, kind, assembledComment, true);
306      }
307    });
308  }
309
310  /**
311   * 封装符合格式的注释
312   *
313   * @param commentInfo
314   * @returns
315   */
316  static assembleComment(commentInfo: comment.CommentInfo): string {
317    const writer = new CommentWriter(commentInfo.isMultiLine);
318    return writer.publish(commentInfo);
319  }
320
321
322  /**
323   * 设置节点注释,覆盖已有的注释。(注意:原始文件的注释不会被替换,只在输出时忽略原始注释,打印新的注释)
324   *
325   * @param node
326   * @param commentInfos
327   * @param commentKind
328   * @returns
329   */
330  static setComment(node: ts.Node, commentInfos: Array<comment.CommentInfo>, commentKind?: number): void {
331    if (commentInfos.length === 0) {
332      return;
333    }
334    CommentHelper.ignoreOriginalComment(node);
335    const syntheticLeadingComments: Array<ts.SynthesizedComment> = new Array();
336    commentInfos.forEach((info) => {
337      // might be a license
338      if (info.ignore) {
339        return;
340      }
341      syntheticLeadingComments.push({
342        text: CommentHelper.assembleComment(info),
343        pos: -1,
344        end: -1,
345        hasLeadingNewline: true,
346        hasTrailingNewLine: true,
347        kind: info.isMultiLine ? ts.SyntaxKind.MultiLineCommentTrivia : ts.SyntaxKind.SingleLineCommentTrivia
348      });
349    });
350
351    if (!commentKind || commentKind === comment.CommentLocationKind.LEADING) {
352      ts.setSyntheticLeadingComments(node, syntheticLeadingComments);
353    } else {
354      ts.setSyntheticTrailingComments(node, syntheticLeadingComments);
355    }
356  }
357
358  /**
359   * 打印源码时忽略节点的原始注释
360   *
361   * @param node
362   */
363  private static ignoreOriginalComment(node: ts.Node): void {
364    // ignore the original comment
365    ts.setEmitFlags(node, ts.EmitFlags.NoLeadingComments);
366  }
367
368  /**
369   * 将多段注释文本解析成注释对象。
370   *
371   * @param comment
372   * @returns
373   */
374  static parseComment(comment: string, commentKind: ts.CommentKind, isLeading: boolean): comment.CommentInfo {
375    const { parse } = require('comment-parser');
376    const commentInfo: comment.CommentInfo = {
377      text: comment,
378      isMultiLine: commentKind === ts.SyntaxKind.MultiLineCommentTrivia,
379      isLeading: isLeading,
380      description: '',
381      commentTags: [],
382      parsedComment: undefined,
383      pos: -1,
384      end: -1,
385      ignore: false,
386      isApiComment: false,
387      isInstruct: false
388    };
389    let commentString = comment;
390    let parsedComments = parse(commentString);
391    const START_POSITION_INDEX = 2;
392    // 无法被解析的注释,可能以 /* 开头或是单行注释
393    if (parsedComments.length === 0) {
394      // 注释是 /// <reference path="" /> 或 单行注释
395      if (StringUtils.hasSubstring(commentString, this.REFERENCE_REGEXP) ||
396        commentKind === ts.SyntaxKind.SingleLineCommentTrivia) {
397        commentInfo.isMultiLine = false;
398        // 注释内容需丢弃 "//"
399        commentInfo.text = commentString.substring(START_POSITION_INDEX, commentString.length);
400      }
401      return commentInfo;
402    }
403    commentInfo.parsedComment = parsedComments[0];
404    commentInfo.description = parsedComments[0].description;
405    parsedComments[0].tags.forEach((tagObject: CommentData) => {
406      commentInfo.commentTags.push({
407        tag: tagObject.tag,
408        name: tagObject.name,
409        type: tagObject.type,
410        optional: tagObject.optional,
411        description: tagObject.description,
412        source: tagObject.source[0].source,
413        lineNumber: tagObject.source[0].number,
414        tokenSource: tagObject.source,
415        defaultValue: tagObject.default ? tagObject.default : undefined
416      });
417    });
418    commentInfo.isApiComment = true;
419    return commentInfo;
420  }
421
422  /**
423   * 获取指定AST节点上的注释,若无注释返回空数组。
424   *
425   * @param node
426   * @param sourceFile
427   * @returns
428   */
429  static getNodeLeadingComments(node: ts.Node, sourceFile: ts.SourceFile): comment.CommentInfo[] {
430    try {
431      const leadingCommentRange: CommentRange[] | undefined = ts.getLeadingCommentRanges(sourceFile.getFullText(), node.getFullStart());
432      if (leadingCommentRange?.length) {
433        const parsedCommentInfos: Array<comment.CommentInfo> = [];
434        leadingCommentRange.forEach((range) => {
435          const comment = sourceFile.getFullText().slice(range.pos, range.end);
436          const commentInfo = CommentHelper.parseComment(comment, range.kind, true);
437          commentInfo.pos = range.pos;
438          commentInfo.end = range.end;
439          parsedCommentInfos.push(commentInfo);
440          this.fixReferenceComment(commentInfo, parsedCommentInfos);
441        });
442        this.fixLicenseComment(node, parsedCommentInfos);
443        return parsedCommentInfos;
444      }
445      return [];
446    } catch (error) {
447      LogUtil.d('CommentHelper', `node(kind=${node.kind}) is created in memory.`);
448      return [];
449    }
450  }
451
452  private static fixReferenceComment(commentInfo: comment.CommentInfo, parsedCommentInfos: Array<comment.CommentInfo>): void {
453    if (commentInfo.isMultiLine || commentInfo.isApiComment ||
454      !StringUtils.hasSubstring(commentInfo.text, this.REFERENCE_COMMENT_REGEXP)) {
455      return;
456    }
457    parsedCommentInfos.push(this.getEmptyLineComment());
458  }
459
460  /**
461   * 如果license注释与头注释间没有空白行会导致license丢失.
462   *
463   * @param commentInfos
464   */
465  private static fixLicenseComment(node: ts.Node, commentInfos: comment.CommentInfo[]): void {
466    if (commentInfos.length === 0) {
467      return;
468    }
469    // 如果只有一个注释并且是license, license 与 node 间需要一个空行
470    if (commentInfos.length === 1) {
471      if (StringUtils.hasSubstring(commentInfos[0].text, this.LICENSE_KEYWORD)) {
472        const copyRightPosition = node.getSourceFile().getLineAndCharacterOfPosition(commentInfos[0].end);
473        const nodePosition = node.getSourceFile().getLineAndCharacterOfPosition(node.getStart());
474        if ((nodePosition.line - copyRightPosition.line) === 1) {
475          commentInfos.push(this.getEmptyLineComment());
476        } else {
477          commentInfos[0].ignore = true;
478        }
479      }
480      return;
481    }
482    const firstCommentInfo: comment.CommentInfo = commentInfos[0];
483    if (!StringUtils.hasSubstring(firstCommentInfo.text, this.LICENSE_KEYWORD)) {
484      return;
485    }
486    firstCommentInfo.isApiComment = false;
487    // license 注释在第一位
488    const firstPosition = node.getSourceFile().getLineAndCharacterOfPosition(firstCommentInfo.end);
489    const secondPosition = node.getSourceFile().getLineAndCharacterOfPosition(commentInfos[1].pos);
490    // 无空格
491    if ((secondPosition.line - firstPosition.line) === 1) {
492      firstCommentInfo.ignore = false;
493    } else {
494      firstCommentInfo.ignore = true;
495    }
496    // license注释与头注释之间没有import语句会导致两者间的空行丢失, 插入空行指令
497    commentInfos.splice(1, 0, this.getEmptyLineComment());
498  }
499
500  static getEmptyLineComment(): comment.CommentInfo {
501    return {
502      text: Instruct.EMPTY_LINE,
503      isMultiLine: false,
504      isLeading: true,
505      description: '',
506      commentTags: [],
507      parsedComment: undefined,
508      pos: -1,
509      end: -1,
510      ignore: false,
511      isApiComment: false,
512      isInstruct: true
513    };
514  }
515
516  static getNodeTrailingComments(node: ts.Node, sourceFile: ts.SourceFile): comment.CommentInfo[] {
517    try {
518      const trailingCommentRange: CommentRange[] | undefined = ts.getTrailingCommentRanges(sourceFile.getFullText(), node.getEnd() + 1);
519      if (trailingCommentRange?.length) {
520        const commentNodes = trailingCommentRange.map((range) => {
521          const comment = sourceFile.getFullText().slice(range.pos, range.end);
522          return CommentHelper.parseComment(comment.replace(/^\s*\/\//g, ''), range.kind, false);
523        });
524        return commentNodes;
525      }
526      return [];
527    } catch (error) {
528      LogUtil.w('CommentHelper', 'getNodeTrailingComments:' + error);
529      return [];
530    }
531  }
532}
533
534/**
535 * 注释封装类
536 */
537class CommentWriter {
538  isMultiLine: boolean;
539  commentBlockDelimiter = '/**';
540
541  constructor(isMultiLine: boolean) {
542    this.isMultiLine = isMultiLine;
543  }
544
545  /**
546   * 构建完整的注释文本段
547   */
548  publish(commentInfo: comment.CommentInfo): string {
549    const parsedComment: comment.ParsedComment | undefined = commentInfo.parsedComment;
550    // 如果没有解析过的注释对象(可能是license),使用原始注释内容
551    let plainComment = parsedComment ? this.restoreParsedComment(parsedComment, commentInfo.commentTags) : commentInfo.text;
552    const START_POSITION_INDEX = 2;
553    if (commentInfo.isMultiLine) {
554      // 删除起始 /* 和末尾 */ 符号
555      plainComment = plainComment.substring(START_POSITION_INDEX, plainComment.length - START_POSITION_INDEX);
556    }
557    return plainComment;
558  }
559
560  private restoreParsedComment(parsedComment: comment.ParsedComment, commentTags: Array<comment.CommentTag>): string {
561    const newSourceArray: Array<comment.CommentSource> = [];
562    const { stringify } = require('comment-parser');
563    if (parsedComment.source.length === 1) {
564      return stringify(parsedComment);
565    }
566    for (let source of parsedComment.source) {
567      // 保留描述信息,去除标签注释
568      if (source.tokens.tag !== '') {
569        break;
570      }
571      // 描述信息内有空行
572      if (source.tokens.description === '') {
573        continue;
574      }
575      // 描述信息写在/** 之后
576      if (source.tokens.delimiter === this.commentBlockDelimiter) {
577        source.tokens.delimiter = '*';
578        source.tokens.postDelimiter = ' ';
579      }
580      // 除了起始符号为空,其他场景则重置为一个空格
581      // ts api 提供的写注释接口缩进是参考当前节点的,因此一个空格可以保证格式正确
582      if (source.tokens.start !== '') {
583        source.tokens.start = ' ';
584      }
585      newSourceArray.push(source);
586    }
587    // 添加注释首行
588    newSourceArray.splice(0, 0, { number: 0, source: '', tokens: this.getCommentStartToken() });
589    const lastSource = newSourceArray[newSourceArray.length - 1];
590    if (commentTags.length !== 0) {
591      if (!this.isEndLine(lastSource.tokens)) {
592        this.addNewEmptyLineIfNeeded(newSourceArray, lastSource);
593        this.addTags(newSourceArray, commentTags);
594      }
595    } else {
596      newSourceArray.push({ number: 0, source: '', tokens: this.getCommentEndToken() });
597    }
598    parsedComment.source = newSourceArray;
599    return stringify(parsedComment);
600  }
601
602  private addNewEmptyLineIfNeeded(newSourceArray: Array<comment.CommentSource>, lastSource: comment.CommentSource): void {
603    if (this.shouldInsertNewEmptyLine(lastSource.tokens)) {
604      // 注释还原成字符串时, number, source 属性不会被用到
605      newSourceArray.push({ number: 0, source: '', tokens: this.getNewLineToken() });
606    }
607  }
608
609  private addTags(newSourceArray: Array<comment.CommentSource>, commentTags: Array<comment.CommentTag>): void {
610    commentTags.forEach((commentTag) => {
611      // tag的描述为多行,复用原始的描述信息
612      const tokenSourceLen = commentTag.tokenSource.length;
613      if (tokenSourceLen > 1) {
614        // 数组第一个为包含tag的注释
615        const tagSameLineDescription = commentTag.tokenSource[0].tokens.description;
616        newSourceArray.push({ number: 0, source: '', tokens: this.getCommentToken(commentTag, tagSameLineDescription) });
617        // 将tag剩余的描述信息放入
618        for (let index = 1; index < tokenSourceLen; index++) {
619          const tagDescriptionSource = commentTag.tokenSource[index];
620          // 末尾的tag会包含注释结束符, tag 间可能有空行
621          if (tagDescriptionSource.tokens.end !== '*/' && tagDescriptionSource.tokens.description !== '') {
622            tagDescriptionSource.tokens.start = ' ';
623            newSourceArray.push(tagDescriptionSource);
624          }
625        }
626      } else {
627        // tag注释只有一行
628        newSourceArray.push({ number: 0, source: '', tokens: this.getCommentToken(commentTag, commentTag.description) });
629      }
630    });
631    newSourceArray.push({ number: 0, source: '', tokens: this.getEndLineToken() });
632  }
633
634  private getCommentToken(commentTag: comment.CommentTag, description: string): comment.CommentToken {
635    const tokens: comment.CommentToken = {
636      // 起始空格
637      start: ' ',
638      // * 定界符
639      delimiter: '*',
640      // 定界符之后空格
641      postDelimiter: ' ',
642      // 标签
643      tag: `@${commentTag.tag}`,
644      // 标签后空格
645      postTag: ' ',
646      // 标签名称
647      name: commentTag.optional ?
648        (commentTag.defaultValue ? `[${commentTag.name} = ${commentTag.defaultValue}]` : `[${commentTag.name}]`) : commentTag.name,
649      // 标签名称后空格
650      postName: commentTag.name === '' ? '' : ' ',
651      // 类型
652      type: commentTag.type === '' ? '' : `{ ${commentTag.type} }`,
653      // 类型之后空格
654      postType: commentTag.type === '' ? '' : ' ',
655      // 描述
656      description: description,
657      end: '',
658      lineEnd: ''
659    };
660    return tokens;
661  }
662
663  private getCommentEndToken(): comment.CommentToken {
664    return {
665      start: ' ',
666      delimiter: '',
667      postDelimiter: '',
668      tag: '',
669      postTag: '',
670      name: '',
671      postName: '',
672      type: '',
673      postType: '',
674      description: '',
675      end: '*、',
676      lineEnd: ''
677    };
678  }
679
680  private getCommentStartToken(): comment.CommentToken {
681    return {
682      start: '',
683      delimiter: this.commentBlockDelimiter,
684      postDelimiter: '',
685      tag: '',
686      postTag: '',
687      name: '',
688      postName: '',
689      type: '',
690      postType: '',
691      description: '',
692      end: '',
693      lineEnd: ''
694    };
695  }
696
697  private getNewLineToken(): comment.CommentToken {
698    return {
699      start: ' ',
700      delimiter: '*',
701      postDelimiter: '',
702      tag: '',
703      postTag: '',
704      name: '',
705      postName: '',
706      type: '',
707      postType: '',
708      description: '',
709      end: '',
710      lineEnd: ''
711    };
712  }
713
714  private getEndLineToken(): comment.CommentToken {
715    return {
716      start: ' ',
717      delimiter: '',
718      postDelimiter: '',
719      tag: '',
720      postTag: '',
721      name: '',
722      postName: '',
723      type: '',
724      postType: '',
725      description: '',
726      end: '*/',
727      lineEnd: ''
728    };
729  }
730
731  private shouldInsertNewEmptyLine(token: comment.CommentToken): boolean {
732    const lastDescriptionNotEmpty = token.tag === '' && token.delimiter === '*' && token.description !== '';
733    const commentStartLine = token.tag === '' && token.delimiter === this.commentBlockDelimiter;
734    return lastDescriptionNotEmpty && !commentStartLine;
735  }
736
737  private isEndLine(token: comment.CommentToken): boolean {
738    return token.tag === '' && token.end === '*/';
739  }
740}
741
742export class AstNodeHelper {
743  static noNeedSignatureNodeTypes: Array<number> = [
744    ts.SyntaxKind.ModuleBlock, ts.SyntaxKind.Block, ts.SyntaxKind.CaseBlock
745  ];
746  static skipSignatureNode: Array<number> = [
747    ts.SyntaxKind.PropertySignature, ts.SyntaxKind.MethodSignature, ts.SyntaxKind.EnumMember, ts.SyntaxKind.Constructor,
748    ts.SyntaxKind.PropertyDeclaration, ts.SyntaxKind.MethodDeclaration, ts.SyntaxKind.TypeLiteral
749  ];
750
751  /**
752   * 获取节点的签名信息。签名信息会追加父节点签名信息,确保全局唯一。
753   * 注释改动导致行数变化, AST节点的顺序不会影响签名。
754   *
755   * @param node Ast节点
756   * @returns string 节点签名信息
757   */
758  static getNodeSignature(node: ts.Node): string {
759    if (!node || AstNodeHelper.noNeedSignatureNodeTypes.includes(node.kind)) {
760      return '';
761    }
762    let nodeSignature = `${AstNodeHelper.getType(node)}#`;
763    node.forEachChild((child) => {
764      if (!AstNodeHelper.skipSignatureNode.includes(child.kind) && !AstNodeHelper.noNeedSignatureNodeTypes.includes(child.kind)) {
765        nodeSignature += `${AstNodeHelper.getChildPlainText(child)}`;
766      }
767    });
768    const qualifiedSignature = this.getParentNodeSignature(node.parent) + nodeSignature;
769    LogUtil.d('AstNodeHelper', `qualifiedSignature = ${qualifiedSignature}`);
770    return qualifiedSignature;
771  }
772
773  private static getChildPlainText(node: ts.Node): string {
774    if (node.getChildCount() === 0) {
775      return node.getText();
776    }
777    let content = '';
778    node.forEachChild((child) => {
779      content += `${this.getChildPlainText(child)}`;
780    });
781    return content;
782  }
783
784  private static getType(node: ts.Node): string {
785    return `${ts.SyntaxKind[node.kind]}`;
786  }
787
788  private static getParentNodeSignature(node: ts.Node): string {
789    if (!node || node.kind === ts.SyntaxKind.SourceFile) {
790      return '';
791    }
792    const parentNodeSignature = this.getNodeSignature(node);
793    if (parentNodeSignature === '') {
794      return this.getNodeSignature(node.parent);
795    }
796    return parentNodeSignature;
797  }
798}
799
800/**
801 *
802 * 命令行入参对象
803 *
804 */
805export class InputParameter {
806  inputFilePath: string = '';
807  outputFilePath: string | undefined;
808  logLevel: string = '';
809  splitUnionTypeApi: boolean = false;
810  branch: string = 'master';
811  test: boolean = false;
812  options: Options = new Options();
813
814  parse(): void {
815    const program = new Command();
816    program
817      .name('jsdoc-tool')
818      .description('CLI to format d.ts')
819      .version('0.1.0')
820      .allowUnknownOption(true)
821      .requiredOption('-i, --input <path>', `${StringResource.getString(StringResourceId.COMMAND_INPUT_DESCRIPTION)}`)
822      .option('-o, --output <path>', `${StringResource.getString(StringResourceId.COMMAND_OUT_DESCRIPTION)}`)
823      .option('-l, --logLevel <INFO,WARN,DEBUG,ERR>', `${StringResource.getString(StringResourceId.COMMAND_LOGLEVEL_DESCRIPTION)}`, 'INFO')
824      .option('-s, --split', `${StringResource.getString(StringResourceId.COMMAND_SPLIT_API)}`, false)
825      .option('-b, --branch <string>', `${StringResource.getString(StringResourceId.COMMAND_BRANCH)}`, 'master')
826      .option('-t, --test', `${StringResource.getString(StringResourceId.COMMAND_TEST)}`, false);
827
828    program.parse();
829    const options = program.opts();
830    this.inputFilePath = options.input;
831    this.outputFilePath = options.output;
832    this.logLevel = options.logLevel;
833    this.splitUnionTypeApi = options.split;
834    this.branch = options.branch;
835    this.test = options.test;
836    this.checkInput();
837  }
838
839  private checkInput(): void {
840    this.inputFilePath = path.resolve(this.inputFilePath);
841    this.outputFilePath = this.outputFilePath ? path.resolve(this.outputFilePath) : undefined;
842
843    if (this.outputFilePath && (this.outputFilePath === path.basename(this.outputFilePath))) {
844      throw StringResource.getString(StringResourceId.INVALID_PATH);
845    }
846
847    this.checkFileExists(this.inputFilePath);
848
849    if (FileUtils.isDirectory(this.inputFilePath)) {
850      if (this.outputFilePath) {
851        const extName = path.extname(this.outputFilePath);
852        // if input is a directory, output must be a directory
853        if (extName !== '') {
854          throw `-o, --output ${StringResource.getString(StringResourceId.OUTPUT_MUST_DIR)}`;
855        }
856        const relativePath = path.relative(this.inputFilePath, this.outputFilePath);
857        // the output directory cannot be a subdirectory of the input directory
858        if (relativePath === '' || !relativePath.startsWith('..')) {
859          throw `-o, --output ${StringResource.getString(StringResourceId.OUTPUT_SUBDIR_INPUT)}`;
860        }
861      }
862    } else {
863      if (!this.inputFilePath.endsWith(ConstantValue.DTS_EXTENSION)) {
864        throw StringResource.getString(StringResourceId.NOT_DTS_FILE);
865      }
866      // if input is a file, output file must be a file
867      if (this.outputFilePath) {
868        const extName = path.extname(this.outputFilePath);
869        if (extName === '') {
870          throw `-o, --output ${StringResource.getString(StringResourceId.OUTPUT_MUST_FILE)}`;
871        }
872        // output file is same with input file
873        if (this.outputFilePath === this.inputFilePath) {
874          throw `-o, --output ${StringResource.getString(StringResourceId.OUTPUT_SAME_WITH_INPUT)}`;
875        }
876      }
877    }
878    this.options.splitUnionTypeApi = this.splitUnionTypeApi;
879    this.options.workingBranch = this.branch;
880    this.options.isTest = this.test;
881  }
882
883  private checkFileExists(filePath: string): void {
884    if (!FileUtils.isExists(filePath)) {
885      throw `${StringResource.getString(StringResourceId.INPUT_FILE_NOT_FOUND)}: ${filePath}`;
886    }
887  }
888
889  isHandleMultiFiles(): boolean {
890    return FileUtils.isDirectory(this.inputFilePath);
891  }
892
893  getOptions(): Options {
894    return this.options;
895  }
896}
897
898export class RawSourceCodeInfoImpl extends rawInfo.RawSourceCodeInfo {
899  rawInfoMap: Map<string, rawInfo.RawNodeInfo> = new Map();
900
901  addRawNodeInfo(ndoeSignature: string, node: ts.Node, line: number, character: number): void {
902    this.rawInfoMap.set(ndoeSignature, {
903      line: line,
904      character: character,
905      astNode: node
906    });
907  }
908
909  findRawNodeInfo(node: ts.Node): rawInfo.RawNodeInfo | undefined {
910    const nodeSignature = AstNodeHelper.getNodeSignature(node);
911    return this.rawInfoMap.get(nodeSignature);
912  }
913
914  toString(): string {
915    let value = '';
916    this.rawInfoMap.forEach((info, signature) => {
917      value += `[${signature}, ${info.line}]\n`;
918    });
919    return value;
920  }
921}
922
923/**
924 * 报告结果实现类
925 */
926export class LogReporterImpl implements LogReporter {
927
928  checkResults: Array<CheckLogResult> = [];
929  modifyResults: Array<ModifyLogResult> = [];
930  writer: LogWriter | undefined;
931
932  checkResultMap: Map<string, string> = new Map([
933    ['filePath', '文件地址'],
934    ['apiName', '接口名称'],
935    ['apiContent', '接口内容'],
936    ['errorType', '异常类型'],
937    ['errorInfo', '告警信息'],
938    ['version', '版本号'],
939    ['moduleName', '模块名称']
940  ]);
941
942  modifyResultMap: Map<string, string> = new Map([
943    ['filePath', '文件地址'],
944    ['apiName', '接口名称'],
945    ['apiContent', '接口内容'],
946    ['modifyType', '整改类型'],
947    ['description', '整改内容'],
948    ['version', '版本号'],
949    ['description', '整改说明'],
950    ['moduleName', '模块名称']
951  ]);
952
953  constructor() { }
954
955  setWriter(writer: LogWriter): void {
956    this.writer = writer;
957  }
958
959  addCheckResult(checkResult: CheckLogResult): void {
960    this.checkResults.push(checkResult);
961  }
962
963  addModifyResult(modifyResult: ModifyLogResult): void {
964    this.modifyResults.push(modifyResult);
965  }
966
967  getCheckResultMap(): Map<string, string> {
968    return this.checkResultMap;
969  }
970
971  getModifyResultMap(): Map<string, string> {
972    return this.modifyResultMap;
973  }
974
975  async writeCheckResults(path: string): Promise<void> {
976    await this.writer?.writeResults(this.checkResults, undefined, path);
977  }
978
979  async writeModifyResults(path: string): Promise<void> {
980    await this.writer?.writeResults(undefined, this.modifyResults, path);
981  }
982
983  async writeAllResults(path: string): Promise<void> {
984    await this.writer?.writeResults(this.checkResults, this.modifyResults, path);
985  }
986}
987
988export namespace logWriter {
989  export class ExcelWriter implements LogWriter {
990    workBook: excelJs.Workbook = new excelJs.Workbook();
991    checkResultsColumns: Array<object> = [];
992    modifyResultsColumns: Array<object> = [];
993
994    constructor(logReporter: LogReporter) {
995      // 初始化列名
996      this.initCheckResultsColumns(logReporter.getCheckResultMap());
997      this.initModifyResultsColumns(logReporter.getModifyResultMap());
998    }
999
1000    initCheckResultsColumns(checkResultMap: Map<string, string>): void {
1001      checkResultMap.forEach((value: string, key: string) => {
1002        this.checkResultsColumns.push({
1003          'header': value,
1004          'key': key
1005        });
1006      });
1007    }
1008
1009    initModifyResultsColumns(modifyResultMap: Map<string, string>): void {
1010      modifyResultMap.forEach((value: string, key: string) => {
1011        this.modifyResultsColumns.push({
1012          'header': value,
1013          'key': key
1014        });
1015      });
1016    }
1017
1018    async writeResults(checkResults: Array<CheckLogResult> | undefined,
1019      modifyResults: Array<ModifyLogResult> | undefined, filePath: string): Promise<void> {
1020      if (checkResults) {
1021        const checkResultsSheet: excelJs.Worksheet = this.workBook.addWorksheet('待确认报告');
1022        checkResultsSheet.columns = this.checkResultsColumns;
1023        checkResultsSheet.addRows(checkResults);
1024      }
1025      if (modifyResults) {
1026        const modifyResultsSheet: excelJs.Worksheet = this.workBook.addWorksheet('整改报告');
1027        modifyResultsSheet.columns = this.modifyResultsColumns;
1028        modifyResultsSheet.addRows(modifyResults);
1029      }
1030      const fileName: string = `${filePath}.xlsx`;
1031      await this.workBook.xlsx.writeFile(fileName);
1032    }
1033  }
1034
1035  interface JsonResult {
1036    checkResults: Array<CheckLogResult> | undefined,
1037    modifyResults: Array<ModifyLogResult> | undefined
1038  }
1039
1040  export class JsonWriter implements LogWriter {
1041    async writeResults(checkResults: Array<CheckLogResult> | undefined,
1042      modifyResults: Array<ModifyLogResult> | undefined, filePath: string): Promise<void> {
1043      const results: JsonResult = {
1044        checkResults: undefined,
1045        modifyResults: undefined
1046      };
1047      if (checkResults) {
1048        results.checkResults = checkResults;
1049      }
1050      if (modifyResults) {
1051        results.modifyResults = modifyResults;
1052      }
1053      const SPACE_NUMBER = 2;
1054      const jsonText: string = JSON.stringify(results, null, SPACE_NUMBER);
1055      const fileName: string = `${filePath}.json`;
1056      fs.writeFileSync(fileName, jsonText);
1057    }
1058  }
1059}
1060
1061export class LogResult {
1062  static createCheckResult(node: ts.Node, comments: Array<comment.CommentInfo>, errorInfo: string,
1063    context: Context | undefined, apiName: string, errorType: JSDocCheckErrorType): CheckLogResult {
1064    const url: string = context ? context.getInputFile() : '';
1065    const moduleName: string = path.basename(url, ConstantValue.DTS_EXTENSION);
1066    const rawCodeInfo = context ? context.getRawSourceCodeInfo().findRawNodeInfo(node) : undefined;
1067    const filePath: string = `${url}(${rawCodeInfo?.line},${rawCodeInfo?.character})`;
1068    let version = 'N/A';
1069    if (comments.length !== 0) {
1070      comments[comments.length - 1].commentTags.forEach((commentTag) => {
1071        if (commentTag.tag === 'since') {
1072          version = commentTag.name ? commentTag.name : 'N/A';
1073        }
1074      });
1075    }
1076    let apiContent: string = '';
1077    comments.forEach((curComment: comment.CommentInfo) => {
1078      if (curComment.isApiComment) {
1079        apiContent += `${curComment.text}\n`;
1080      }
1081    });
1082    apiContent += node.getText();
1083    const checkLogResult: CheckLogResult = {
1084      filePath: filePath,
1085      apiName: apiName,
1086      apiContent: apiContent,
1087      errorType: errorType,
1088      version: version,
1089      errorInfo: errorInfo,
1090      moduleName: moduleName
1091    };
1092    return checkLogResult;
1093  }
1094
1095  static createModifyResult(node: ts.Node, comments: Array<comment.CommentInfo>, description: string,
1096    context: Context | undefined, apiName: string, jsDocModifyType: JSDocModifyType): ModifyLogResult {
1097    const url: string = context ? context.getInputFile() : '';
1098    const moduleName: string = path.basename(url, ConstantValue.DTS_EXTENSION);
1099    const rawCodeInfo = context ? context.getRawSourceCodeInfo().findRawNodeInfo(node) : undefined;
1100    const filePath: string = `${url}(${rawCodeInfo?.line},${rawCodeInfo?.character})`;
1101    let version = 'N/A';
1102    if (comments.length !== 0) {
1103      comments[comments.length - 1].commentTags.forEach((commentTag) => {
1104        if (commentTag.tag === 'since') {
1105          version = commentTag.name ? commentTag.name : 'N/A';
1106        }
1107      });
1108    }
1109    let apiContent: string = '';
1110    comments.forEach((curComment: comment.CommentInfo) => {
1111      if (curComment.isApiComment) {
1112        apiContent += `${curComment.text}\n`;
1113      }
1114    });
1115    apiContent += node.getText();
1116    const modifyLogResult: ModifyLogResult = {
1117      filePath: filePath,
1118      apiName: apiName,
1119      apiContent: apiContent,
1120      modifyType: jsDocModifyType,
1121      version: version,
1122      description: description,
1123      moduleName: moduleName
1124    };
1125    return modifyLogResult;
1126  }
1127}