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 ArkInstanceFieldRef, 19 ArkInstanceInvokeExpr, 20 CallGraph, 21 CallGraphBuilder, 22 Local, 23 Stmt, 24 Value, 25 FieldSignature, 26 ArkMethod, 27} from 'arkanalyzer'; 28import Logger, { LOG_MODULE_TYPE } from 'arkanalyzer/lib/utils/logger'; 29import { BaseChecker, BaseMetaData } from '../BaseChecker'; 30import { Rule, Defects, MatcherTypes, MatcherCallback, ClassMatcher, MethodMatcher } from '../../Index'; 31import { IssueReport } from '../../model/Defects'; 32import { CALL_DEPTH_LIMIT, CALLBACK_METHOD_NAME, CallGraphHelper } from './Utils'; 33import { WarnInfo } from '../../utils/common/Utils'; 34 35const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, 'ModifyStateVarCheck'); 36const gMetaData: BaseMetaData = { 37 severity: 1, 38 ruleDocPath: '', 39 description: 'It is not allowed to update state when the build function is running', 40}; 41 42export class ModifyStateVarCheck implements BaseChecker { 43 readonly metaData: BaseMetaData = gMetaData; 44 public rule: Rule; 45 public defects: Defects[] = []; 46 public issues: IssueReport[] = []; 47 48 private classMatcher: ClassMatcher = { 49 matcherType: MatcherTypes.CLASS, 50 hasViewTree: true, 51 }; 52 53 private buildMatcher: MethodMatcher = { 54 matcherType: MatcherTypes.METHOD, 55 class: [this.classMatcher], 56 name: ['build'], 57 }; 58 59 public registerMatchers(): MatcherCallback[] { 60 const matchBuildCb: MatcherCallback = { 61 matcher: this.buildMatcher, 62 callback: this.check, 63 }; 64 return [matchBuildCb]; 65 } 66 67 public check = (target: ArkMethod): void => { 68 const scene = target.getDeclaringArkFile().getScene(); 69 let callGraph = CallGraphHelper.getCGInstance(scene); 70 let callGraphBuilder = new CallGraphBuilder(callGraph, scene); 71 callGraphBuilder.buildClassHierarchyCallGraph([target.getSignature()]); 72 73 const arkClass = target.getDeclaringArkClass(); 74 const stateVars = new Set( 75 arkClass 76 .getFields() 77 .filter(f => f.hasDecorator('State')) 78 .map(f => f.getSignature()) 79 ); 80 if (stateVars.size > 0) { 81 this.checkMethod(target, callGraph, stateVars); 82 } 83 }; 84 85 private checkMethod(target: ArkMethod, cg: CallGraph, stateVars: Set<FieldSignature>, depth: number = 0): void { 86 if (depth > CALL_DEPTH_LIMIT) { 87 return; 88 } 89 let aliases = new Set<Local>(); 90 const stmts = target.getBody()?.getCfg().getStmts() ?? []; 91 for (const stmt of stmts) { 92 const invokeExpr = stmt.getInvokeExpr(); 93 if (invokeExpr && invokeExpr instanceof ArkInstanceInvokeExpr) { 94 if ( 95 CALLBACK_METHOD_NAME.includes( 96 invokeExpr.getMethodSignature().getMethodSubSignature().getMethodName() 97 ) 98 ) { 99 continue; 100 } 101 } 102 cg.getCallSiteByStmt(stmt).forEach(cs => { 103 const callee = cg.getArkMethodByFuncID(cs.calleeFuncID); 104 if (callee) { 105 this.checkMethod(callee, cg, stateVars, depth + 1); 106 } 107 }); 108 if (!(stmt instanceof ArkAssignStmt)) { 109 continue; 110 } 111 if (this.isAssignToStateVar(stmt, stateVars, aliases)) { 112 this.addIssueReport(stmt, stmt.getLeftOp()); 113 } else { 114 const alias = this.isAliasOfStateVar(stmt, stateVars, aliases); 115 if (alias) { 116 aliases.add(alias); 117 } 118 } 119 } 120 } 121 122 private isAssignToStateVar(stmt: ArkAssignStmt, stateVars: Set<FieldSignature>, aliases: Set<Local>): boolean { 123 const leftOp = stmt.getLeftOp(); 124 if (leftOp instanceof ArkInstanceFieldRef) { 125 // this.n = 1 or this.obj.n = 1 126 return stateVars.has(leftOp.getFieldSignature()) || aliases.has(leftOp.getBase()); 127 } else if (leftOp instanceof Local) { 128 return aliases.has(leftOp); 129 } 130 return false; 131 } 132 133 private isAliasOfStateVar( 134 stmt: ArkAssignStmt, 135 stateVars: Set<FieldSignature>, 136 aliases: Set<Local> 137 ): Local | undefined { 138 const leftOp = stmt.getLeftOp(); 139 const rightOp = stmt.getRightOp(); 140 if (leftOp instanceof Local && rightOp instanceof ArkInstanceFieldRef) { 141 // %1 = this.n 142 // or 143 // %1 = this.obj; %2 = %1.n 144 if (stateVars.has(rightOp.getFieldSignature()) || aliases.has(rightOp.getBase())) { 145 return leftOp; 146 } 147 } 148 return undefined; 149 } 150 151 private addIssueReport(stmt: Stmt, operand: Value): void { 152 const severity = this.rule.alert ?? this.metaData.severity; 153 const warnInfo = this.getLineAndColumn(stmt, operand); 154 const problem = 'NoStateUpdateDuringRender'; 155 const desc = `${this.metaData.description} (${this.rule.ruleId.replace('@migration/', '')})`; 156 let defects = new Defects( 157 warnInfo.line, 158 warnInfo.startCol, 159 warnInfo.endCol, 160 problem, 161 desc, 162 severity, 163 this.rule.ruleId, 164 warnInfo.filePath, 165 this.metaData.ruleDocPath, 166 true, 167 false, 168 false 169 ); 170 this.issues.push(new IssueReport(defects, undefined)); 171 } 172 173 private getLineAndColumn(stmt: Stmt, operand: Value): WarnInfo { 174 const arkFile = stmt.getCfg().getDeclaringMethod().getDeclaringArkFile(); 175 const originPosition = stmt.getOperandOriginalPosition(operand); 176 if (arkFile && originPosition) { 177 const originPath = arkFile.getFilePath(); 178 const line = originPosition.getFirstLine(); 179 const startCol = originPosition.getFirstCol(); 180 const endCol = startCol; 181 return { line, startCol, endCol, filePath: originPath }; 182 } else { 183 logger.debug('ArkFile is null.'); 184 } 185 return { line: -1, startCol: -1, endCol: -1, filePath: '' }; 186 } 187} 188