1/* 2 * Copyright (c) 2024-2025 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 { ArkFile } from '../model/ArkFile'; 17import { ArkExport, ExportInfo } from '../model/ArkExport'; 18import { COMPONENT_ATTRIBUTE } from './EtsConst'; 19import { GLOBAL_THIS_NAME, THIS_NAME } from './TSConst'; 20import { TEMP_LOCAL_PREFIX } from './Const'; 21import { ArkClass, ClassCategory } from '../model/ArkClass'; 22import { LocalSignature } from '../model/ArkSignature'; 23import { Local } from '../base/Local'; 24import { ArkMethod } from '../model/ArkMethod'; 25import path from 'path'; 26import { ClassType } from '../base/Type'; 27import { AbstractFieldRef } from '../base/Ref'; 28import { ArkNamespace } from '../model/ArkNamespace'; 29import Logger, { LOG_MODULE_TYPE } from '../../utils/logger'; 30import { Sdk } from '../../Config'; 31import ts from 'ohos-typescript'; 32import fs from 'fs'; 33 34const logger = Logger.getLogger(LOG_MODULE_TYPE.ARKANALYZER, 'SdkUtils'); 35 36export class SdkUtils { 37 private static esVersion: string = 'ES2017'; 38 private static esVersionMap: Map<string, string> = new Map<string, string>([ 39 ['ES2017', 'lib.es2020.d.ts'], 40 ['ES2021', 'lib.es2021.d.ts'] 41 ]); 42 43 private static sdkImportMap: Map<string, ArkFile> = new Map<string, ArkFile>(); 44 public static BUILT_IN_NAME = 'built-in'; 45 private static BUILT_IN_PATH = 'node_modules/ohos-typescript/lib'; 46 public static BUILT_IN_SDK: Sdk = { 47 moduleName: '', 48 name: `${SdkUtils.BUILT_IN_NAME}`, 49 path: '' 50 }; 51 52 public static setEsVersion(buildProfile: any): void { 53 const accessChain = 'buildOption.arkOptions.tscConfig.targetESVersion'; 54 const version = accessChain.split('.').reduce((acc, key) => acc?.[key], buildProfile); 55 if (version && this.esVersionMap.has(version)) { 56 this.esVersion = version; 57 } 58 } 59 60 public static fetchBuiltInFiles(): string[] { 61 let builtInPath = this.BUILT_IN_PATH; 62 try { 63 // If arkanalyzer is used as dependency by other project, the base directory should be the module path. 64 const moduleRoot = path.dirname(path.dirname(require.resolve('ohos-typescript'))); 65 builtInPath = path.join(moduleRoot, 'lib'); 66 logger.debug(`arkanalyzer is used as dependency, so using builtin sdk file in ${builtInPath}.`); 67 } catch { 68 logger.debug(`use builtin sdk file in ${builtInPath}.`); 69 } 70 let filePath = path.resolve(builtInPath, this.esVersionMap.get(this.esVersion) ?? ''); 71 if (!fs.existsSync(filePath)) { 72 logger.error(`built in directory ${filePath} is not exist, please check!`); 73 // This is the temporarily solution for linter, will update with using config to set builtin sdk path 74 builtInPath = 'dist'; 75 filePath = path.resolve(builtInPath, this.esVersionMap.get(this.esVersion) ?? ''); 76 logger.debug(`use new builtin sdk file in ${builtInPath}.`); 77 if (!fs.existsSync(filePath)) { 78 logger.error(`new built in directory ${filePath} is not exist, please check!`); 79 return []; 80 } 81 } 82 this.BUILT_IN_SDK.path = path.resolve(builtInPath); 83 const result = new Set<string>(); 84 this.dfsFiles(filePath, result); 85 return Array.from(result); 86 } 87 88 private static dfsFiles(filePath: string, files: Set<string>): void { 89 const sourceFile = ts.createSourceFile(filePath, fs.readFileSync(filePath, 'utf8'), ts.ScriptTarget.Latest); 90 const references = sourceFile.libReferenceDirectives; 91 references.forEach(ref => { 92 this.dfsFiles(path.join(path.dirname(filePath), `lib.${ref.fileName}.d.ts`), files); 93 }); 94 files.add(filePath); 95 } 96 97 /* 98 * Set static field to be null, then all related objects could be freed by GC. 99 * Class SdkUtils is only internally used by ArkAnalyzer type inference, the dispose method should be called at the end of type inference. 100 */ 101 public static dispose(): void { 102 this.sdkImportMap.clear(); 103 } 104 105 public static buildSdkImportMap(file: ArkFile): void { 106 const fileName = path.basename(file.getName()); 107 if (fileName.startsWith('@')) { 108 this.sdkImportMap.set(fileName.replace(/\.d\.e?ts$/, ''), file); 109 } 110 } 111 112 public static getImportSdkFile(from: string): ArkFile | undefined { 113 return this.sdkImportMap.get(from); 114 } 115 116 public static loadGlobalAPI(file: ArkFile, globalMap: Map<string, ArkExport>): void { 117 const isGlobalPath = file 118 .getScene() 119 .getOptions() 120 .sdkGlobalFolders?.find(x => file.getFilePath().includes(path.sep + x + path.sep)); 121 if (!isGlobalPath) { 122 return; 123 } 124 file.getClasses().forEach(cls => { 125 if (!cls.isAnonymousClass() && !cls.isDefaultArkClass()) { 126 this.loadAPI(cls, globalMap); 127 } 128 if (cls.isDefaultArkClass()) { 129 cls.getMethods() 130 .filter(mtd => !mtd.isDefaultArkMethod() && !mtd.isAnonymousMethod()) 131 .forEach(mtd => this.loadAPI(mtd, globalMap)); 132 } 133 }); 134 file.getDefaultClass().getDefaultArkMethod() 135 ?.getBody() 136 ?.getAliasTypeMap() 137 ?.forEach(a => this.loadAPI(a[0], globalMap, true)); 138 file.getNamespaces().forEach(ns => this.loadAPI(ns, globalMap)); 139 } 140 141 public static mergeGlobalAPI(file: ArkFile, globalMap: Map<string, ArkExport>): void { 142 const isGlobalPath = file 143 .getScene() 144 .getOptions() 145 .sdkGlobalFolders?.find(x => file.getFilePath().includes(path.sep + x + path.sep)); 146 if (!isGlobalPath) { 147 return; 148 } 149 file.getClasses().forEach(cls => { 150 if (!cls.isAnonymousClass() && !cls.isDefaultArkClass()) { 151 this.loadClass(globalMap, cls); 152 } 153 }); 154 } 155 156 public static loadAPI(api: ArkExport, globalMap: Map<string, ArkExport>, override: boolean = false): void { 157 const old = globalMap.get(api.getName()); 158 if (!old) { 159 globalMap.set(api.getName(), api); 160 } else if (override) { 161 logger.trace(`${old.getSignature()} is override`); 162 globalMap.set(api.getName(), api); 163 } else { 164 logger.trace(`duplicated api: ${api.getSignature()}`); 165 } 166 } 167 168 public static postInferredSdk(file: ArkFile, globalMap: Map<string, ArkExport>): void { 169 const isGlobalPath = file 170 .getScene() 171 .getOptions() 172 .sdkGlobalFolders?.find(x => file.getFilePath().includes(path.sep + x + path.sep)); 173 if (!isGlobalPath) { 174 return; 175 } 176 const defaultArkMethod = file.getDefaultClass().getDefaultArkMethod(); 177 defaultArkMethod 178 ?.getBody() 179 ?.getLocals() 180 .forEach(local => { 181 const name = local.getName(); 182 if (name !== THIS_NAME && !name.startsWith(TEMP_LOCAL_PREFIX)) { 183 this.loadGlobalLocal(local, defaultArkMethod, globalMap); 184 } 185 }); 186 } 187 188 private static loadClass(globalMap: Map<string, ArkExport>, cls: ArkClass): void { 189 const old = globalMap.get(cls.getName()); 190 if (cls === old) { 191 return; 192 } else if (old instanceof ArkClass && old.getDeclaringArkFile().getProjectName() === cls.getDeclaringArkFile().getProjectName()) { 193 if (old.getCategory() === ClassCategory.CLASS || old.getCategory() === ClassCategory.INTERFACE) { 194 this.copyMembers(cls, old); 195 } else { 196 this.copyMembers(old, cls); 197 globalMap.delete(cls.getName()); 198 this.loadAPI(cls, globalMap, true); 199 } 200 } else { 201 this.loadAPI(cls, globalMap, true); 202 } 203 } 204 205 private static loadGlobalLocal(local: Local, defaultArkMethod: ArkMethod, globalMap: Map<string, ArkExport>): void { 206 const name = local.getName(); 207 local.setSignature(new LocalSignature(name, defaultArkMethod.getSignature())); 208 const scene = defaultArkMethod.getDeclaringArkFile().getScene(); 209 if (scene.getOptions().isScanAbc) { 210 const instance = globalMap.get(name + 'Interface'); 211 const attr = globalMap.get(name + COMPONENT_ATTRIBUTE); 212 if (attr instanceof ArkClass && instance instanceof ArkClass) { 213 this.copyMembers(instance, attr); 214 globalMap.set(name, attr); 215 return; 216 } 217 } 218 const old = globalMap.get(name); 219 if (old instanceof ArkClass && local.getType() instanceof ClassType) { 220 const localConstructor = globalMap.get((local.getType() as ClassType).getClassSignature().getClassName()); 221 if (localConstructor instanceof ArkClass) { 222 this.copyMembers(localConstructor, old); 223 } else { 224 this.loadAPI(local, globalMap, true); 225 } 226 } else { 227 this.loadAPI(local, globalMap, true); 228 } 229 } 230 231 private static copyMembers(from: ArkClass, to: ArkClass): void { 232 from.getMethods().forEach(method => { 233 const dist = method.isStatic() ? to.getStaticMethodWithName(method.getName()) : to.getMethodWithName(method.getName()); 234 const distSignatures = dist?.getDeclareSignatures(); 235 if (distSignatures) { 236 method.getDeclareSignatures()?.forEach(x => distSignatures.push(x)); 237 } else { 238 to.addMethod(method); 239 } 240 }); 241 from.getFields().forEach(field => { 242 const dist = field.isStatic() ? to.getStaticFieldWithName(field.getName()) : to.getFieldWithName(field.getName()); 243 if (!dist) { 244 to.addField(field); 245 } 246 }); 247 } 248 249 public static computeGlobalThis(leftOp: AbstractFieldRef, arkMethod: ArkMethod): void { 250 const globalThis = arkMethod.getDeclaringArkFile().getScene().getSdkGlobal(GLOBAL_THIS_NAME); 251 if (globalThis instanceof ArkNamespace) { 252 const exportInfo = new ExportInfo.Builder() 253 .exportClauseName(leftOp.getFieldName()) 254 .arkExport(new Local(leftOp.getFieldName(), leftOp.getType())) 255 .build(); 256 globalThis.addExportInfo(exportInfo); 257 } 258 } 259} 260