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