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