1/*
2* Copyright (c) 2021-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
16const whiteLists = require('../config/jsdocCheckWhiteList.json');
17const { parseJsDoc, commentNodeWhiteList, requireTypescriptModule, ErrorType, ErrorLevel, FileType, ErrorValueInfo,
18  createErrorInfo, isWhiteListFile } = require('./utils');
19const { checkApiOrder, checkAPITagName, checkInheritTag } = require('./check_jsdoc_value/check_order');
20const { addAPICheckErrorLogs } = require('./compile_info');
21const ts = requireTypescriptModule();
22
23// 标签合法性校验
24function checkJsDocLegality(node, comments, checkInfoMap) {
25  // since
26  legalityCheck(node, comments, commentNodeWhiteList, ['since'], true, checkInfoMap);
27  // syscap
28  legalityCheck(node, comments, getIllegalKinds([ts.SyntaxKind.ModuleDeclaration, ts.SyntaxKind.ClassDeclaration]),
29    ['syscap'], true, checkInfoMap);
30  // const定义语句必填
31  legalityCheck(node, comments, [ts.SyntaxKind.VariableStatement], ['constant'], true, checkInfoMap,
32    (currentNode, checkResult) => {
33      return (checkResult && (currentNode.kind !== ts.SyntaxKind.VariableStatement || !/^const\s/.test(currentNode.getText()))) ||
34        (!checkResult && currentNode.kind === ts.SyntaxKind.VariableStatement && /^const\s/.test(currentNode.getText()));
35    });
36  // 'enum'
37  legalityCheck(node, comments, [ts.SyntaxKind.EnumDeclaration], ['enum'], true, checkInfoMap);
38  // 'extends'
39  legalityCheck(node, comments, [ts.SyntaxKind.ClassDeclaration], ['extends'], true, checkInfoMap,
40    (currentNode, checkResult) => {
41      let tagCheckResult = false;
42      if (ts.isClassDeclaration(currentNode) && currentNode.heritageClauses) {
43        const clauses = currentNode.heritageClauses;
44        clauses.forEach(claus => {
45          if (/^extends\s/.test(claus.getText())) {
46            tagCheckResult = true;
47          }
48        });
49      }
50      return (checkResult && !tagCheckResult) || (!checkResult && tagCheckResult);
51    }
52  );
53  // 'namespace'
54  legalityCheck(node, comments, [ts.SyntaxKind.ModuleDeclaration], ['namespace'], true, checkInfoMap);
55  // 'param'
56  legalityCheck(node, comments, [ts.SyntaxKind.FunctionDeclaration, ts.SyntaxKind.MethodSignature,
57    ts.SyntaxKind.MethodDeclaration, ts.SyntaxKind.CallSignature, ts.SyntaxKind.Constructor,
58    ts.SyntaxKind.TypeAliasDeclaration], ['param'], true, checkInfoMap,
59    (currentNode, checkResult) => {
60      if (!new Set([ts.SyntaxKind.FunctionDeclaration, ts.SyntaxKind.MethodSignature,
61      ts.SyntaxKind.MethodDeclaration, ts.SyntaxKind.Constructor]).has(currentNode.kind)) {
62        return true;
63      }
64      return currentNode.parameters;
65    }
66  );
67  // 'returns'
68  legalityCheck(node, comments, [ts.SyntaxKind.FunctionDeclaration, ts.SyntaxKind.MethodSignature,
69    ts.SyntaxKind.MethodDeclaration, ts.SyntaxKind.CallSignature, ts.SyntaxKind.TypeAliasDeclaration],
70    ['returns'], true, checkInfoMap,
71    (currentNode, checkResult) => {
72      if (!checkResult && !new Set([ts.SyntaxKind.FunctionDeclaration, ts.SyntaxKind.MethodSignature,
73        ts.SyntaxKind.MethodDeclaration, ts.SyntaxKind.CallSignature,
74        ts.SyntaxKind.TypeAliasDeclaration]).has(currentNode.kind)) {
75        return false;
76      }
77      return !(!checkResult && !new Set([ts.SyntaxKind.FunctionDeclaration, ts.SyntaxKind.MethodSignature,
78        ts.SyntaxKind.MethodDeclaration, ts.SyntaxKind.CallSignature,
79        ts.SyntaxKind.TypeAliasDeclaration]).has(currentNode.kind)) && (currentNode.type &&
80        currentNode.type.kind !== ts.SyntaxKind.VoidKeyword);
81  }
82  );
83  // 'useinstead'
84  legalityCheck(node, comments, commentNodeWhiteList, ['useinstead'], true, checkInfoMap,
85    (currentNode, checkResult) => {
86      return new Set(commentNodeWhiteList).has(currentNode.kind);
87    }
88  );
89  // typedef/interface
90  legalityCheck(node, comments, [ts.SyntaxKind.InterfaceDeclaration, ts.SyntaxKind.TypeAliasDeclaration],
91    ['interface', 'typedef'], true, checkInfoMap);
92  // 'type', 'readonly'
93  legalityCheck(node, comments, [ts.SyntaxKind.PropertyDeclaration, ts.SyntaxKind.PropertySignature],
94    ['type', 'readonly'], false, checkInfoMap);
95  // 'default'
96  legalityCheck(node, comments, [ts.SyntaxKind.PropertyDeclaration, ts.SyntaxKind.PropertySignature,
97    ts.SyntaxKind.VariableStatement], ['default'], false, checkInfoMap);
98  return checkInfoMap;
99}
100exports.checkJsDocLegality = checkJsDocLegality;
101
102function getIllegalKinds(legalKinds) {
103  const illegalKinds = [];
104  const legalKindSet = new Set(legalKinds);
105  commentNodeWhiteList.forEach(kind => {
106    if (!legalKindSet.has(kind)) {
107      illegalKinds.push(kind);
108    }
109  });
110  return illegalKinds;
111}
112
113function dealSpecialTag(comment, tagName) {
114  let checkResult = false;
115  const useinsteadResultObj = {
116    hasUseinstead: false,
117    hasDeprecated: false,
118  };
119  let paramTagNum = 0;
120  comment.tags.forEach(tag => {
121    if (tagName === 'useinstead') {
122      if (tag.tag === tagName) {
123        useinsteadResultObj.hasUseinstead = true;
124      } else if (tag.tag === 'deprecated') {
125        useinsteadResultObj.hasDeprecated = true;
126      }
127    } else if (((tagName === 'interface' || tagName === 'typedef') && (tag.tag === 'interface' ||
128      tag.tag === 'typedef')) || tag.tag === tagName) {
129      checkResult = true;
130    }
131    if (tag.tag === 'param') {
132      paramTagNum++;
133    }
134  });
135  return {
136    useinsteadResultObj: useinsteadResultObj,
137    checkResult: checkResult,
138    paramTagNum: paramTagNum,
139  };
140}
141
142function legalityCheck(node, comments, legalKinds, tagsName, isRequire, checkInfoMap, extraCheckCallback) {
143  const illegalKinds = getIllegalKinds(legalKinds);
144  let illegalKindSet = new Set(illegalKinds);
145  const legalKindSet = new Set(legalKinds);
146  const isFunctionType = ts.SyntaxKind.FunctionType === node.type?.kind;
147  const functionTag = ['param', 'returns', 'throws'];
148  tagsName.forEach(tagName => {
149    if (tagName === 'extends') {
150      illegalKindSet = new Set(commentNodeWhiteList);
151    } else if (tagName === 'syscap') {
152      illegalKindSet = new Set([]);
153    }
154    if (functionTag.includes(tagName) && !isFunctionType) {
155      legalKindSet.delete(ts.SyntaxKind.TypeAliasDeclaration);
156    }
157    if (tagName === 'returns' && node.kind === ts.SyntaxKind.TypeAliasDeclaration && isFunctionType &&
158      node.type?.type?.kind === ts.SyntaxKind.VoidKeyword) {
159      legalKindSet.delete(ts.SyntaxKind.TypeAliasDeclaration);
160    }
161    comments.forEach((comment, index) => {
162      if (!checkInfoMap[index]) {
163        checkInfoMap[index] = {
164          missingTags: [],
165          illegalTags: [],
166        };
167      }
168      const dealSpecialTagResult = dealSpecialTag(comment, tagName);
169      let parameterNum = 0;
170      if (tagName === 'since') {
171      }
172      if (tagName === 'param' && (ts.isMethodDeclaration(node) || ts.isMethodSignature(node) ||
173        ts.isFunctionDeclaration(node) || ts.isCallSignatureDeclaration(node) || ts.isConstructorDeclaration(node) ||
174        ts.isTypeAliasDeclaration(node))) {
175        const parameterLength = ts.isTypeAliasDeclaration(node) ? node.type.parameters?.length : node.parameters.length;
176        parameterNum = parameterLength === undefined ? 0 : parameterLength;
177        checkResult = parameterNum !== dealSpecialTagResult.paramTagNum;
178      }
179      let extraCheckResult = false;
180      if (!extraCheckCallback) {
181        extraCheckResult = true;
182      } else {
183        extraCheckResult = extraCheckCallback(node, dealSpecialTagResult.checkResult);
184      }
185      // useinstead特殊处理
186      if (isRequire && tagName !== 'useinstead' && ((tagName !== 'useinstead' && tagName !== 'param' &&
187        !dealSpecialTagResult.checkResult && legalKindSet.has(node.kind)) || (tagName === 'param' &&
188          dealSpecialTagResult.paramTagNum < parameterNum)) && extraCheckResult) {
189        // 报错
190        checkInfoMap[index].missingTags.push(tagName);
191      } else if (((tagName !== 'useinstead' && tagName !== 'param' && dealSpecialTagResult.checkResult &&
192        illegalKindSet.has(node.kind)) || (tagName === 'useinstead' &&
193          !dealSpecialTagResult.useinsteadResultObj.hasDeprecated &&
194          dealSpecialTagResult.useinsteadResultObj.hasUseinstead) ||
195        (tagName === 'param' && dealSpecialTagResult.paramTagNum > parameterNum)) && extraCheckResult) {
196        // 报错
197        let errorInfo = createErrorInfo(ErrorValueInfo.ERROR_USE, [tagName]);
198        if (tagName === 'param') {
199          errorInfo = createErrorInfo(ErrorValueInfo.ERROR_MORELABEL, [parameterNum + 1, tagName]);
200        }
201        checkInfoMap[index].illegalTags.push({
202          checkResult: false,
203          errorInfo,
204          index,
205        });
206      }
207    });
208  });
209  return checkInfoMap;
210}
211
212// 标签重复性检查
213function checkTagsQuantity(comment, index, errorLogs) {
214  const multipleTags = ['throws', 'param'];
215  const tagCountObj = {};
216  comment.tags.forEach(tag => {
217    if (!tagCountObj[tag.tag]) {
218      tagCountObj[tag.tag] = 0;
219    }
220    tagCountObj[tag.tag] = tagCountObj[tag.tag] + 1;
221  });
222  for (const tagName in tagCountObj) {
223    if (tagCountObj[tagName] > 1 && multipleTags.indexOf(tagName) < 0) {
224      errorLogs.push({
225        checkResult: false,
226        errorInfo: createErrorInfo(ErrorValueInfo.ERROR_REPEATLABEL, [tagName]),
227        index,
228      });
229    }
230  }
231  // interface/typedef互斥校验
232  if (tagCountObj.interface > 0 & tagCountObj.typedef > 0) {
233    errorLogs.push({
234      checkResult: false,
235      errorInfo: ErrorValueInfo.ERROR_USE_INTERFACE,
236      index,
237    });
238  }
239}
240
241let paramIndex = 0;
242let throwsIndex = 0;
243
244function checkTagValue(tag, index, node, fileName, errorLogs) {
245  const { JsDocValueChecker } = require('./check_jsdoc_value/check_rest_value');
246  const checker = JsDocValueChecker[tag.tag];
247
248  if (checker) {
249    let valueCheckResult;
250    if (tag.tag === 'param' && [ts.SyntaxKind.FunctionDeclaration, ts.SyntaxKind.MethodSignature,
251      ts.SyntaxKind.MethodDeclaration, ts.SyntaxKind.CallSignature, ts.SyntaxKind.Constructor].indexOf(node.kind) >= 0) {
252      valueCheckResult = checker(tag, node, fileName, paramIndex++);
253    } else if (tag.tag === 'throws') {
254      valueCheckResult = checker(tag, node, fileName, throwsIndex++);
255    } else {
256      valueCheckResult = checker(tag, node, fileName);
257    }
258    if (!valueCheckResult.checkResult) {
259      valueCheckResult.index = index;
260      // 输出告警
261      errorLogs.push(valueCheckResult);
262    }
263  }
264}
265
266function checkJsDocOfCurrentNode(node, sourcefile, fileName, isGuard) {
267  const checkInfoArray = [];
268  const lastComment = parseJsDoc(node).length > 0 ? [parseJsDoc(node).pop()] : [];
269  const comments = isGuard ? lastComment : parseJsDoc(node);
270  const checkInfoMap = checkJsDocLegality(node, comments, {});
271  const checkOrderResult = checkApiOrder(comments);
272  checkOrderResult.forEach((result, index) => {
273    checkInfoMap[index.toString()].orderResult = result;
274  });
275  comments.forEach((comment, index) => {
276    const errorLogs = [];
277    // 继承校验
278    checkInheritTag(comment, node, sourcefile, fileName, index);
279    // 值检验
280    comment.tags.forEach(tag => {
281      const checkAPIDecorator = checkAPITagName(tag, node, sourcefile, fileName, index);
282      if (!checkAPIDecorator.checkResult) {
283        errorLogs.push(checkAPIDecorator);
284      }
285      checkTagValue(tag, index, node, fileName, errorLogs);
286    });
287    paramIndex = 0;
288    throwsIndex = 0;
289    // 标签数量校验
290    checkTagsQuantity(comment, index, errorLogs);
291    checkInfoMap[index.toString()].illegalTags = checkInfoMap[index.toString()].illegalTags.concat(errorLogs);
292  });
293  for (const key in checkInfoMap) {
294    checkInfoArray.push(checkInfoMap[key]);
295  }
296  return checkInfoArray;
297}
298exports.checkJsDocOfCurrentNode = checkJsDocOfCurrentNode;
299
300function checkJSDoc(node, sourcefile, fileName, isGuard) {
301  const verificationResult = checkJsDocOfCurrentNode(node, sourcefile, fileName, isGuard);
302
303  verificationResult.forEach(item => {
304    let errorInfo = '';
305    if (item.missingTags.length > 0) {
306      item.missingTags.forEach(lostLabel => {
307        errorInfo = createErrorInfo(ErrorValueInfo.ERROR_LOST_LABEL, [lostLabel]);
308        addAPICheckErrorLogs(node, sourcefile, fileName, ErrorType.WRONG_SCENE, errorInfo, FileType.JSDOC,
309          ErrorLevel.MIDDLE);
310      });
311    }
312    if (item.illegalTags.length > 0) {
313      item.illegalTags.forEach(wrongValueLabel => {
314        errorInfo = wrongValueLabel.errorInfo;
315        addAPICheckErrorLogs(node, sourcefile, fileName, ErrorType.WRONG_VALUE, errorInfo, FileType.JSDOC,
316          ErrorLevel.MIDDLE);
317      });
318    }
319    if (!item.orderResult.checkResult) {
320      errorInfo = item.orderResult.errorInfo;
321      addAPICheckErrorLogs(node, sourcefile, fileName, ErrorType.WRONG_ORDER, errorInfo, FileType.JSDOC,
322        ErrorLevel.MIDDLE);
323    }
324  });
325}
326exports.checkJSDoc = checkJSDoc;
327