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}