• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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