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 path from 'path'; 17import { 18 AbstractInvokeExpr, 19 ArkAssignStmt, 20 ArkClass, 21 ArkField, 22 ArkNewExpr, 23 ArkReturnStmt, 24 AstTreeUtils, 25 ClassType, 26 fileSignatureCompare, 27 Local, 28 Scene, 29 Type, 30} from 'arkanalyzer'; 31import { ClassCategory } from 'arkanalyzer/lib/core/model/ArkClass'; 32import Logger, { LOG_MODULE_TYPE } from 'arkanalyzer/lib/utils/logger'; 33import { BaseChecker, BaseMetaData } from '../BaseChecker'; 34import { Rule, Defects, ClassMatcher, MatcherTypes, MatcherCallback } from '../../Index'; 35import { IssueReport } from '../../model/Defects'; 36import { RuleFix } from '../../model/Fix'; 37import { FixUtils } from '../../utils/common/FixUtils'; 38import { WarnInfo } from '../../utils/common/Utils'; 39 40const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, 'ObservedDecoratorCheck'); 41const gMetaData: BaseMetaData = { 42 severity: 1, 43 ruleDocPath: '', 44 description: '', 45}; 46 47const DECORATOR_SET: Set<string> = new Set<string>([ 48 'State', 49 'Prop', 50 'Link', 51 'Provide', 52 'Consume', 53 'LocalStorageProp', 54 'LocalStorageLink', 55 'StorageProp', 56 'StorageLink', 57]); 58 59// TODO: 需要考虑type alias、union type、intersection type中涉及class的场景 60export class ObservedDecoratorCheck implements BaseChecker { 61 readonly metaData: BaseMetaData = gMetaData; 62 public rule: Rule; 63 public defects: Defects[] = []; 64 public issues: IssueReport[] = []; 65 66 private clsMatcher: ClassMatcher = { 67 matcherType: MatcherTypes.CLASS, 68 category: [ClassCategory.STRUCT], 69 }; 70 71 public registerMatchers(): MatcherCallback[] { 72 const matchClsCb: MatcherCallback = { 73 matcher: this.clsMatcher, 74 callback: this.check, 75 }; 76 return [matchClsCb]; 77 } 78 79 public check = (arkClass: ArkClass): void => { 80 const scene = arkClass.getDeclaringArkFile().getScene(); 81 const projectName = arkClass.getDeclaringArkFile().getProjectName(); 82 for (const field of arkClass.getFields()) { 83 if (!field.getDecorators().some(d => DECORATOR_SET.has(d.getKind()))) { 84 continue; 85 } 86 // usedClasses用于记录field的初始化中涉及的所有class 87 let usedClasses: Set<ArkClass> = new Set(); 88 // issueClasses用于记录usedClasses以及他们的所有父类 89 let issueClasses: Set<ArkClass> = new Set(); 90 // ArkAnalyzer此处有问题,若field的类型注解为unclear type,会用右边的替换左边的。 91 const fieldType = field.getType(); 92 // 此处仅对field为class类型进行检查,包含class和interface,非class类型不在本规则检查范围之内 93 if (!(fieldType instanceof ClassType)) { 94 continue; 95 } 96 const initializers = field.getInitializer(); 97 let canFindAllTargets = true; 98 99 let locals: Set<Local> = new Set(); 100 101 // field的初始化语句的最后一句,一定是将右边的value赋值给field,此处仍然判断一次,排除其他场景或者初始化语句为空的场景 102 const lastStmt = initializers[initializers.length - 1]; 103 if (!(lastStmt instanceof ArkAssignStmt)) { 104 continue; 105 } 106 const start = lastStmt.getRightOp(); 107 // 直接对属性进行常量赋值属于这种场景 108 if (!(start instanceof Local)) { 109 continue; 110 } 111 locals.add(start); 112 for (const stmt of initializers.slice(0, -1).reverse()) { 113 if (!(stmt instanceof ArkAssignStmt)) { 114 continue; 115 } 116 117 const leftOp = stmt.getLeftOp(); 118 const rightOp = stmt.getRightOp(); 119 if (!(leftOp instanceof Local)) { 120 continue; 121 } 122 if (!locals.has(leftOp)) { 123 continue; 124 } 125 if (rightOp instanceof Local) { 126 locals.add(rightOp); 127 } else if (rightOp instanceof ArkNewExpr) { 128 // 此处需要区分field = new cls()和field = {}两种场景,查找完毕需继续遍历stmts以解析条件表达式造成的多赋值场景 129 canFindAllTargets = canFindAllTargets && this.handleNewExpr(scene, fieldType, rightOp, usedClasses, projectName); 130 } else if (rightOp instanceof AbstractInvokeExpr) { 131 canFindAllTargets = 132 canFindAllTargets && this.handleInvokeExpr(scene, fieldType, rightOp, usedClasses, projectName); 133 } else { 134 // 对应场景为使用条件表达式cond ? 123 : 456赋值时 135 continue; 136 } 137 } 138 139 for (const cls of usedClasses) { 140 issueClasses.add(cls); 141 this.getAllSuperClasses( 142 cls, 143 superCls => superCls.getCategory() === ClassCategory.CLASS && issueClasses.add(superCls) 144 ); 145 } 146 147 for (const target of issueClasses) { 148 if (target.hasDecorator('Observed')) { 149 continue; 150 } 151 const pos = this.getClassPos(target); 152 const description = this.generateIssueDescription(field, target); 153 const ruleFix = this.generateRuleFix(pos, target) ?? undefined; 154 this.addIssueReport(pos, description, ruleFix); 155 } 156 157 if (!canFindAllTargets) { 158 const pos = this.getFieldPos(field); 159 const description = this.generateIssueDescription(field, null, false); 160 this.addIssueReport(pos, description); 161 } 162 } 163 }; 164 165 // 此处需要区分field = new cls()和field = {}两种场景 166 // 对于field = new cls()场景,需要查找此右边class的所有父class 167 // 对于field = {}场景,需要查找左边field类型为class时的所有父class 168 private handleNewExpr(scene: Scene, fieldType: Type, rightOp: ArkNewExpr, targets: Set<ArkClass>, projectName: string): boolean { 169 const target = scene.getClass(rightOp.getClassType().getClassSignature()); 170 if (target === null) { 171 return false; 172 } 173 // class为非本项目的内容时,表示调用到三方库、SDK等内容,不再继续进行查找 174 if (target.getDeclaringArkFile().getProjectName() !== projectName) { 175 return true; 176 } 177 178 if (!target.isAnonymousClass()) { 179 // 理论上来说ArkNewExpr中的class一定ClassCategory.CLASS,此处仍然显式的检查一次 180 if (target.getCategory() !== ClassCategory.CLASS) { 181 return true; 182 } 183 targets.add(target); 184 return true; 185 } 186 187 // 处理匿名类场景,若右边为object literal时,需考虑左边是什么类型注解,将涉及的class找出 188 if (target.getCategory() !== ClassCategory.OBJECT) { 189 return true; 190 } 191 if (!(fieldType instanceof ClassType)) { 192 return true; 193 } 194 const fieldClass = scene.getClass(fieldType.getClassSignature()); 195 if (fieldClass === null) { 196 return false; 197 } 198 // fieldClass为非本项目的内容时,表示调用到三方库、SDK等内容,不再继续进行查找 199 if (fieldClass.getDeclaringArkFile().getProjectName() !== projectName) { 200 return true; 201 } 202 if (fieldClass.getCategory() !== ClassCategory.CLASS) { 203 return true; 204 } 205 targets.add(fieldClass); 206 return true; 207 } 208 209 // 遍历此处的调用方法的所有return stmts,查找class 210 // 此处需要区分返回值为class和object literal两种场景 211 // 对于返回值为class的场景,需要查找此class的所有父class 212 // 对于存在返回值为object literal的场景,需要查找左边field类型为class时的所有父class 213 private handleInvokeExpr( 214 scene: Scene, 215 fieldType: Type, 216 invokeExpr: AbstractInvokeExpr, 217 targets: Set<ArkClass>, 218 projectName: string 219 ): boolean { 220 let canFindAllTargets = true; 221 const callMethod = scene.getMethod(invokeExpr.getMethodSignature()); 222 if (callMethod === null) { 223 return false; 224 } 225 // callMethod为非本项目的内容时,表示调用到三方库、SDK等内容,不再继续进行查找 226 if (callMethod.getDeclaringArkFile().getProjectName() !== projectName) { 227 return true; 228 } 229 const stmts = callMethod.getBody()?.getCfg().getStmts(); 230 if (stmts === undefined) { 231 return false; 232 } 233 for (const stmt of stmts) { 234 if (!(stmt instanceof ArkReturnStmt)) { 235 continue; 236 } 237 const opType = stmt.getOp().getType(); 238 if (!(opType instanceof ClassType)) { 239 continue; 240 } 241 const returnClass = scene.getClass(opType.getClassSignature()); 242 if (returnClass === null) { 243 canFindAllTargets = false; 244 continue; 245 } 246 if (returnClass.getCategory() === ClassCategory.CLASS) { 247 targets.add(returnClass); 248 } else if (returnClass.getCategory() === ClassCategory.OBJECT) { 249 if (!(fieldType instanceof ClassType)) { 250 continue; 251 } 252 const leftClass = scene.getClass(fieldType.getClassSignature()); 253 if (leftClass === null) { 254 canFindAllTargets = false; 255 continue; 256 } 257 if (leftClass.getCategory() === ClassCategory.CLASS) { 258 targets.add(leftClass); 259 } 260 } 261 } 262 return canFindAllTargets; 263 } 264 265 // 采用广度优先遍历方式,逐层获取该class的所有父类,一直查找到基类 266 // arkanalyzer getAllHeritageClasses有点问题,对于未能推出来的父类会忽略,不加入列表中返回。 267 private getAllSuperClasses(arkClass: ArkClass, callback: (value: ArkClass) => void): void { 268 let superClasses: Set<ArkClass> = new Set(); 269 const classes = arkClass.getAllHeritageClasses(); 270 while (classes.length > 0) { 271 const superCls = classes.shift()!; 272 const superSuperCls = superCls.getAllHeritageClasses(); 273 callback(superCls); 274 275 if (superSuperCls.length > 0) { 276 classes.push(...superSuperCls); 277 } 278 } 279 } 280 281 private generateIssueDescription( 282 field: ArkField, 283 issueClass: ArkClass | null, 284 canFindAllTargets: boolean = true 285 ): string { 286 if (issueClass === null || !canFindAllTargets) { 287 return `can not find all classes, please check this field manually (arkui-data-observation)`; 288 } 289 const fieldLine = field.getOriginPosition().getLineNo(); 290 const fieldColumn = field.getOriginPosition().getColNo(); 291 292 const fieldFileSig = field.getDeclaringArkClass().getDeclaringArkFile().getFileSignature(); 293 const issueClassSig = issueClass.getDeclaringArkFile().getFileSignature(); 294 let res = `but it's not be annotated by @Observed (arkui-data-observation)`; 295 if (fileSignatureCompare(fieldFileSig, issueClassSig)) { 296 res = `Class ${issueClass.getName()} is used by state property in [${fieldLine}, ${fieldColumn}], ` + res; 297 } else { 298 const filePath = path.normalize(fieldFileSig.getFileName()); 299 res = `Class ${issueClass.getName()} is used by state property in file ${filePath} [${fieldLine}, ${fieldColumn}], ` + res; 300 } 301 return res; 302 } 303 304 private getClassPos(cls: ArkClass): WarnInfo { 305 const arkFile = cls.getDeclaringArkFile(); 306 if (arkFile) { 307 const originPath = path.normalize(arkFile.getFilePath()); 308 const line = cls.getLine(); 309 const startCol = cls.getColumn(); 310 const endCol = startCol; 311 return { line, startCol, endCol, filePath: originPath }; 312 } else { 313 logger.debug('ArkFile is null.'); 314 return { line: -1, startCol: -1, endCol: -1, filePath: '' }; 315 } 316 } 317 318 private getFieldPos(field: ArkField): WarnInfo { 319 const arkFile = field.getDeclaringArkClass().getDeclaringArkFile(); 320 const pos = field.getOriginPosition(); 321 if (arkFile && pos) { 322 const originPath = arkFile.getFilePath(); 323 const line = pos.getLineNo(); 324 const startCol = pos.getColNo(); 325 const endCol = startCol; 326 return { line, startCol, endCol, filePath: originPath }; 327 } else { 328 logger.debug('ArkFile is null.'); 329 return { line: -1, startCol: -1, endCol: -1, filePath: '' }; 330 } 331 } 332 333 private addIssueReport(warnInfo: WarnInfo, description: string, ruleFix?: RuleFix): void { 334 const problem = 'DataObservationNeedObserved'; 335 const severity = this.rule.alert ?? this.metaData.severity; 336 let defects = new Defects( 337 warnInfo.line, 338 warnInfo.startCol, 339 warnInfo.endCol, 340 problem, 341 description, 342 severity, 343 this.rule.ruleId, 344 warnInfo.filePath, 345 this.metaData.ruleDocPath, 346 true, 347 false, 348 true 349 ); 350 this.issues.push(new IssueReport(defects, ruleFix)); 351 if (ruleFix === undefined) { 352 defects.fixable = false; 353 } 354 } 355 356 private generateRuleFix(warnInfo: WarnInfo, targetClass: ArkClass): RuleFix | null { 357 const arkFile = targetClass.getDeclaringArkFile(); 358 const sourceFile = AstTreeUtils.getASTNode(arkFile.getName(), arkFile.getCode()); 359 const startLineRange = FixUtils.getLineRange(sourceFile, warnInfo.line); 360 if (startLineRange === null) { 361 return null; 362 } 363 364 const ruleFix = new RuleFix(); 365 ruleFix.range = startLineRange; 366 367 const startLineStr = FixUtils.getSourceWithRange(sourceFile, startLineRange); 368 if (startLineStr === null) { 369 return null; 370 } 371 372 const eol = FixUtils.getEolSymbol(sourceFile, warnInfo.line); 373 const startLineIndent = FixUtils.getIndentOfLine(sourceFile, warnInfo.line); 374 if (startLineIndent === null) { 375 return null; 376 } 377 const space = ' '; 378 ruleFix.text = `@Observed${eol}${space.repeat(startLineIndent)}${startLineStr}`; 379 return ruleFix; 380 } 381} 382