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