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 { 17 ArkAssignStmt, 18 ArkIfStmt, 19 ArkInstanceFieldRef, 20 ArkInstanceInvokeExpr, 21 ArkMethod, 22 ClassType, 23 FunctionType, 24 Local, 25 Stmt, 26 Value, 27 Scene, 28 ArkNewArrayExpr, 29 ArkNewExpr, 30 ArkClass, 31 ClassSignature, 32 ArkReturnStmt, 33} from 'arkanalyzer'; 34import Logger, { LOG_MODULE_TYPE } from 'arkanalyzer/lib/utils/logger'; 35import { BaseChecker, BaseMetaData } from '../BaseChecker'; 36import { Rule, Defects, MatcherTypes, MethodMatcher, MatcherCallback } from '../../Index'; 37import { IssueReport } from '../../model/Defects'; 38import { WarnInfo } from '../../utils/common/Utils'; 39 40const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, 'ThisBindCheck'); 41 42const ARKTS_RULE_ID = '@migration/arkts-instance-method-bind-this'; 43const arktsMetaData: BaseMetaData = { 44 severity: 1, 45 ruleDocPath: '', 46 description: "Instance method shall bind the 'this' by default", 47}; 48 49const ARKUI_RULE_ID = '@migration/arkui-buildparam-passing'; 50const arkuiMetaData: BaseMetaData = { 51 severity: 1, 52 ruleDocPath: '', 53 description: 54 'The execution context of the function annotated with @Builder is determined at the time of declaration. Please check the code carefully to ensure the correct function context', 55}; 56const LOCAL_BUILDER_PREFIX = 'The @LocalBuilder decorator is not supported, it will be transformed to @Builder decorator. '; 57 58export class ThisBindCheck implements BaseChecker { 59 readonly metaData: BaseMetaData = arktsMetaData; 60 public rule: Rule; 61 public defects: Defects[] = []; 62 public issues: IssueReport[] = []; 63 64 private methodMatcher: MethodMatcher = { 65 matcherType: MatcherTypes.METHOD, 66 }; 67 68 public registerMatchers(): MatcherCallback[] { 69 const methodCb: MatcherCallback = { 70 matcher: this.methodMatcher, 71 callback: this.check, 72 }; 73 return [methodCb]; 74 } 75 76 public check = (targetMtd: ArkMethod): void => { 77 const file = targetMtd.getDeclaringArkFile(); 78 if (file.getName().includes('.test.ets')) { 79 return; 80 } 81 const scene = file.getScene(); 82 const stmts = targetMtd.getBody()?.getCfg().getStmts() ?? []; 83 for (let i = 0; i < stmts.length; ++i) { 84 const stmt = stmts[i]; 85 // const method = a.foo 86 if (!(stmt instanceof ArkAssignStmt)) { 87 continue; 88 } 89 const rightOp = stmt.getRightOp(); 90 if (!(rightOp instanceof ArkInstanceFieldRef)) { 91 continue; 92 } 93 const base = rightOp.getBase(); 94 const classTy = base.getType(); 95 if (!(classTy instanceof ClassType)) { 96 continue; 97 } 98 if (!(rightOp.getFieldSignature().getType() instanceof FunctionType)) { 99 continue; 100 } 101 const klass = scene.getClass(classTy.getClassSignature()); 102 const method = klass?.getMethodWithName(rightOp.getFieldName()); 103 if (!method || !method.getCfg() || !this.useThisInBody(method)) { 104 continue; 105 } 106 const isBuilder = method.hasBuilderDecorator(); 107 const isLocalBuilder = method.hasDecorator('LocalBuilder'); 108 const leftOp = stmt.getLeftOp(); 109 if (i + 1 >= stmts.length || !this.hasBindThis(leftOp, stmts[i + 1])) { 110 if (!this.isSafeUse(leftOp)) { 111 this.addIssueReport(stmt, isBuilder, isLocalBuilder, base, scene); 112 } 113 } 114 } 115 }; 116 117 private useThisInBody(method: ArkMethod): boolean { 118 const thisInstance = (method.getThisInstance() as Local)!; 119 const usedStmts = thisInstance.getUsedStmts(); 120 if (method.getName() !== 'constructor') { 121 return usedStmts.length > 0; 122 } 123 // constructor方法一定会有return this语句,此句若为ArkAnalyzer为constructor方法自动生成,则不在检查范围内 124 for (const stmt of usedStmts) { 125 if (stmt instanceof ArkReturnStmt && stmt.getOriginPositionInfo().getLineNo() <= 0) { 126 continue; 127 } 128 return true; 129 } 130 return false; 131 } 132 133 private isSafeUse(v: Value): boolean { 134 if (!(v instanceof Local)) { 135 return false; 136 } 137 138 const users = v.getUsedStmts(); 139 if (users.length === 0) { 140 return false; 141 } 142 for (const user of users) { 143 if (user instanceof ArkIfStmt) { 144 const cond = user.getConditionExpr(); 145 if (v !== cond.getOp1() && v !== cond.getOp2()) { 146 return false; 147 } 148 } else { 149 return false; 150 } 151 } 152 return true; 153 } 154 155 private hasBindThis(base: Value, next: Stmt): boolean { 156 if (!(next instanceof ArkAssignStmt)) { 157 return false; 158 } 159 const rightOp = next.getRightOp(); 160 if (rightOp instanceof ArkInstanceFieldRef && rightOp.getBase() === base) { 161 // const method = a.foo.name 162 return true; 163 } 164 if (!(rightOp instanceof ArkInstanceInvokeExpr)) { 165 return false; 166 } 167 if (rightOp.getBase() !== base) { 168 return false; 169 } 170 if (rightOp.getMethodSignature().getMethodSubSignature().getMethodName() !== 'bind') { 171 return false; 172 } 173 return true; 174 } 175 176 private addIssueReport(stmt: ArkAssignStmt, isBuilder: boolean, isLocalBuilder: boolean, operand: Value, scene: Scene): void { 177 if ((isBuilder || isLocalBuilder) && this.isAssignToBuilderParam(stmt, scene)) { 178 this.reportArkUIIssue(stmt, operand, isLocalBuilder); 179 } else { 180 this.reportArkTSIssue(stmt, operand); 181 } 182 } 183 184 private isAssignToBuilderParam(assign: ArkAssignStmt, scene: Scene): boolean { 185 /** 186 * class CA { 187 * @Builder builder() { ... } 188 * build() { 189 * Column() { CB({ content: this.builder }) } 190 * } 191 * } 192 * class CB { 193 * @BuilderParam content: () => void 194 * } 195 * 196 * ================================================== 197 * class %AC2$CA.build { 198 * constructor() { ... } 199 * %instInit() { 200 * %0 = this.builder 201 * this.content = %0 202 * } 203 * } 204 * class CA { 205 * ... 206 * build() { 207 * ... 208 * %3 = new %AC2$CA.build 209 * %3.constructor() 210 * %4 = new CB 211 * %4.constructor(%3) 212 * ... 213 * } 214 * ... 215 * } 216 */ 217 const currentMethod = assign.getCfg().getDeclaringMethod(); 218 if (currentMethod.getName() !== '%instInit') { 219 return false; 220 } 221 const currentClass = currentMethod.getDeclaringArkClass(); 222 if (!currentClass.isAnonymousClass()) { 223 return false; 224 } 225 const currentClassSig = currentClass.getSignature(); 226 227 const leftOp = assign.getLeftOp(); 228 if (!(leftOp instanceof Local)) { 229 return false; 230 } 231 const usedStmts = leftOp.getUsedStmts(); 232 if (usedStmts.length !== 1) { 233 return false; 234 } 235 const usedStmt = usedStmts[0]; 236 if (!(usedStmt instanceof ArkAssignStmt)) { 237 return false; 238 } 239 const target = usedStmt.getLeftOp(); 240 if (!(target instanceof ArkInstanceFieldRef)) { 241 return false; 242 } 243 const baseTy = target.getBase().getType(); 244 if (!(baseTy instanceof ClassType)) { 245 return false; 246 } 247 if ((baseTy as ClassType).getClassSignature() !== currentClassSig) { 248 return false; 249 } 250 const fieldName = target.getFieldName(); 251 252 const declaringClassName = currentClassSig.getDeclaringClassName(); 253 if (declaringClassName === currentClass.getName()) { 254 return false; 255 } 256 const declaringClass = currentClass.getDeclaringArkFile().getClassWithName(declaringClassName); 257 if (!declaringClass) { 258 return false; 259 } 260 let targetClassSig = this.findDefinitionOfAnonymousClass(declaringClass, currentClassSig); 261 if (!targetClassSig) { 262 return false; 263 } 264 const targetClass = scene.getClass(targetClassSig); 265 if (!targetClass) { 266 return false; 267 } 268 const arkField = targetClass.getFieldWithName(fieldName); 269 if (!arkField) { 270 return false; 271 } 272 return arkField.hasBuilderParamDecorator(); 273 } 274 275 private findDefinitionOfAnonymousClass(declaringClass: ArkClass, anonymousClassSig: ClassSignature): ClassSignature | undefined { 276 for (const m of declaringClass.getMethods()) { 277 const stmts = m.getBody()?.getCfg().getStmts() ?? []; 278 for (const stmt of stmts) { 279 if (!(stmt instanceof ArkAssignStmt)) { 280 continue; 281 } 282 const rightOp = stmt.getRightOp(); 283 if (!(rightOp instanceof ArkNewExpr)) { 284 continue; 285 } 286 if (rightOp.getClassType().getClassSignature() !== anonymousClassSig) { 287 continue; 288 } 289 const local = stmt.getLeftOp() as Local; 290 const classSignature = this.processUsedStmts(local, anonymousClassSig); 291 if (!classSignature) { 292 continue; 293 } 294 return classSignature; 295 } 296 } 297 return undefined; 298 } 299 300 private processUsedStmts(local: Local, anonymousClassSig: ClassSignature): ClassSignature | null { 301 for (const usedStmt of local.getUsedStmts()) { 302 const invoke = usedStmt.getInvokeExpr(); 303 if (!invoke) { 304 continue; 305 } 306 const sig = invoke.getMethodSignature(); 307 if (sig.getMethodSubSignature().getMethodName() !== 'constructor') { 308 continue; 309 } 310 if (sig.getDeclaringClassSignature() === anonymousClassSig) { 311 continue; 312 } 313 return sig.getDeclaringClassSignature(); 314 } 315 return null; 316 } 317 318 private reportArkTSIssue(stmt: ArkAssignStmt, operand: Value): void { 319 const severity = this.rule.alert ?? this.metaData.severity; 320 const warnInfo = this.getLineAndColumn(stmt, operand); 321 const problem = 'DefaultBindThis'; 322 const desc = `${this.metaData.description} (${this.rule.ruleId.replace('@migration/', '')})`; 323 let defects = new Defects( 324 warnInfo.line, 325 warnInfo.startCol, 326 warnInfo.endCol, 327 problem, 328 desc, 329 severity, 330 ARKTS_RULE_ID, 331 warnInfo.filePath, 332 this.metaData.ruleDocPath, 333 true, 334 false, 335 false 336 ); 337 this.issues.push(new IssueReport(defects, undefined)); 338 } 339 340 private reportArkUIIssue(stmt: ArkAssignStmt, operand: Value, isLocalBuilder: boolean): void { 341 const severity = this.rule.alert ?? arkuiMetaData.severity; 342 const warnInfo = this.getLineAndColumn(stmt, operand); 343 const problem = 'BuilderParamContextChanged'; 344 const desc = `${isLocalBuilder ? LOCAL_BUILDER_PREFIX : ''}${arkuiMetaData.description} (${ARKUI_RULE_ID.replace('@migration/', '')})`; 345 let defects = new Defects( 346 warnInfo.line, 347 warnInfo.startCol, 348 warnInfo.endCol, 349 problem, 350 desc, 351 severity, 352 ARKUI_RULE_ID, 353 warnInfo.filePath, 354 arkuiMetaData.ruleDocPath, 355 true, 356 false, 357 false 358 ); 359 this.issues.push(new IssueReport(defects, undefined)); 360 } 361 362 private getLineAndColumn(stmt: ArkAssignStmt, operand: Value): WarnInfo { 363 const arkFile = stmt.getCfg().getDeclaringMethod().getDeclaringArkFile(); 364 const originPosition = stmt.getOperandOriginalPosition(operand); 365 if (arkFile && originPosition) { 366 const originPath = arkFile.getFilePath(); 367 const line = originPosition.getFirstLine(); 368 const startCol = originPosition.getFirstCol(); 369 const endCol = startCol; 370 return { line, startCol, endCol, filePath: originPath }; 371 } else { 372 logger.debug('ArkFile is null.'); 373 } 374 return { line: -1, startCol: -1, endCol: -1, filePath: '' }; 375 } 376} 377