• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 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 { BaseChecker, BaseMetaData } from '../BaseChecker';
17import { Rule } from '../../model/Rule';
18import { Defects, IssueReport } from '../../model/Defects';
19import { FileMatcher, MatcherCallback, MatcherTypes } from '../../matcher/Matchers';
20import {
21    AbstractInvokeExpr,
22    ArkAssignStmt,
23    ArkClass,
24    ArkFile,
25    ArkMethod,
26    ArkNamespace,
27    ArkNewExpr,
28    ClassType,
29    FunctionType,
30    ImportInfo,
31    Local,
32    LOG_MODULE_TYPE,
33    Logger,
34    Scene,
35    Stmt,
36    Type,
37} from 'arkanalyzer';
38import { ExportType } from 'arkanalyzer/lib/core/model/ArkExport';
39import { WarnInfo } from '../../utils/common/Utils';
40import { Language } from 'arkanalyzer/lib/core/model/ArkFile';
41import { getLanguageStr } from './Utils';
42
43const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, 'InteropBoxedTypeCheck');
44const gMetaData: BaseMetaData = {
45    severity: 1,
46    ruleDocPath: '',
47    description: '',
48};
49
50const ruleId: string = '@migration/interop-boxed-type-check';
51const s2dRuleId: string = 'arkts-interop-s2d-boxed-type';
52const d2sRuleId: string = 'arkts-interop-d2s-boxed-type';
53const ts2sRuleId: string = 'arkts-interop-ts2s-boxed-type';
54const js2RuleId: string = 'arkts-interop-js2s-boxed-type';
55
56const BOXED_SET: Set<string> = new Set<string>(['String', 'Boolean', 'Number']);
57
58type CheckedObj = {
59    namespaces: Map<string, boolean | null>;
60    classes: Map<string, boolean | null>;
61    methods: Map<string, boolean | null>;
62};
63
64export class InteropBoxedTypeCheck implements BaseChecker {
65    readonly metaData: BaseMetaData = gMetaData;
66    public rule: Rule;
67    public defects: Defects[] = [];
68    public issues: IssueReport[] = [];
69
70    private fileMatcher: FileMatcher = {
71        matcherType: MatcherTypes.FILE,
72    };
73
74    public registerMatchers(): MatcherCallback[] {
75        const fileMatcher: MatcherCallback = {
76            matcher: this.fileMatcher,
77            callback: this.check,
78        };
79        return [fileMatcher];
80    }
81
82    public check = (arkFile: ArkFile): void => {
83        let hasChecked: CheckedObj = {
84            namespaces: new Map<string, boolean | null>(),
85            classes: new Map<string, boolean | null>(),
86            methods: new Map<string, boolean | null>(),
87        };
88        const scene = arkFile.getScene();
89        // Import对象对应的Export信息的推导在类型推导过程中是懒加载机制,调用getLazyExportInfo接口会自动进行推导
90        arkFile.getImportInfos().forEach(importInfo => {
91            const exportInfo = importInfo.getLazyExportInfo();
92            // TODO: import * from xxx是如何表示的?
93            if (exportInfo === null) {
94                // 导入内置库时为null
95                return;
96            }
97            const arkExport = exportInfo.getArkExport();
98            if (arkExport === null || arkExport === undefined) {
99                // 按正常流程,上面的exportInfo不为null时,这里一定会将实际找到的export对象set为arkExport,所以这里应该走不到
100                // import三方包时为null,未推导为undefined,推导后无结果为null
101                return;
102            }
103
104            const exportType = arkExport.getExportType();
105            // 如果import的是sdk,exportType可能是namespace等,但是找不到body体等详细赋值语句等内容,所以不影响如下的判断
106            switch (exportType) {
107                case ExportType.NAME_SPACE:
108                    this.findBoxedTypeInNamespace(importInfo, arkExport as ArkNamespace, scene, hasChecked);
109                    return;
110                case ExportType.CLASS:
111                    this.findBoxedTypeInClass(importInfo, arkExport as ArkClass, scene, hasChecked);
112                    return;
113                case ExportType.METHOD:
114                    this.findBoxedTypeWithMethodReturn(importInfo, arkExport as ArkMethod, scene, hasChecked);
115                    return;
116                case ExportType.LOCAL:
117                    this.findBoxedTypeWithLocal(importInfo, arkExport as Local, scene, hasChecked);
118                    return;
119                default:
120                    return;
121            }
122        });
123    };
124
125    private findBoxedTypeInNamespace(
126        importInfo: ImportInfo,
127        arkNamespace: ArkNamespace,
128        scene: Scene,
129        hasChecked: CheckedObj
130    ): boolean | null {
131        // 判断namespace是否已查找过,避免陷入死循环
132        const existing = hasChecked.namespaces.get(arkNamespace.getSignature().toString());
133        if (existing !== undefined) {
134            return existing;
135        }
136        hasChecked.namespaces.set(arkNamespace.getSignature().toString(), null);
137        const exports = arkNamespace.getExportInfos();
138        let found: boolean | null = null;
139        for (const exportInfo of exports) {
140            const arkExport = exportInfo.getArkExport();
141            if (arkExport === undefined) {
142                continue;
143            }
144
145            if (arkExport === null) {
146                // ArkAnalyzer此处有一个问题,无法区分export local是来自arkfile还是arknamespace,导致类型推导推出来是null
147                continue;
148            }
149            if (arkExport instanceof Local) {
150                found = this.findBoxedTypeWithLocal(importInfo, arkExport, scene, hasChecked);
151            } else if (arkExport instanceof ArkMethod) {
152                found = this.findBoxedTypeWithMethodReturn(importInfo, arkExport, scene, hasChecked);
153            } else if (arkExport instanceof ArkClass) {
154                found = this.findBoxedTypeInClass(importInfo, arkExport, scene, hasChecked);
155            } else if (arkExport instanceof ArkNamespace) {
156                found = this.findBoxedTypeInNamespace(importInfo, arkExport, scene, hasChecked);
157            }
158            if (found) {
159                hasChecked.namespaces.set(arkNamespace.getSignature().toString(), true);
160                return true;
161            }
162        }
163        hasChecked.namespaces.set(arkNamespace.getSignature().toString(), false);
164        return false;
165    }
166
167    private isClassHasBoxedType(arkClass: ArkClass, scene: Scene, hasChecked: CheckedObj): boolean | null {
168        // step0: 判断class是否已查找过,避免陷入死循环
169        const existing = hasChecked.classes.get(arkClass.getSignature().toString());
170        if (existing !== undefined) {
171            return existing;
172        }
173        hasChecked.classes.set(arkClass.getSignature().toString(), null);
174        // step1: 查找class中的所有field,包含static和非static,判断initialized stmts中是否会用boxed类型对象给field赋值
175        const allFields = arkClass.getFields();
176        for (const field of allFields) {
177            // 此处不检查field signature中的Type,因为type直接写String时也表示成Class Type,无法区分是否为new String()生成的
178            const initializer = field.getInitializer();
179            if (initializer.length < 1) {
180                continue;
181            }
182            const lastStmt = initializer[initializer.length - 1];
183            if (!(lastStmt instanceof ArkAssignStmt)) {
184                continue;
185            }
186            if (this.isValueAssignedByBoxed(lastStmt, initializer.slice(0, -1).reverse(), scene, hasChecked)) {
187                // 这里没有顺着field的定义语句中使用到的import对象去寻找原始的Boxed类型定义所在的文件的Language,而是直接使用field所在的语言
188                // 应该也是ok的,因为上述import chain如何不合法,也会有告警在其import的地方给出
189                hasChecked.classes.set(arkClass.getSignature().toString(), true);
190                return true;
191            }
192        }
193
194        // step2: 查找class中的所有非generated method,判断所有的return操作符类型是否为boxed
195        const methods = arkClass.getMethods();
196        for (const method of methods) {
197            const found = this.isMethodReturnHasBoxedType(method, scene, hasChecked);
198            if (found) {
199                hasChecked.classes.set(arkClass.getSignature().toString(), true);
200                return true;
201            }
202        }
203        hasChecked.classes.set(arkClass.getSignature().toString(), false);
204        return false;
205    }
206
207    private isMethodReturnHasBoxedType(arkMethod: ArkMethod, scene: Scene, hasChecked: CheckedObj): boolean | null {
208        // 判断method是否已查找过,避免陷入死循环
209        const existing = hasChecked.methods.get(arkMethod.getSignature().toString());
210        if (existing !== undefined) {
211            return existing;
212        }
213        hasChecked.methods.set(arkMethod.getSignature().toString(), null);
214        const returnOps = arkMethod.getReturnValues();
215        for (const op of returnOps) {
216            if (this.isBoxedType(op.getType())) {
217                hasChecked.methods.set(arkMethod.getSignature().toString(), true);
218                return true;
219            }
220            if (op instanceof Local && this.isLocalHasBoxedType(op, scene, hasChecked)) {
221                hasChecked.methods.set(arkMethod.getSignature().toString(), true);
222                return true;
223            }
224        }
225        hasChecked.methods.set(arkMethod.getSignature().toString(), false);
226        return false;
227    }
228
229    // 此处不检查local的Type,因为type直接写String时也表示成Class Type,无法区分是否为new String()生成的
230    private isLocalHasBoxedType(local: Local, scene: Scene, hasChecked: CheckedObj): boolean {
231        const method = local.getDeclaringStmt()?.getCfg().getDeclaringMethod();
232        if (method === undefined) {
233            return false;
234        }
235        const stmts = method.getCfg()?.getStmts().reverse();
236        if (stmts === undefined || stmts.length < 1) {
237            return false;
238        }
239
240        const declaringStmt = local.getDeclaringStmt();
241        if (
242            declaringStmt !== null &&
243            declaringStmt instanceof ArkAssignStmt &&
244            this.isValueAssignedByBoxed(declaringStmt, stmts, scene, hasChecked)
245        ) {
246            return true;
247        }
248        for (const stmt of local.getUsedStmts()) {
249            if (stmt instanceof ArkAssignStmt) {
250                const leftOp = stmt.getLeftOp();
251                if (
252                    leftOp instanceof Local &&
253                    leftOp.toString() === local.toString() &&
254                    this.isValueAssignedByBoxed(stmt, stmts, scene, hasChecked)
255                ) {
256                    return true;
257                }
258            }
259        }
260        return false;
261    }
262
263    private findBoxedTypeInClass(
264        importInfo: ImportInfo,
265        arkClass: ArkClass,
266        scene: Scene,
267        hasChecked: CheckedObj
268    ): boolean {
269        const importOpPosition = importInfo.getOriginTsPosition();
270        const warnInfo: WarnInfo = {
271            line: importOpPosition.getLineNo(),
272            startCol: importOpPosition.getColNo(),
273            endCol: importOpPosition.getColNo(),
274            filePath: importInfo.getDeclaringArkFile().getFilePath(),
275        };
276        const currLanguage = importInfo.getLanguage();
277        const result = this.isClassHasBoxedType(arkClass, scene, hasChecked);
278        if (result) {
279            this.addIssueReport(warnInfo, currLanguage, arkClass.getLanguage());
280            return true;
281        }
282        return false;
283    }
284
285    private findBoxedTypeWithMethodReturn(
286        importInfo: ImportInfo,
287        arkMethod: ArkMethod,
288        scene: Scene,
289        hasChecked: CheckedObj
290    ): boolean {
291        const importOpPostion = importInfo.getOriginTsPosition();
292        const warnInfo: WarnInfo = {
293            line: importOpPostion.getLineNo(),
294            startCol: importOpPostion.getColNo(),
295            endCol: importOpPostion.getColNo(),
296            filePath: importInfo.getDeclaringArkFile().getFilePath(),
297        };
298        const currLanguage = importInfo.getLanguage();
299
300        // 此处不检查method signature中的return Type,因为return type直接写String时也表示成Class Type,无法区分是否为new String()生成的
301        if (this.isMethodReturnHasBoxedType(arkMethod, scene, hasChecked)) {
302            this.addIssueReport(warnInfo, currLanguage, arkMethod.getLanguage());
303            return true;
304        }
305        return false;
306    }
307
308    private findBoxedTypeWithLocal(
309        importInfo: ImportInfo,
310        local: Local,
311        scene: Scene,
312        hasChecked: CheckedObj
313    ): boolean {
314        const importOpPosition = importInfo.getOriginTsPosition();
315        const warnInfo: WarnInfo = {
316            line: importOpPosition.getLineNo(),
317            startCol: importOpPosition.getColNo(),
318            endCol: importOpPosition.getColNo(),
319            filePath: importInfo.getDeclaringArkFile().getFilePath(),
320        };
321        const currLanguage = importInfo.getLanguage();
322        const method = local.getDeclaringStmt()?.getCfg().getDeclaringMethod();
323        if (method === undefined) {
324            return false;
325        }
326        if (this.isLocalHasBoxedType(local, scene, hasChecked)) {
327            this.addIssueReport(warnInfo, currLanguage, method.getLanguage());
328            return true;
329        }
330        return false;
331    }
332
333    private isBoxedType(checkType: Type): boolean {
334        // ArkAnalyzer表示new String()形式的类型为ClassType,Class Name为String、Boolean、Number
335        // TODO: 此处底座有一个bug,表示String()时推导为Unknown Type,正确的应该为string,但是不影响本规则的判断
336        return checkType instanceof ClassType && BOXED_SET.has(checkType.getClassSignature().getClassName());
337    }
338
339    private addIssueReport(warnInfo: WarnInfo, currLanguage: Language, targetLanguage: Language): void {
340        const interopRule = this.getInteropRule(currLanguage, targetLanguage);
341        if (interopRule === null) {
342            return;
343        }
344        const severity = this.metaData.severity;
345        const currLanStr = getLanguageStr(currLanguage);
346        const targetLanStr = getLanguageStr(targetLanguage);
347        const problem = 'Interop';
348        const describe = `Could not import object with boxed type from ${targetLanStr} to ${currLanStr} (${interopRule})`;
349        let defects = new Defects(
350            warnInfo.line,
351            warnInfo.startCol,
352            warnInfo.endCol,
353            problem,
354            describe,
355            severity,
356            ruleId,
357            warnInfo.filePath,
358            this.metaData.ruleDocPath,
359            true,
360            false,
361            false
362        );
363        this.issues.push(new IssueReport(defects, undefined));
364    }
365
366    private getInteropRule(currLanguage: Language, targetLanguage: Language): string | null {
367        if (currLanguage === Language.ARKTS1_1) {
368            if (targetLanguage === Language.ARKTS1_2) {
369                return s2dRuleId;
370            }
371        } else if (currLanguage === Language.ARKTS1_2) {
372            if (targetLanguage === Language.TYPESCRIPT) {
373                return ts2sRuleId;
374            }
375            if (targetLanguage === Language.ARKTS1_1) {
376                return d2sRuleId;
377            }
378            if (targetLanguage === Language.JAVASCRIPT) {
379                return js2RuleId;
380            }
381        }
382        return null;
383    }
384
385    // lastStmt为当前需要查找的对象的赋值语句,左值为查找对象,右值为往前继续查找的赋值起点
386    // reverseStmtChain为以待查找对象为起点,所有一系列赋值语句的倒序排列
387    private isValueAssignedByBoxed(
388        lastStmt: ArkAssignStmt,
389        previousReverseChain: Stmt[],
390        scene: Scene,
391        hasChecked: CheckedObj
392    ): boolean {
393        let locals: Set<Local> = new Set();
394        const targetLocal = lastStmt.getRightOp();
395        const targetLocalType = targetLocal.getType();
396        if (this.isBoxedType(targetLocalType)) {
397            return true;
398        }
399        if (targetLocalType instanceof ClassType) {
400            const arkClass = scene.getClass(targetLocalType.getClassSignature());
401            if (arkClass !== null && this.isClassHasBoxedType(arkClass, scene, hasChecked)) {
402                return true;
403            }
404        }
405        if (targetLocalType instanceof FunctionType) {
406            const arkMethod = scene.getMethod(targetLocalType.getMethodSignature());
407            if (arkMethod !== null && this.isMethodReturnHasBoxedType(arkMethod, scene, hasChecked)) {
408                return true;
409            }
410        }
411
412        if (!(targetLocal instanceof Local)) {
413            return false;
414        }
415        locals.add(targetLocal);
416
417        const rightOp = lastStmt.getRightOp();
418        if (!(rightOp instanceof Local)) {
419            return false;
420        }
421        locals.add(rightOp);
422        for (const stmt of previousReverseChain) {
423            if (!(stmt instanceof ArkAssignStmt)) {
424                continue;
425            }
426            const leftOp = stmt.getLeftOp();
427            const rightOp = stmt.getRightOp();
428            if (!(leftOp instanceof Local) || !locals.has(leftOp)) {
429                continue;
430            }
431            if (rightOp instanceof Local) {
432                locals.add(rightOp);
433                continue;
434            }
435            if (rightOp instanceof ArkNewExpr) {
436                if (this.isBoxedType(rightOp.getClassType())) {
437                    return true;
438                }
439                continue;
440            }
441            if (rightOp instanceof AbstractInvokeExpr) {
442                if (this.isBoxedType(rightOp.getType())) {
443                    return true;
444                }
445                continue;
446            }
447        }
448        return false;
449    }
450}
451