• 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    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