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 Scene, 19 Local, 20 Stmt, 21 Type, 22 ArkMethod, 23 AliasType, 24 AbstractInvokeExpr, 25 Value, 26 AstTreeUtils, 27 ts, 28} from 'arkanalyzer'; 29import Logger, { LOG_MODULE_TYPE } from 'arkanalyzer/lib/utils/logger'; 30import { BaseChecker, BaseMetaData } from '../BaseChecker'; 31import { Rule, Defects, MatcherTypes, MatcherCallback, MethodMatcher } from '../../Index'; 32import { IssueReport } from '../../model/Defects'; 33import { RuleFix } from '../../model/Fix'; 34import { FixPosition, FixUtils } from '../../utils/common/FixUtils'; 35import { WarnInfo } from '../../utils/common/Utils'; 36 37const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, 'CustomBuilderCheck'); 38const gMetaData: BaseMetaData = { 39 severity: 1, 40 ruleDocPath: '', 41 description: 'The CustomBuilder type parameter only accepts functions annotated with @Builder', 42}; 43 44export class CustomBuilderCheck implements BaseChecker { 45 readonly metaData: BaseMetaData = gMetaData; 46 public rule: Rule; 47 public defects: Defects[] = []; 48 public issues: IssueReport[] = []; 49 50 private buildMatcher: MethodMatcher = { 51 matcherType: MatcherTypes.METHOD, 52 }; 53 54 public registerMatchers(): MatcherCallback[] { 55 const matchBuildCb: MatcherCallback = { 56 matcher: this.buildMatcher, 57 callback: this.check, 58 }; 59 return [matchBuildCb]; 60 } 61 62 public check = (target: ArkMethod): void => { 63 const scene = target.getDeclaringArkFile().getScene(); 64 const stmts = target.getBody()?.getCfg().getStmts() ?? []; 65 let locals = new Set<Local>(); 66 for (const stmt of stmts) { 67 // 场景1:函数调用赋值给CustomBuilder类型的对象 68 const local = this.isCallToBuilder(stmt, scene); 69 if (local) { 70 locals.add(local); 71 continue; 72 } 73 const usage = this.isPassToCustomBuilder(stmt, locals); 74 if (usage) { 75 this.addIssueReport(usage.getDeclaringStmt()!, usage); 76 } 77 } 78 }; 79 80 private isCallToBuilder(stmt: Stmt, scene: Scene): Local | undefined { 81 if (!(stmt instanceof ArkAssignStmt)) { 82 return undefined; 83 } 84 const leftOp = stmt.getLeftOp(); 85 if (!(leftOp instanceof Local)) { 86 return undefined; 87 } 88 const rightOp = stmt.getRightOp(); 89 if (!(rightOp instanceof AbstractInvokeExpr)) { 90 return undefined; 91 } 92 const method = scene.getMethod(rightOp.getMethodSignature()); 93 if (method && method.hasBuilderDecorator()) { 94 return leftOp; 95 } 96 return undefined; 97 } 98 99 private isCustomBuilderTy(ty: Type): boolean { 100 return ty instanceof AliasType && ty.getName() === 'CustomBuilder'; 101 } 102 103 private isPassToCustomBuilder(stmt: Stmt, locals: Set<Local>): Local | undefined { 104 let res: Local | undefined = undefined; 105 if (stmt instanceof ArkAssignStmt) { 106 if (this.isCustomBuilderTy(stmt.getLeftOp().getType())) { 107 const rightOp = stmt.getRightOp(); 108 if (rightOp instanceof Local && locals.has(rightOp)) { 109 res = rightOp; 110 } 111 } 112 } 113 if (res !== undefined) { 114 return res; 115 } 116 const invokeExpr = stmt.getInvokeExpr(); 117 if (invokeExpr) { 118 const paramTys = invokeExpr.getMethodSignature().getMethodSubSignature().getParameterTypes(); 119 const args = invokeExpr.getArgs(); 120 for (let i = 0; i < paramTys.length && i < args.length; ++i) { 121 if (!this.isCustomBuilderTy(paramTys[i])) { 122 continue; 123 } 124 const arg = args[i]; 125 if (arg instanceof Local && locals.has(arg)) { 126 return arg; 127 } 128 } 129 } 130 return undefined; 131 } 132 133 private addIssueReport(stmt: Stmt, operand: Value): void { 134 const severity = this.rule.alert ?? this.metaData.severity; 135 const warnInfo = this.getLineAndColumn(stmt, operand); 136 const problem = 'CustomBuilderTypeChanged'; 137 const desc = `${this.metaData.description} (${this.rule.ruleId.replace('@migration/', '')})`; 138 let defects = new Defects( 139 warnInfo.line, 140 warnInfo.startCol, 141 warnInfo.endCol, 142 problem, 143 desc, 144 severity, 145 this.rule.ruleId, 146 warnInfo.filePath, 147 this.metaData.ruleDocPath, 148 true, 149 false, 150 true 151 ); 152 const fixPosition: FixPosition = { 153 startLine: warnInfo.line, 154 startCol: warnInfo.startCol, 155 endLine: -1, 156 endCol: -1, 157 }; 158 const ruleFix = this.generateRuleFix(fixPosition, stmt) ?? undefined; 159 this.issues.push(new IssueReport(defects, ruleFix)); 160 if (ruleFix === undefined) { 161 defects.fixable = false; 162 } 163 } 164 165 private getLineAndColumn(stmt: Stmt, operand: Value): WarnInfo { 166 const arkFile = stmt.getCfg().getDeclaringMethod().getDeclaringArkFile(); 167 const originPosition = stmt.getOperandOriginalPosition(operand); 168 if (arkFile && originPosition) { 169 const originPath = arkFile.getFilePath(); 170 const line = originPosition.getFirstLine(); 171 const startCol = originPosition.getFirstCol(); 172 const endCol = startCol; 173 return { line, startCol, endCol, filePath: originPath }; 174 } else { 175 logger.debug('ArkFile is null.'); 176 } 177 return { line: -1, startCol: -1, endCol: -1, filePath: '' }; 178 } 179 180 private generateRuleFix(fixPosition: FixPosition, stmt: Stmt): RuleFix | null { 181 let ruleFix: RuleFix = new RuleFix(); 182 const endPosition = this.getEndPositionOfStmt(stmt); 183 if (endPosition) { 184 fixPosition.endLine = endPosition.line; 185 fixPosition.endCol = endPosition.col; 186 } 187 const arkFile = stmt.getCfg().getDeclaringMethod().getDeclaringArkFile(); 188 const sourceFile = AstTreeUtils.getASTNode(arkFile.getName(), arkFile.getCode()); 189 const range = FixUtils.getRangeWithAst(sourceFile, fixPosition); 190 ruleFix.range = range; 191 const originalText = FixUtils.getSourceWithRange(sourceFile, range); 192 if (originalText !== null) { 193 ruleFix.text = this.generateReplaceText(sourceFile, originalText, fixPosition); 194 } else { 195 return null; 196 } 197 return ruleFix; 198 } 199 200 private getEndPositionOfStmt(stmt: Stmt): { line: number; col: number } | null { 201 const allPositions = stmt.getOperandOriginalPositions(); 202 if (allPositions === undefined) { 203 return null; 204 } 205 let res = { line: -1, col: -1 }; 206 allPositions.forEach(position => { 207 if (position.getLastLine() > res.line) { 208 res = { line: position.getLastLine(), col: position.getLastCol() }; 209 return; 210 } 211 if (position.getLastLine() === res.line && position.getLastCol() > res.col) { 212 res = { line: position.getLastLine(), col: position.getLastCol() }; 213 return; 214 } 215 }); 216 return res; 217 } 218 219 private generateReplaceText(sourceFile: ts.SourceFile, originalText: string, fixPosition: FixPosition): string { 220 // 已经是箭头函数的场景,无需任何处理 221 if (originalText.includes('=>')) { 222 return originalText; 223 } 224 225 // 非箭头函数包裹的函数调用,需要使用箭头函数包裹 226 const eol = FixUtils.getEolSymbol(sourceFile, fixPosition.startLine); 227 const startLineIndent = FixUtils.getIndentOfLine(sourceFile, fixPosition.startLine) ?? 0; 228 const increaseSpaces = FixUtils.getIndentWidth(sourceFile, fixPosition.startLine); 229 const space = ' '; 230 231 let res = `() => {${eol}`; 232 const originalLineStrs = originalText.split(eol); 233 res += `${space.repeat(startLineIndent + increaseSpaces)}${originalLineStrs[0]}${eol}`; 234 for (let index = 1; index < originalLineStrs.length; index++) { 235 res += `${space.repeat(increaseSpaces)}${originalLineStrs[index]}${eol}`; 236 } 237 res += `${space.repeat(startLineIndent)}}`; 238 return res; 239 } 240} 241