• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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