• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2024 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 path from 'path';
18import fs from 'fs';
19
20import {
21  projectConfig,
22  extendSdkConfigs,
23  globalProgram,
24  ohosSystemModulePaths,
25  systemModules,
26  allModulesPaths,
27  ohosSystemModuleSubDirPaths
28} from '../../../main';
29import {
30  LogType,
31  LogInfo,
32  IFileLog,
33  CurrentProcessFile
34} from '../../utils';
35import { type ResolveModuleInfo } from '../../ets_checker';
36import {
37  PERMISSION_TAG_CHECK_NAME,
38  PERMISSION_TAG_CHECK_ERROR,
39  SYSTEM_API_TAG_CHECK_NAME,
40  SYSTEM_API_TAG_CHECK_WARNING,
41  TEST_TAG_CHECK_NAME,
42  TEST_TAG_CHECK_ERROR,
43  SYSCAP_TAG_CHECK_NAME,
44  SYSCAP_TAG_CONDITION_CHECK_WARNING,
45  SYSCAP_TAG_CHECK_WARNING,
46  CANIUSE_FUNCTION_NAME,
47  FORM_TAG_CHECK_NAME,
48  FORM_TAG_CHECK_ERROR,
49  FIND_MODULE_WARNING,
50  CROSSPLATFORM_TAG_CHECK_NAME,
51  CROSSPLATFORM_TAG_CHECK_ERROER,
52  DEPRECATED_TAG_CHECK_NAME,
53  DEPRECATED_TAG_CHECK_WARNING,
54  FA_TAG_CHECK_NAME,
55  FA_TAG_HUMP_CHECK_NAME,
56  FA_TAG_CHECK_ERROR,
57  STAGE_TAG_CHECK_NAME,
58  STAGE_TAG_HUMP_CHECK_NAME,
59  STAGE_TAG_CHECK_ERROR,
60  STAGE_COMPILE_MODE,
61  ATOMICSERVICE_BUNDLE_TYPE,
62  ATOMICSERVICE_TAG_CHECK_NAME,
63  ATOMICSERVICE_TAG_CHECK_ERROER,
64  ATOMICSERVICE_TAG_CHECK_VERSION,
65  RUNTIME_OS_OH,
66  CONSTANT_STEP_0,
67  CONSTANT_STEP_1,
68  CONSTANT_STEP_2,
69  CONSTANT_STEP_3,
70  GLOBAL_DECLARE_WHITE_LIST,
71  SINCE_TAG_NAME,
72  SINCE_TAG_CHECK_ERROER,
73  VERSION_CHECK_FUNCTION_NAME
74} from './api_check_define';
75import { JsDocCheckService } from './api_check_permission';
76
77/**
78 * bundle info
79 *
80 * @interface BundleInfo
81 */
82interface BundleInfo {
83  bundlePath: string;
84  bundleVersion: string;
85}
86
87export interface CheckValidCallbackInterface {
88  (jsDocTag: ts.JSDocTag, config: ts.JsDocNodeCheckConfigItem): boolean;
89}
90
91export interface CheckJsDocSpecialValidCallbackInterface {
92  (jsDocTags: readonly ts.JSDocTag[], config: ts.JsDocNodeCheckConfigItem): boolean;
93}
94
95export interface checkConditionValidCallbackInterface {
96  (node: ts.CallExpression, specifyFuncName: string, importSymbol: string, jsDocs?: ts.JSDoc[]): boolean;
97}
98
99interface HasJSDocNode extends ts.Node {
100  jsDoc?: ts.JSDoc[];
101}
102
103/**
104 * get the bundleInfo of ohm
105 *
106 * @param {string} modulePath
107 * @return {BundleInfo}
108 */
109function parseOhmBundle(modulePath: string): BundleInfo {
110  const apiCode: string = fs.readFileSync(modulePath, { encoding: 'utf-8' });
111  const bundleTags: string[] = apiCode.match(/@bundle.+/g);
112  const bundleInfo: BundleInfo = {
113    bundlePath: '',
114    bundleVersion: ''
115  };
116  if (bundleTags && bundleTags.length > CONSTANT_STEP_0) {
117    const bundleTag: string = bundleTags[CONSTANT_STEP_0];
118    const bundleInfos: string[] = bundleTag.split(' ');
119    if (bundleInfos.length === CONSTANT_STEP_3) {
120      bundleInfo.bundlePath = bundleInfos[CONSTANT_STEP_1];
121      bundleInfo.bundleVersion = bundleInfos[CONSTANT_STEP_2];
122    }
123  }
124  return bundleInfo;
125}
126
127/**
128 * jude a version string , string has two format
129 *   xx:is a number and need greater than 10
130 *   x.x.x: a string join '.', the first part and second part is number and need greater than 4.1
131 *
132 * @param {string} bundleVersion - version string
133 * @returns {boolean}
134 */
135function checkBundleVersion(bundleVersion: string): boolean {
136  if (!projectConfig.compatibleSdkVersion) {
137    return true;
138  }
139  const compatibleSdkVersion: string = projectConfig.compatibleSdkVersion;
140  let bundleVersionNumber: number = 0;
141  const bundleVersionArr = bundleVersion.match(/(?<=\().*(?=\))/g);
142  if (bundleVersionArr && bundleVersionArr.length === 1) {
143    bundleVersionNumber = Number(bundleVersionArr[CONSTANT_STEP_0]);
144  } else {
145    bundleVersionNumber = Number(bundleVersion);
146  }
147  if (bundleVersion && bundleVersion !== '' && !isNaN(bundleVersionNumber) &&
148    !isNaN(Number(compatibleSdkVersion)) && Number(compatibleSdkVersion) >= bundleVersionNumber) {
149    return true;
150  }
151  return false;
152}
153
154/**
155 * get the real path about a list in module path
156 *
157 * @param {string[]} apiDirs - file list
158 * @param {string} moduleName - module dir
159 * @param {string[]} exts - ext
160 * @returns  {ResolveModuleInfo}
161 */
162export function getRealModulePath(apiDirs: string[], moduleName: string, exts: string[]): ResolveModuleInfo {
163  const resolveResult: ResolveModuleInfo = {
164    modulePath: '',
165    isEts: true
166  };
167  for (let i = 0; i < apiDirs.length; i++) {
168    const dir = apiDirs[i];
169    for (let i = 0; i < exts.length; i++) {
170      const ext = exts[i];
171      const moduleDir = path.resolve(dir, moduleName + ext);
172      if (!fs.existsSync(moduleDir)) {
173        continue;
174      }
175      resolveResult.modulePath = moduleDir;
176      if (ext === '.d.ts') {
177        resolveResult.isEts = false;
178      }
179      break;
180    }
181  }
182  return resolveResult;
183}
184
185/**
186 * get a request path about ohos
187 *
188 * @param {string} moduleRequest - import request path
189 * @param {string} _ - import request path
190 * @param {number} moduleType
191 * @param {string} systemKey
192 * @returns {string}
193 */
194export function moduleRequestCallback(moduleRequest: string, _: string,
195  moduleType: string, systemKey: string): string {
196  for (const config of extendSdkConfigs.values()) {
197    if (config.prefix === '@arkui-x') {
198      continue;
199    }
200    if (moduleRequest.startsWith(config.prefix + '.')) {
201      let compileRequest: string = `${config.prefix}:${systemKey}`;
202      const resolveModuleInfo: ResolveModuleInfo = getRealModulePath(config.apiPath, moduleRequest,
203        ['.d.ts', '.d.ets']);
204      const modulePath: string = resolveModuleInfo.modulePath;
205      if (!fs.existsSync(modulePath)) {
206        return compileRequest;
207      }
208      const bundleInfo: BundleInfo = parseOhmBundle(modulePath);
209      if (checkBundleVersion(bundleInfo.bundleVersion)) {
210        compileRequest = `@bundle:${bundleInfo.bundlePath}`;
211      }
212      return compileRequest;
213    }
214  }
215  return '';
216}
217
218/**
219 * check arkui dependences in ts files
220 * api check from sdk
221 *
222 * @param {ts.TypeReferenceNode} node - typeReferenceNode
223 * @param {IFileLog} transformLog - log info
224 */
225export function checkTypeReference(node: ts.TypeReferenceNode, transformLog: IFileLog): void {
226  const fileName: string = transformLog.sourceFile.fileName;
227  const currentTypeName: string = node.getText();
228  if (/(?<!\.d)\.ts$/g.test(fileName)) {
229    const checker: ts.TypeChecker | undefined = CurrentProcessFile.getChecker();
230    if (!checker) {
231      return;
232    }
233    const type: ts.Type = checker.getTypeAtLocation(node);
234    let sourceFile: ts.SourceFile | undefined;
235    if (type && type.aliasSymbol && type.aliasSymbol.declarations && type.aliasSymbol.declarations.length > 0) {
236      sourceFile = ts.getSourceFileOfNode(type.aliasSymbol.declarations[0]);
237    } else if (type && type.symbol && type.symbol.declarations && type.symbol.declarations.length > 0) {
238      sourceFile = ts.getSourceFileOfNode(type.symbol.declarations[0]);
239    }
240    if (!sourceFile) {
241      return;
242    }
243    const sourceBaseName: string = path.basename(sourceFile.fileName);
244    if (isArkuiDependence(sourceFile.fileName) &&
245      sourceBaseName !== 'common_ts_ets_api.d.ts' &&
246      sourceBaseName !== 'global.d.ts'
247    ) {
248      // TODO: change to error
249      transformLog.errors.push({
250        type: LogType.WARN,
251        message: `Cannot find name '${currentTypeName}'.`,
252        pos: node.getStart()
253      });
254    } else if (GLOBAL_DECLARE_WHITE_LIST.has(currentTypeName) &&
255      ohosSystemModulePaths.includes(sourceFile.fileName.replace(/\//g, '\\'))) {
256      transformLog.errors.push({
257        type: LogType.WARN,
258        message: `Cannot find name '${currentTypeName}'.`,
259        pos: node.getStart()
260      });
261    }
262  }
263}
264
265/**
266 * get jsDocNodeCheckConfigItem object
267 *
268 * @param {string[]} tagName - tag name
269 * @param {string} message - error message
270 * @param {ts.DiagnosticCategory} type - error type
271 * @param {boolean} tagNameShouldExisted - tag is required
272 * @param {CheckValidCallbackInterface} checkValidCallback
273 * @param {CheckJsDocSpecialValidCallbackInterface} checkJsDocSpecialValidCallback
274 * @param {checkConditionValidCallbackInterface} checkConditionValidCallback
275 * @returns  {ts.JsDocNodeCheckConfigItem}
276 */
277function getJsDocNodeCheckConfigItem(tagName: string[], message: string, needConditionCheck: boolean,
278  type: ts.DiagnosticCategory, specifyCheckConditionFuncName: string,
279  tagNameShouldExisted: boolean, checkValidCallback?: CheckValidCallbackInterface,
280  checkJsDocSpecialValidCallback?: CheckJsDocSpecialValidCallbackInterface,
281  checkConditionValidCallback?: checkConditionValidCallbackInterface): ts.JsDocNodeCheckConfigItem {
282  return {
283    tagName: tagName,
284    message: message,
285    needConditionCheck: needConditionCheck,
286    type: type,
287    specifyCheckConditionFuncName: specifyCheckConditionFuncName,
288    tagNameShouldExisted: tagNameShouldExisted,
289    checkValidCallback: checkValidCallback,
290    checkJsDocSpecialValidCallback: checkJsDocSpecialValidCallback,
291    checkConditionValidCallback: checkConditionValidCallback
292  };
293}
294
295/**
296 * judge a file is card file
297 *
298 * @param {string} file - file path
299 * @returns {boolean}
300 */
301export function isCardFile(file: string): boolean {
302  for (const key in projectConfig.cardEntryObj) {
303    if (path.normalize(projectConfig.cardEntryObj[key]) === path.normalize(file)) {
304      return true;
305    }
306  }
307  return false;
308}
309
310const jsDocNodeCheckConfigCache: Map<string, Map<string, ts.JsDocNodeCheckConfig>> = new Map<string, Map<string, ts.JsDocNodeCheckConfig>>();
311let permissionsArray: string[] = [];
312/**
313 * get tagName where need to be determined based on the file path
314 *
315 * @param {string} fileName - file name
316 * @param {string} sourceFileName - resource reference path
317 * @returns {ts.JsDocNodeCheckConfig}
318 */
319export function getJsDocNodeCheckConfig(fileName: string, sourceFileName: string): ts.JsDocNodeCheckConfig {
320  let byFileName: Map<string, ts.JsDocNodeCheckConfig> | undefined = jsDocNodeCheckConfigCache.get(fileName);
321  if (byFileName === undefined) {
322    byFileName = new Map<string, ts.JsDocNodeCheckConfig>();
323    jsDocNodeCheckConfigCache.set(fileName, byFileName);
324  }
325  let result: ts.JsDocNodeCheckConfig | undefined = byFileName.get(sourceFileName);
326  if (result !== undefined) {
327    return result;
328  }
329  let needCheckResult: boolean = false;
330  const checkConfigArray: ts.JsDocNodeCheckConfigItem[] = [];
331  const apiName: string = path.basename(fileName);
332  const sourceBaseName: string = path.basename(sourceFileName);
333  if (/(?<!\.d)\.ts$/g.test(fileName) && isArkuiDependence(sourceFileName) &&
334    sourceBaseName !== 'common_ts_ets_api.d.ts' && sourceBaseName !== 'global.d.ts') {
335    checkConfigArray.push(getJsDocNodeCheckConfigItem([], FIND_MODULE_WARNING, false, ts.DiagnosticCategory.Warning,
336      '', true));
337  }
338  if (!systemModules.includes(apiName) && (allModulesPaths.includes(path.normalize(sourceFileName)) ||
339    isArkuiDependence(sourceFileName))) {
340    permissionsArray = projectConfig.requestPermissions;
341    checkConfigArray.push(getJsDocNodeCheckConfigItem([DEPRECATED_TAG_CHECK_NAME], DEPRECATED_TAG_CHECK_WARNING, false,
342      ts.DiagnosticCategory.Warning, '', false));
343    checkConfigArray.push(getJsDocNodeCheckConfigItem([SYSTEM_API_TAG_CHECK_NAME], SYSTEM_API_TAG_CHECK_WARNING, false,
344      ts.DiagnosticCategory.Warning, '', false));
345    checkConfigArray.push(getJsDocNodeCheckConfigItem([SINCE_TAG_NAME],
346      SINCE_TAG_CHECK_ERROER, false, ts.DiagnosticCategory.Warning,
347      VERSION_CHECK_FUNCTION_NAME, false, undefined, checkSinceValue));
348    // TODO: the third param is to be opened
349    if (projectConfig.deviceTypes && projectConfig.deviceTypes.length > 0) {
350      const fileContent: string = fs.readFileSync(fileName, { encoding: 'utf-8' });
351      const needCanIUseCheck: boolean = /canIUse\(.*\)/.test(fileContent);
352      checkConfigArray.push(getJsDocNodeCheckConfigItem([SYSCAP_TAG_CHECK_NAME],
353        SYSCAP_TAG_CHECK_WARNING, needCanIUseCheck, ts.DiagnosticCategory.Warning, CANIUSE_FUNCTION_NAME, false, undefined,
354        checkSyscapAbility, checkSyscapConditionValidCallback));
355    }
356    if (projectConfig.projectRootPath) {
357      const ohosTestDir = ts.sys.resolvePath(path.join(projectConfig.projectRootPath, 'entry', 'src', 'ohosTest'));
358      // TODO:fix error type in the feature
359      if (!ts.sys.resolvePath(fileName).startsWith(ohosTestDir)) {
360        permissionsArray = projectConfig.requestPermissions;
361        checkConfigArray.push(getJsDocNodeCheckConfigItem([TEST_TAG_CHECK_NAME], TEST_TAG_CHECK_ERROR, false,
362          ts.DiagnosticCategory.Warning, '', false));
363      }
364    }
365    checkConfigArray.push(getJsDocNodeCheckConfigItem([PERMISSION_TAG_CHECK_NAME], PERMISSION_TAG_CHECK_ERROR, false,
366      ts.DiagnosticCategory.Warning, '', false, undefined, checkPermissionValue));
367    if (isCardFile(fileName)) {
368      needCheckResult = true;
369      checkConfigArray.push(getJsDocNodeCheckConfigItem([FORM_TAG_CHECK_NAME], FORM_TAG_CHECK_ERROR, false,
370        ts.DiagnosticCategory.Error, '', true));
371    }
372    if (projectConfig.isCrossplatform) {
373      needCheckResult = true;
374      const logType: ts.DiagnosticCategory = projectConfig.ignoreCrossplatformCheck !== true ? ts.DiagnosticCategory.Error :
375        ts.DiagnosticCategory.Warning;
376      checkConfigArray.push(getJsDocNodeCheckConfigItem([CROSSPLATFORM_TAG_CHECK_NAME], CROSSPLATFORM_TAG_CHECK_ERROER,
377        false, logType, '', true));
378    }
379    if (process.env.compileMode === STAGE_COMPILE_MODE) {
380      needCheckResult = true;
381      checkConfigArray.push(getJsDocNodeCheckConfigItem([FA_TAG_CHECK_NAME, FA_TAG_HUMP_CHECK_NAME],
382        FA_TAG_CHECK_ERROR, false, ts.DiagnosticCategory.Error, '', false));
383    } else if (process.env.compileMode !== '') {
384      needCheckResult = true;
385      checkConfigArray.push(getJsDocNodeCheckConfigItem([STAGE_TAG_CHECK_NAME, STAGE_TAG_HUMP_CHECK_NAME],
386        STAGE_TAG_CHECK_ERROR, false,
387        ts.DiagnosticCategory.Error, '', false));
388    }
389    if (projectConfig.bundleType === ATOMICSERVICE_BUNDLE_TYPE &&
390      projectConfig.compileSdkVersion >= ATOMICSERVICE_TAG_CHECK_VERSION) {
391      needCheckResult = true;
392      checkConfigArray.push(getJsDocNodeCheckConfigItem([ATOMICSERVICE_TAG_CHECK_NAME], ATOMICSERVICE_TAG_CHECK_ERROER,
393        false, ts.DiagnosticCategory.Error, '', true));
394    }
395  }
396  result = {
397    nodeNeedCheck: needCheckResult,
398    checkConfig: checkConfigArray
399  };
400  byFileName.set(sourceFileName, result);
401  return result;
402}
403
404const arkuiDependenceMap: Map<string, boolean> = new Map<string, boolean>();
405/**
406 * return a file path is Arkui path
407 *
408 * @param {string} file - file path
409 * @returns {boolean}
410 */
411function isArkuiDependence(file: string): boolean {
412  let exists: boolean | undefined = arkuiDependenceMap.get(file);
413  if (exists !== undefined) {
414    return exists;
415  }
416  const fileDir: string = path.dirname(file);
417  const declarationsPath: string = path.resolve(__dirname, '../../../declarations').replace(/\\/g, '/');
418  const componentPath: string = path.resolve(__dirname, '../../../../../component').replace(/\\/g, '/');
419  exists = fileDir === declarationsPath || fileDir === componentPath;
420  arkuiDependenceMap.set(file, exists);
421  return exists;
422}
423
424/**
425 * check a secondary directory of Arkui is used in the moduleSpecifier of import
426 *
427 * @param {ts.Expression} moduleSpecifier - the moduleSpecifier of import
428 * @param {LogInfo[]} log - log list
429 */
430export function validateModuleSpecifier(moduleSpecifier: ts.Expression, log: LogInfo[]): void {
431  const moduleSpecifierStr: string = moduleSpecifier.getText().replace(/'|"/g, '');
432  const hasSubDirPath: boolean = ohosSystemModuleSubDirPaths.some((filePath: string) => {
433    return filePath === moduleSpecifierStr;
434  });
435  if (hasSubDirPath) {
436    // TODO: change to error
437    const error: LogInfo = {
438      type: LogType.WARN,
439      message: `Cannot find module '${moduleSpecifierStr}' or its corresponding type declarations.`,
440      pos: moduleSpecifier.getStart()
441    };
442    log.push(error);
443  }
444}
445
446interface SystemConfig {
447  deviceTypesMessage: string,
448  deviceTypes: string[],
449  runtimeOS: string,
450  externalApiPaths: string[],
451  syscapIntersectionSet: Set<string>,
452  syscapUnionSet: Set<string>
453}
454
455interface SyscapConfig {
456  SysCaps: string[]
457}
458
459/**
460 * configure syscapInfo to this.share.projectConfig
461 *
462 * @param config this.share.projectConfig
463 */
464export function configureSyscapInfo(config: SystemConfig): void {
465  config.deviceTypesMessage = config.deviceTypes.join(',');
466  const deviceDir: string = path.resolve(__dirname, '../../../../../api/device-define/');
467  const deviceInfoMap: Map<string, string[]> = new Map();
468  const syscaps: Array<string[]> = [];
469  let allSyscaps: string[] = [];
470  config.deviceTypes.forEach((deviceType: string) => {
471    collectOhSyscapInfos(deviceType, deviceDir, deviceInfoMap);
472  });
473  if (config.runtimeOS !== RUNTIME_OS_OH) {
474    collectExternalSyscapInfos(config.externalApiPaths, config.deviceTypes, deviceInfoMap);
475  }
476  deviceInfoMap.forEach((value: string[]) => {
477    syscaps.push(value);
478    allSyscaps = allSyscaps.concat(value);
479  });
480  const intersectNoRepeatTwice = (arrs: Array<string[]>) => {
481    return arrs.reduce(function (prev: string[], cur: string[]) {
482      return Array.from(new Set(cur.filter((item: string) => {
483        return prev.includes(item);
484      })));
485    });
486  };
487  let syscapIntersection: string[] = [];
488  if (config.deviceTypes.length === 1 || syscaps.length === 1) {
489    syscapIntersection = syscaps[0];
490  } else if (syscaps.length > 1) {
491    syscapIntersection = intersectNoRepeatTwice(syscaps);
492  }
493  config.syscapIntersectionSet = new Set(syscapIntersection);
494  config.syscapUnionSet = new Set(allSyscaps);
495}
496
497function collectOhSyscapInfos(deviceType: string, deviceDir: string, deviceInfoMap: Map<string, string[]>) {
498  let syscapFilePath: string = '';
499  if (deviceType === 'phone') {
500    syscapFilePath = path.resolve(deviceDir, 'default.json');
501  } else {
502    syscapFilePath = path.resolve(deviceDir, deviceType + '.json');
503  }
504  if (fs.existsSync(syscapFilePath)) {
505    const content: SyscapConfig = JSON.parse(fs.readFileSync(syscapFilePath, 'utf-8'));
506    if (deviceInfoMap.get(deviceType)) {
507      deviceInfoMap.set(deviceType, deviceInfoMap.get(deviceType).concat(content.SysCaps));
508    } else {
509      deviceInfoMap.set(deviceType, content.SysCaps);
510    }
511  }
512}
513
514function collectExternalSyscapInfos(
515  externalApiPaths: string[],
516  deviceTypes: string[],
517  deviceInfoMap: Map<string, string[]>
518) {
519  const externalDeviceDirs: string[] = [];
520  externalApiPaths.forEach((externalApiPath: string) => {
521    const externalDeviceDir: string = path.resolve(externalApiPath, './api/device-define');
522    if (fs.existsSync(externalDeviceDir)) {
523      externalDeviceDirs.push(externalDeviceDir);
524    }
525  });
526  externalDeviceDirs.forEach((externalDeviceDir: string) => {
527    deviceTypes.forEach((deviceType: string) => {
528      let syscapFilePath: string = '';
529      const files: string[] = fs.readdirSync(externalDeviceDir);
530      files.forEach((fileName: string) => {
531        if (fileName.startsWith(deviceType)) {
532          syscapFilePath = path.resolve(externalDeviceDir, fileName);
533          if (fs.existsSync(syscapFilePath)) {
534            const content: SyscapConfig = JSON.parse(fs.readFileSync(syscapFilePath, 'utf-8'));
535            if (deviceInfoMap.get(deviceType)) {
536              deviceInfoMap.set(deviceType, deviceInfoMap.get(deviceType).concat(content.SysCaps));
537            } else {
538              deviceInfoMap.set(deviceType, content.SysCaps);
539            }
540          }
541        }
542      });
543    });
544  });
545}
546
547/**
548 * Determine the necessity of since check.
549 *
550 * @param jsDocTags
551 * @param config
552 * @returns
553 */
554function checkSinceValue(jsDocTags: readonly ts.JSDocTag[], config: ts.JsDocNodeCheckConfigItem): boolean {
555  if (!jsDocTags[0]?.parent?.parent || !projectConfig.compatibleSdkVersion) {
556    return false;
557  }
558  const currentNode: HasJSDocNode = jsDocTags[0].parent.parent as HasJSDocNode;
559  if (!currentNode.jsDoc) {
560    return false;
561  }
562  const minSince: string = getMinVersion(currentNode.jsDoc);
563  const hasSince: boolean = minSince !== '';
564
565  const compatibleSdkVersion: string = projectConfig.compatibleSdkVersion.toString();
566  if (!isCompliantSince(minSince) || !isCompliantSince(compatibleSdkVersion)) {
567    return false;
568  }
569  if (hasSince && comparePointVersion(compatibleSdkVersion.toString(), minSince) === -1) {
570    config.message = SINCE_TAG_CHECK_ERROER.replace('$SINCE1', minSince).replace('$SINCE2', compatibleSdkVersion);
571    return true;
572  }
573  return false;
574}
575
576/**
577 * Confirm compliance since
578 * Only major version can be passed in, such as "19";
579 * major and minor version can be passed in, such as "19.1"; major minor and patch
580 * patch version can be passed in, such as "19.1.2"
581 * the major version be from 1-999
582 * the minor version be from 0-999
583 * the patch version be from 0-999
584 *
585 * @param {string} since
586 * @return {boolean}
587 */
588function isCompliantSince(since: string): boolean {
589  return /^(?!0\d)[1-9]\d{0,2}(?:\.[1-9]\d{0,2}|\.0){0,2}$\d{0,2}$/.test(since);
590}
591
592/**
593 * Determine the necessity of syscap check.
594 * @param jsDocTags
595 * @param config
596 * @returns
597 */
598export function checkSyscapAbility(jsDocTags: readonly ts.JSDocTag[], config: ts.JsDocNodeCheckConfigItem): boolean {
599  let currentSyscapValue: string = '';
600  for (let i = 0; i < jsDocTags.length; i++) {
601    const jsDocTag: ts.JSDocTag = jsDocTags[i];
602    if (jsDocTag && jsDocTag.tagName.escapedText.toString() === SYSCAP_TAG_CHECK_NAME) {
603      currentSyscapValue = jsDocTag.comment as string;
604      break;
605    }
606  }
607  return projectConfig.syscapIntersectionSet && !projectConfig.syscapIntersectionSet.has(currentSyscapValue);
608}
609
610interface ConfigPermission {
611  requestPermissions: Array<{ name: string }>;
612  definePermissions: Array<{ name: string }>;
613}
614
615interface PermissionsConfig {
616  permission: ConfigPermission,
617  requestPermissions: string[],
618  definePermissions: string[],
619}
620/**
621 * configure permissionInfo to this.share.projectConfig
622 *
623 * @param config this.share.projectConfig
624 */
625export function configurePermission(config: PermissionsConfig): void {
626  const permission: ConfigPermission = config.permission;
627  config.requestPermissions = [];
628  config.definePermissions = [];
629  if (permission.requestPermissions) {
630    config.requestPermissions = getNameFromArray(permission.requestPermissions);
631  }
632  if (permission.definePermissions) {
633    config.definePermissions = getNameFromArray(permission.definePermissions);
634  }
635}
636
637function getNameFromArray(array: Array<{ name: string }>): string[] {
638  return array.map((item: { name: string }) => {
639    return String(item.name);
640  });
641}
642
643/**
644 *  Determine the necessity of permission check
645 *
646 * @param {ts.JSDocTag[]} jsDocTags
647 * @param {ts.JsDocNodeCheckConfigItem} config
648 * @returns {boolean}
649 */
650export function checkPermissionValue(jsDocTags: readonly ts.JSDocTag[], config: ts.JsDocNodeCheckConfigItem): boolean {
651  const jsDocTag: ts.JSDocTag = jsDocTags.find((item: ts.JSDocTag) => {
652    return item.tagName.getText() === PERMISSION_TAG_CHECK_NAME;
653  });
654  if (!jsDocTag) {
655    return false;
656  }
657  const comment: string = typeof jsDocTag.comment === 'string' ?
658    jsDocTag.comment :
659    ts.getTextOfJSDocComment(jsDocTag.comment);
660  config.message = PERMISSION_TAG_CHECK_ERROR.replace('$DT', comment);
661  return comment !== '' && !JsDocCheckService.validPermission(comment, permissionsArray);
662}
663
664/**
665 * custom condition check
666 * @param { ts.FileCheckModuleInfo } jsDocFileCheckedInfo
667 * @param { ts.JsDocTagInfo[] } jsDocTagInfos
668 * @param { ?ts.JSDoc[] } jsDocs
669 * @returns
670 */
671export function getJsDocNodeConditionCheckResult(jsDocFileCheckedInfo: ts.FileCheckModuleInfo, jsDocTagInfos: ts.JsDocTagInfo[], jsDocs?: ts.JSDoc[]):
672  ts.ConditionCheckResult {
673  let result: ts.ConditionCheckResult = {
674    valid: true
675  };
676  if (jsDocFileCheckedInfo.tagName.includes(SYSCAP_TAG_CHECK_NAME)) {
677    result = checkSyscapCondition(jsDocTagInfos);
678  } else if (jsDocFileCheckedInfo.tagName.includes(SINCE_TAG_NAME) && jsDocs) {
679    result = checkSinceCondition(jsDocs);
680  }
681  return result;
682}
683
684/**
685 * syscap condition check
686 * @param { ts.JSDoc[] } jsDocs
687 * @returns { ts.ConditionCheckResult }
688 */
689function checkSyscapCondition(jsDocs: ts.JsDocTagInfo[]): ts.ConditionCheckResult {
690  const result: ts.ConditionCheckResult = {
691    valid: true
692  };
693  let currentSyscapValue: string = '';
694  for (let i = 0; i < jsDocs.length; i++) {
695    const jsDocTag: ts.JsDocTagInfo = jsDocs[i];
696    if (jsDocTag.name === SYSCAP_TAG_CHECK_NAME) {
697      currentSyscapValue = jsDocTag.text as string;
698      break;
699    }
700  }
701  if (!projectConfig.syscapIntersectionSet || !projectConfig.syscapUnionSet) {
702    return result;
703  }
704  if (!projectConfig.syscapIntersectionSet.has(currentSyscapValue) && projectConfig.syscapUnionSet.has(currentSyscapValue)) {
705    result.valid = false;
706    result.type = ts.DiagnosticCategory.Warning;
707    result.message = SYSCAP_TAG_CONDITION_CHECK_WARNING;
708  } else if (!projectConfig.syscapUnionSet.has(currentSyscapValue)) {
709    result.valid = false;
710    // TODO: fix to error in the feature
711    result.type = ts.DiagnosticCategory.Warning;
712    result.message = SYSCAP_TAG_CHECK_WARNING.replace('$DT', projectConfig.deviceTypesMessage);
713  }
714  return result;
715}
716
717/**
718 * version condition check
719 * @param { ts.JSDoc[] } jsDocs
720 * @returns { ts.ConditionCheckResult }
721 */
722function checkSinceCondition(jsDocs: ts.JSDoc[]): ts.ConditionCheckResult {
723  const result: ts.ConditionCheckResult = {
724    valid: true
725  };
726  if (!jsDocs || !jsDocs[0] || !projectConfig.compatibleSdkVersion) {
727    return result;
728  }
729  const minVersion: string = getMinVersion(jsDocs);
730  const hasSince: boolean = minVersion !== '';
731
732  const compatibleSdkVersion: string = projectConfig.compatibleSdkVersion.toString();
733
734  if (hasSince && comparePointVersion(compatibleSdkVersion, minVersion) === -1) {
735    result.valid = false;
736    result.type = ts.DiagnosticCategory.Warning;
737    result.message = SINCE_TAG_CHECK_ERROER.replace('$SINCE1', minVersion).replace('$SINCE2', compatibleSdkVersion);
738  }
739  return result;
740}
741
742/**
743 * version condition check, print error message
744 * @param { ts.CallExpression } node
745 * @param { string } specifyFuncName
746 * @param { string } targetVersion
747 * @param { ?ts.JSDoc[] } jsDocs
748 * @returns { boolean }
749 */
750function checkVersionConditionValidCallback(node: ts.CallExpression, specifyFuncName: string, targetVersion: string, jsDocs?: ts.JSDoc[]): boolean {
751  if (ts.isIdentifier(node.expression) && node.arguments.length === 1 && node.expression.escapedText.toString() === specifyFuncName) {
752    const expression = node.arguments[0];
753    if (ts.isStringLiteral(expression) && jsDocs && comparePointVersion(expression.text.toString(), getMinVersion(jsDocs)) !== -1) {
754      return true;
755    }
756  }
757  return false;
758}
759
760/**
761 * syscap condition check, print error message
762 * @param { ts.CallExpression } node
763 * @param { string } specifyFuncName
764 * @param { string } tagValue
765 * @param { ?ts.JSDoc[] } jsDocs
766 * @returns { boolean }
767 */
768function checkSyscapConditionValidCallback(node: ts.CallExpression, specifyFuncName: string, tagValue: string, jsDocs?: ts.JSDoc[]): boolean {
769  if (ts.isIdentifier(node.expression) && node.arguments.length === 1 && node.expression.escapedText.toString() === specifyFuncName) {
770    const expression = node.arguments[0];
771    if (ts.isStringLiteral(expression) && tagValue === expression.text) {
772      return true;
773    } else if (ts.isIdentifier(expression)) {
774      const typeChecker: ts.TypeChecker = globalProgram.program.getTypeChecker();
775      const arguSymbol: ts.Symbol | undefined = typeChecker.getSymbolAtLocation(expression);
776      return arguSymbol && arguSymbol.valueDeclaration && ts.isVariableDeclaration(arguSymbol.valueDeclaration) &&
777        arguSymbol.valueDeclaration.initializer && ts.isStringLiteral(arguSymbol.valueDeclaration.initializer) &&
778        arguSymbol.valueDeclaration.initializer.text === tagValue;
779    }
780  }
781  return false;
782}
783
784/**
785 * get minversion
786 * @param { ts.JSDoc[] } jsDocs
787 * @returns string
788 */
789function getMinVersion(jsDocs: ts.JSDoc[]): string {
790  if (!jsDocs || !jsDocs[0]) {
791    return '';
792  }
793  let minVersion: string = '';
794  jsDocs.some((doc: ts.JSDoc) => {
795    return doc.tags?.some((tag: ts.JSDocTag) => {
796      if (tag.tagName.escapedText.toString() === SINCE_TAG_NAME) {
797        minVersion = tag.comment.toString();
798        return true;
799      }
800      return false;
801    });
802  });
803  return minVersion;
804}
805
806/**
807 * compare point version
808 * @param { string } firstVersion
809 * @param { string } secondVersion
810 * @returns { number }
811 */
812function comparePointVersion(firstVersion: string, secondVersion: string): number {
813  const firstPointVersion = firstVersion.split('.');
814  const secondPointVersion = secondVersion.split('.');
815  for (let i = 0; i < 3; i++) {
816    const part1 = parseInt(firstPointVersion[i] || '0', 10);
817    const part2 = parseInt(secondPointVersion[i] || '0', 10);
818
819    if (part1 < part2) {
820      return -1;
821    }
822    if (part1 > part2) {
823      return 1;
824    }
825  }
826  return 0;
827}