1/*
2 * Copyright (c) 2021-2022 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 fs = require('fs');
17const path = require('path');
18const { exportDiffInfo } = require('../../api_diff/src/api_diff');
19const { StatusCode } = require('../../api_diff/src/reporter');
20const {
21  parseJsDoc,
22  requireTypescriptModule,
23  ErrorType,
24  LogType,
25  ErrorLevel,
26  ErrorValueInfo,
27  getCheckApiVersion,
28  FUNCTION_TYPES,
29  DIFF_INFO,
30  createErrorInfo
31} = require('./utils');
32const ts = requireTypescriptModule();
33const { addAPICheckErrorLogs } = require('./compile_info');
34
35const changeErrors = [];
36
37/**
38 * 检查历史JSDoc是否一致
39 * @param {array} newNodeJSDocs 修改后API节点JSDoc数组
40 * @param {array} oldNodeJSDocs 修改前API节点JSDoc数组
41 * @returns {boolean}
42 */
43function checkHistoryJSDoc(newNodeJSDocs, oldNodeJSDocs) {
44  let checkEndJSDocIndex = isNewApi(oldNodeJSDocs) ? 1 : 0;
45  for (let i = 0; i < oldNodeJSDocs.length - checkEndJSDocIndex; i++) {
46    const oldDescription = oldNodeJSDocs[i].description;
47    const oldTags = oldNodeJSDocs[i].tags;
48    const newDescription = newNodeJSDocs[i].description;
49    const newTags = newNodeJSDocs[i].tags;
50    if (oldDescription !== newDescription || oldTags.length !== newTags.length) {
51      return false;
52    }
53    for (let j = 0; j < oldTags.length; j++) {
54      const oldTag = oldTags[j];
55      const newTag = newTags[j];
56      if (
57        oldTag.tag !== newTag.tag ||
58        oldTag.name !== newTag.name ||
59        oldTag.type !== newTag.type ||
60        oldTag.optional !== newTag.optional ||
61        oldTag.description !== newTag.description
62      ) {
63        return false;
64      }
65    }
66  }
67  return true;
68}
69
70/**
71 * 根据JSDoc获取版本号
72 * @param {JSDoc} JSDoc
73 * @returns {number}
74 */
75function getJSDocVersion(JSDoc) {
76  if (JSDoc) {
77    for (let i = 0; i < JSDoc.tags.length; i++) {
78      if (JSDoc.tags[i].tag === 'since') {
79        return JSDoc.tags[i].name;
80      }
81    }
82  }
83  return NaN;
84}
85
86/**
87 * 检查API变更版本是否正确
88 * @param {JSDoc} currentJSDoc 修改后API节点JSDoc
89 * @param {JSDoc} lastJSDoc 修改前API节点JSDoc
90 * @param {ts.Node} node 修改后API节点
91 */
92function checkApiChangeVersion(currentJSDoc, lastJSDoc, node) {
93  const currentVersion = getJSDocVersion(currentJSDoc);
94  const lastVersion = getJSDocVersion(lastJSDoc);
95  const checkApiVersion = getCheckApiVersion();
96  if (lastVersion === 0 || currentVersion !== checkApiVersion) {
97    changeErrors.push({
98      node: node,
99      errorInfo: createErrorInfo(ErrorValueInfo.ERROR_CHANGES_VERSION, [checkApiVersion]),
100      LogType: LogType.LOG_JSDOC,
101    });
102  }
103}
104
105/**
106 * 检查JSDoc变更
107 * @param {string} tagName 标签名称
108 * @param {JSDoc} currentJSDoc 修改后API节点JSDoc
109 * @param {JSDoc} lastJSDoc 修改前API节点JSDoc
110 * @param {function} customCheckCallback 自定义检查规则
111 */
112function checkJSDocChange(tagName, currentJSDoc, lastJSDoc, customCheckCallback) {
113  const newTagValue = [];
114  const oldTagValue = [];
115  const addTags = [];
116  if (currentJSDoc) {
117    currentJSDoc.tags.forEach(tag => {
118      if (tag.tag === tagName) {
119        newTagValue.push(tag.name);
120      }
121    });
122  }
123  if (lastJSDoc) {
124    lastJSDoc.tags.forEach(tag => {
125      if (tag.tag === tagName) {
126        oldTagValue.push(tag.name);
127      }
128    });
129  }
130  newTagValue.forEach(newValue => {
131    if (!new Set(oldTagValue).has(newValue)) {
132      addTags.push(newValue);
133    }
134  });
135
136  customCheckCallback(newTagValue, oldTagValue, addTags);
137}
138
139/**
140 * 检查权限变化
141 * @param { string } newPermission
142 * @param { string } oldPermission
143 * @returns { boolean }
144 */
145function checkPermissionChange(newPermission, oldPermission) {
146  const permissionChange = newPermission.replace(oldPermission, '');
147  return !newPermission.includes(oldPermission) || /\band\b/.test(permissionChange);
148}
149
150/**
151 * 检查JSDoc变更
152 * @param {array} newNodeJSDocs 修改后API节点JSDoc列表
153 * @param {enum} statusCode api_diff工具返回的变更状态
154 * @param {ts.Node} node 修改后API节点
155 */
156function checkCurrentJSDocChange(newNodeJSDocs, statusCode, node) {
157  const currentJSDoc = newNodeJSDocs[newNodeJSDocs.length - 1];
158  const lastJSDoc =
159    newNodeJSDocs.length === DIFF_INFO.NEW_JSDOCS_LENGTH
160      ? null
161      : newNodeJSDocs[newNodeJSDocs.length - DIFF_INFO.NEW_JSDOC_INDEX];
162
163  checkApiChangeVersion(currentJSDoc, lastJSDoc, node);
164
165  if (statusCode === StatusCode.ERRORCODE_CHANGES || statusCode === StatusCode.NEW_ERRORCODE) {
166    checkJSDocChange('throws', currentJSDoc, lastJSDoc, (newTagValue, oldTagValue, addTags) => {
167      if (addTags.length !== 0 && oldTagValue.length === 0) {
168        changeErrors.push({
169          node: node,
170          errorInfo: ErrorValueInfo.ERROR_CHANGES_JSDOC_TRROWS,
171          LogType: LogType.LOG_JSDOC,
172        });
173      }
174    });
175  } else if (statusCode === StatusCode.PERMISSION_CHANGES) {
176    checkJSDocChange('permission', currentJSDoc, lastJSDoc, (newTagValue, oldTagValue, addTags) => {
177      let checkResult = true;
178      // 从无到有新增权限
179      checkResult = !(newTagValue.length === 1 && oldTagValue.length === 0);
180      // 权限值变更
181      if (newTagValue.length === 1 && oldTagValue.length === 1) {
182        const newPermission = newTagValue[0];
183        const oldPermission = oldTagValue[0];
184        checkResult = checkResult &&
185          !(newPermission !== oldPermission && checkPermissionChange(newPermission, oldPermission));
186      }
187
188      if (!checkResult) {
189        changeErrors.push({
190          node: node,
191          errorInfo: ErrorValueInfo.ERROR_CHANGES_JSDOC_PERMISSION,
192          LogType: LogType.LOG_JSDOC,
193        });
194      }
195    });
196  }
197}
198
199/**
200 * 检查API历史版本JSDoc是否包含废弃标签
201 *
202 * @param {array} historyJSDocs 历史接口JSDoc信息
203 * @returns {boolean}
204 */
205function checkApiDeprecatedStatus(historyJSDocs) {
206  for (let i = 0; i < historyJSDocs.length; i++) {
207    const doc = historyJSDocs[i];
208    for (let j = 0; j < doc.tags.length; j++) {
209      const tag = doc.tags[j];
210      if (tag === 'deprecated') {
211        return true;
212      }
213    }
214  }
215  return false;
216}
217
218/**
219 * 检查JSDoc变更内容
220 * @param {array} newNodeJSDocs 修改后API节点JSDoc数组
221 * @param {array} oldNodeJSDocs 修改前API节点JSDoc数组
222 * @param {object} change api_diff获取的变更数据
223 */
224function checkJSDocChangeInfo(newNodeJSDocs, oldNodeJSDocs, change) {
225  if (checkApiDeprecatedStatus(oldNodeJSDocs)) {
226    changeErrors.push({
227      node: change.newNode,
228      errorInfo: ErrorValueInfo.ERROR_CHANGES_DEPRECATED,
229      LogType: LogType.LOG_JSDOC,
230    });
231  }
232  if (newNodeJSDocs.length !== oldNodeJSDocs.length + 1 && !isNewApi(oldNodeJSDocs)) {
233    changeErrors.push({
234      node: change.newNode,
235      errorInfo: ErrorValueInfo.ERROR_CHANGES_JSDOC_NUMBER,
236      LogType: LogType.LOG_JSDOC,
237    });
238  } else if (!checkHistoryJSDoc(newNodeJSDocs, oldNodeJSDocs)) {
239    changeErrors.push({
240      node: change.newNode,
241      errorInfo: ErrorValueInfo.ERROR_CHANGES_JSDOC_CHANGE,
242      LogType: LogType.LOG_JSDOC,
243    });
244  } else {
245    checkCurrentJSDocChange(newNodeJSDocs, change.statusCode, change.newNode);
246  }
247}
248
249/**
250 * 检查JSDoc变更
251 * @param {object} change api_diff获取的变更数据
252 */
253function checkJSDocChangeEntry(change) {
254  const newNodeJSDocs = parseJsDoc(change.newNode);
255  const oldNodeJSDocs = parseJsDoc(change.oldNode);
256
257  checkJSDocChangeInfo(newNodeJSDocs, oldNodeJSDocs, change);
258}
259
260/**
261 * 解析接口参数类型数据
262 * @param {array} paramType 接口参数类型
263 * @returns {array}
264 */
265function analysisParamType(paramType) {
266  const types = [];
267  if (paramType.kind === ts.SyntaxKind.UnionType) {
268    paramType.types.forEach(type => {
269      types.push(type.getText());
270    });
271  } else {
272    types.push(paramType.getText());
273  }
274  return types;
275}
276
277/**
278 * 检查API接口历史参数
279 * @param {array} currentParameters 修改后API节点参数
280 * @param {array} lastParameters 修改前API节点参数
281 */
282function checkHistoryParameters(currentParameters, lastParameters, change) {
283  for (let i = currentParameters.length - 1; i >= 0; i--) {
284    const historyParamType = analysisParamType(lastParameters[i].type);
285    const currentParamType = analysisParamType(currentParameters[i].type);
286
287    // 拦截可选变必选
288    if (currentParameters[i].isRequired && !lastParameters[i].isRequired) {
289      changeErrors.push({
290        node: change.newNode,
291        errorInfo: ErrorValueInfo.ERROR_CHANGES_API_HISTORY_PARAM_REQUIRED_CHANGE,
292        LogType: LogType.LOG_API,
293      });
294    }
295
296    // 变更后参数无类型定义
297    if (currentParamType.length === 0) {
298      changeErrors.push({
299        node: change.newNode,
300        errorInfo: ErrorValueInfo.ERROR_CHANGES_API_HISTORY_PARAM_WITHOUT_TYPE_CHANGE,
301        LogType: LogType.LOG_API,
302      });
303      // 变更后参数范围大于等于变更前
304    } else if (currentParamType.length >= historyParamType.length) {
305      checkHistoryParametersType(historyParamType, currentParamType, changeErrors, change);
306      // 变更后参数范围小于变更前
307    } else {
308      changeErrors.push({
309        node: change.newNode,
310        errorInfo: ErrorValueInfo.ERROR_CHANGES_API_HISTORY_PARAM_RANGE_CHANGE,
311        LogType: LogType.LOG_API,
312      });
313    }
314  }
315
316  // 拦截参数位置变更
317  for (let i = 0; i < currentParameters.length; i++) {
318    for (let j = 0; j < lastParameters.length; j++) {
319      if (
320        currentParameters[i].paramName === lastParameters[j].paramName &&
321        currentParameters[i].order !== lastParameters[j].order
322      ) {
323        changeErrors.push({
324          node: change.newNode,
325          errorInfo: ErrorValueInfo.ERROR_CHANGES_API_HISTORY_PARAM_POSITION_CHANGE,
326          LogType: LogType.LOG_API,
327        });
328      }
329    }
330  }
331}
332
333function checkHistoryParametersType(historyParamType, currentParamType, changeErrors, change) {
334  for (let j = 0; j < historyParamType.length; j++) {
335    if (!new Set(currentParamType).has(historyParamType[j])) {
336      changeErrors.push({
337        node: change.newNode,
338        errorInfo: ErrorValueInfo.ERROR_CHANGES_API_HISTORY_PARAM_TYPE_CHANGE,
339        LogType: LogType.LOG_API,
340      });
341    }
342  }
343}
344
345/**
346 * 检查API接口新增参数
347 * @param {array} parameters 新增参数列表
348 */
349function checkCurrentParameters(parameters, change) {
350  for (let i = 0; i < parameters.length; i++) {
351    if (parameters[i].isRequired) {
352      changeErrors.push({
353        node: change.newNode,
354        errorInfo: ErrorValueInfo.ERROR_CHANGES_API_NEW_REQUIRED_PARAM,
355        LogType: LogType.LOG_API,
356      });
357      break;
358    }
359  }
360}
361
362/**
363 * 解析接口参数数据
364 * @param {array} params tsNode参数列表
365 * @returns {array}
366 */
367function analysisParameters(params) {
368  const paramInfoData = [];
369  params.forEach((param, index) => {
370    const data = {
371      paramName: param.name ? param.name.getText() : '',
372      order: index,
373      isRequired: param.questionToken && param.questionToken.kind === ts.SyntaxKind.QuestionToken ? false : true,
374      type: param.type,
375    };
376    paramInfoData.push(data);
377  });
378  return paramInfoData;
379}
380
381/**
382 * 判断是否为新增接口或已变更为最新版本接口
383 * @param {array} oldNodeJSDocs 修改前API节点JSDoc数组
384 */
385function isNewApi(oldNodeJSDocs) {
386  const checkApiVersion = getCheckApiVersion();
387  const oldNodeVersion = getJSDocVersion(oldNodeJSDocs[oldNodeJSDocs.length - 1]);
388
389  if (oldNodeVersion === checkApiVersion) {
390    return true;
391  }
392  return false;
393}
394
395/**
396 * 检查API变更
397 * @param {object} change api_diff获取的变更数据
398 */
399function checkApiChangeEntry(change) {
400  // 检查JSDoc
401  const newNodeJSDocs = parseJsDoc(change.newNode);
402  const oldNodeJSDocs = parseJsDoc(change.oldNode);
403
404  checkJSDocChangeInfo(newNodeJSDocs, oldNodeJSDocs, change);
405
406  // 新增接口不检查接口变更
407  if (isNewApi(oldNodeJSDocs) && oldNodeJSDocs.length === 1) {
408    return;
409  }
410  const currentParameters = analysisParameters(change.newNode.parameters);
411  const lastParameters = analysisParameters(change.oldNode.parameters);
412
413  if (currentParameters.length === lastParameters.length) {
414    checkHistoryParameters(currentParameters, lastParameters, change);
415  } else if (currentParameters.length > lastParameters.length) {
416    if (lastParameters.length !== 0) {
417      checkHistoryParameters(currentParameters.slice(0, lastParameters.length), lastParameters, change);
418    }
419    checkCurrentParameters(currentParameters.slice(lastParameters.length, currentParameters.length), change);
420  } else {
421    changeErrors.push({
422      node: change.newNode,
423      errorInfo: ErrorValueInfo.ERROR_CHANGES_API_DELETE_PARAM,
424      LogType: LogType.LOG_API,
425    });
426  }
427}
428
429/**
430 * 分析变更内容
431 * @param {array} changes api_diff获取的变更数据列表
432 */
433function analyseChanges(changes) {
434  const functionTypeSet = new Set(FUNCTION_TYPES);
435  changes.forEach(change => {
436    if (
437      change.statusCode === StatusCode.ERRORCODE_CHANGES ||
438      change.statusCode === StatusCode.NEW_ERRORCODE ||
439      change.statusCode === StatusCode.PERMISSION_CHANGES
440    ) {
441      checkJSDocChangeEntry(change);
442    } else if (
443      change.statusCode === StatusCode.FUNCTION_CHANGES &&
444      functionTypeSet.has(change.oldNode.kind) &&
445      functionTypeSet.has(change.newNode.kind)
446    ) {
447      checkApiChangeEntry(change);
448    }
449  });
450}
451
452/**
453 * 封装错误信息
454 */
455function logChangeErrors() {
456  changeErrors.forEach(error => {
457    const sourceFileNode = ts.getSourceFileOfNode(error.node);
458    addAPICheckErrorLogs(
459      error.node,
460      sourceFileNode,
461      sourceFileNode.fileName,
462      ErrorType.API_CHANGE_ERRORS,
463      error.errorInfo,
464      error.LogType,
465      ErrorLevel.MIDDLE
466    );
467  });
468}
469
470/**
471 * API变更检查入口
472 */
473function checkApiChanges(prId) {
474  let isApiChanged = false;
475  const oldFiles = [];
476  // 编译流水线根目录
477  const rootDir = path.resolve(__dirname, `../../../../../Archive/patch_info/openharmony_interface_sdk-js_${prId}`);
478  if (!fs.existsSync(rootDir)) {
479    return;
480  }
481  const oldApiPath = path.resolve(rootDir, './old');
482  const newFiles = [];
483  const newApiPath = path.resolve(rootDir, './new');
484  const fileNames = fs.readdirSync(rootDir);
485  let patchConfigPath = '';
486  for (let i = 0; i < fileNames.length; i++) {
487    if (/\.json$/.test(fileNames[i])) {
488      patchConfigPath = path.resolve(rootDir, fileNames[i]);
489      break;
490    }
491  }
492  const patchConfig = JSON.parse(fs.readFileSync(patchConfigPath));
493  for (const file in patchConfig) {
494    // 判断为文件修改
495    if (patchConfig[file] === 'M' && /\.d\.ts$/.test(file)) {
496      const oldMdFilePath = path.resolve(oldApiPath, file);
497      const newMdFilePath = path.resolve(newApiPath, file);
498
499      if (fs.existsSync(oldMdFilePath) && fs.existsSync(newMdFilePath)) {
500        oldFiles.push(oldMdFilePath);
501        newFiles.push(newMdFilePath);
502        isApiChanged = true;
503      }
504    }
505  }
506
507  if (isApiChanged) {
508    const diffResult = exportDiffInfo(newFiles, oldFiles, newApiPath, oldApiPath);
509    analyseChanges(diffResult);
510    logChangeErrors();
511  }
512}
513exports.checkApiChanges = checkApiChanges;
514