• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2022-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 * as arkts from '@koalaui/libarkts';
17import { getInteropPath } from '../path';
18const interop = require(getInteropPath());
19const nullptr = interop.nullptr;
20import { AbstractVisitor, VisitorOptions } from '../common/abstract-visitor';
21import {
22    CustomComponentNames,
23    getCustomComponentOptionsName,
24    createOptionalClassProperty,
25    findLocalImport,
26} from './utils';
27import { isAnnotation, updateStructMetadata, backingField, expectName, annotation } from '../common/arkts-utils';
28import { EntryWrapperNames, findEntryWithStorageInClassAnnotations } from './entry-translators/utils';
29import { factory as entryFactory } from './entry-translators/factory';
30import {
31    hasDecorator,
32    DecoratorNames,
33    getStateManagementType,
34    collectPropertyDecorators,
35} from './property-translators/utils';
36import { factory } from './ui-factory';
37import { StructMap } from '../common/program-visitor';
38import { generateTempCallFunction } from './interop';
39import { stringify } from 'querystring';
40
41export interface ComponentTransformerOptions extends VisitorOptions {
42    arkui?: string;
43}
44
45type ScopeInfo = {
46    name: string;
47    isEntry?: boolean;
48    isComponent?: boolean;
49    isReusable?: boolean;
50};
51
52interface ComponentContext {
53    structMembers: Map<string, arkts.AstNode[]>;
54}
55
56export interface InteropContext {
57    className: string;
58    path: string;
59    line?: number;
60    col?: number;
61    arguments?: arkts.ObjectExpression;
62}
63
64export class ComponentTransformer extends AbstractVisitor {
65    private scopeInfos: ScopeInfo[] = [];
66    private componentInterfaceCollection: arkts.TSInterfaceDeclaration[] = [];
67    private entryNames: string[] = [];
68    private reusableNames: string[] = [];
69    private readonly arkui?: string;
70    private context: ComponentContext = { structMembers: new Map() };
71    private isCustomComponentImported: boolean = false;
72    private isEntryPointImported: boolean = false;
73    private hasLegacy = false;
74    private legacyStructMap: Map<string, StructMap> = new Map();
75    private legacyCallMap: Map<string, string> = new Map();
76
77    constructor(options?: ComponentTransformerOptions) {
78        const _options: ComponentTransformerOptions = options ?? {};
79        super(_options);
80        this.arkui = _options.arkui;
81    }
82
83    reset(): void {
84        super.reset();
85        this.scopeInfos = [];
86        this.componentInterfaceCollection = [];
87        this.entryNames = [];
88        this.reusableNames = [];
89        this.context = { structMembers: new Map() };
90        this.isCustomComponentImported = false;
91        this.isEntryPointImported = false;
92        this.hasLegacy = false;
93        this.legacyStructMap = new Map();
94        this.legacyCallMap = new Map();
95    }
96
97    enter(node: arkts.AstNode) {
98        if (arkts.isStructDeclaration(node) && !!node.definition.ident) {
99            const scopeInfo: ScopeInfo = { name: node.definition.ident.name };
100            node.definition.annotations.forEach((anno) => {
101                scopeInfo.isEntry ||= isAnnotation(anno, CustomComponentNames.ENTRY_ANNOTATION_NAME);
102                scopeInfo.isComponent ||= isAnnotation(anno, CustomComponentNames.COMPONENT_ANNOTATION_NAME);
103                scopeInfo.isReusable ||= isAnnotation(anno, CustomComponentNames.RESUABLE_ANNOTATION_NAME);
104            });
105            this.scopeInfos.push(scopeInfo);
106        }
107        if (arkts.isETSImportDeclaration(node) && !this.isCustomComponentImported) {
108            this.isCustomComponentImported = !!findLocalImport(
109                node,
110                CustomComponentNames.COMPONENT_DEFAULT_IMPORT,
111                CustomComponentNames.COMPONENT_CLASS_NAME
112            );
113        }
114        if (arkts.isETSImportDeclaration(node) && !this.isEntryPointImported) {
115            this.isEntryPointImported = !!findLocalImport(
116                node,
117                EntryWrapperNames.ENTRY_DEFAULT_IMPORT,
118                EntryWrapperNames.ENTRY_POINT_CLASS_NAME
119            );
120        }
121    }
122
123    exit(node: arkts.AstNode) {
124        if (arkts.isStructDeclaration(node) || arkts.isClassDeclaration(node)) {
125            if (!node.definition || !node.definition.ident || this.scopeInfos.length === 0) return;
126            if (this.scopeInfos[this.scopeInfos.length - 1]?.name === node.definition.ident.name) {
127                this.scopeInfos.pop();
128            }
129        }
130    }
131
132    isComponentStruct(): boolean {
133        if (this.scopeInfos.length === 0) return false;
134        const scopeInfo: ScopeInfo = this.scopeInfos[this.scopeInfos.length - 1];
135        return !!scopeInfo.isComponent;
136    }
137
138    createImportDeclaration(): void {
139        const source: arkts.StringLiteral = arkts.factory.create1StringLiteral(
140            this.arkui ?? CustomComponentNames.COMPONENT_DEFAULT_IMPORT
141        );
142        const imported: arkts.Identifier = arkts.factory.createIdentifier(CustomComponentNames.COMPONENT_CLASS_NAME);
143        // Insert this import at the top of the script's statements.
144        if (!this.program) {
145            throw Error('Failed to insert import: Transformer has no program');
146        }
147        factory.createAndInsertImportDeclaration(
148            source,
149            imported,
150            imported,
151            arkts.Es2pandaImportKinds.IMPORT_KINDS_VALUE,
152            this.program
153        );
154    }
155
156    processEtsScript(node: arkts.EtsScript): arkts.EtsScript {
157        if (this.isExternal && this.componentInterfaceCollection.length === 0 && this.entryNames.length === 0) {
158            return node;
159        }
160        let updateStatements: arkts.AstNode[] = [];
161        if (this.componentInterfaceCollection.length > 0) {
162            if (!this.isCustomComponentImported) this.createImportDeclaration();
163            updateStatements.push(...this.componentInterfaceCollection);
164        }
165
166        if (this.entryNames.length > 0) {
167            if (!this.isEntryPointImported) entryFactory.createAndInsertEntryPointImport(this.program);
168            // TODO: normally, we should only have at most one @Entry component in a single file.
169            // probably need to handle error message here.
170            updateStatements.push(...this.entryNames.map(entryFactory.generateEntryWrapper));
171        }
172        if (updateStatements.length > 0) {
173            return arkts.factory.updateEtsScript(node, [...node.statements, ...updateStatements]);
174        }
175        return node;
176    }
177
178    createStaticMethod(definition: arkts.ClassDefinition): arkts.MethodDefinition {
179        const param: arkts.ETSParameterExpression = arkts.factory.createParameterDeclaration(
180            arkts.factory.createIdentifier(
181                CustomComponentNames.OPTIONS,
182                arkts.factory.createTypeReference(
183                    arkts.factory.createTypeReferencePart(
184                        arkts.factory.createIdentifier(getCustomComponentOptionsName(definition.ident!.name))
185                    )
186                )
187            ),
188            undefined
189        );
190
191        const script = arkts.factory.createScriptFunction(
192            arkts.factory.createBlock([arkts.factory.createReturnStatement()]),
193            arkts.FunctionSignature.createFunctionSignature(
194                undefined,
195                [param],
196                arkts.factory.createPrimitiveType(arkts.Es2pandaPrimitiveType.PRIMITIVE_TYPE_VOID),
197                false
198            ),
199            arkts.Es2pandaScriptFunctionFlags.SCRIPT_FUNCTION_FLAGS_METHOD,
200            arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_PUBLIC | arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_STATIC
201        );
202
203        return arkts.factory.createMethodDefinition(
204            arkts.Es2pandaMethodDefinitionKind.METHOD_DEFINITION_KIND_METHOD,
205            arkts.factory.createIdentifier(CustomComponentNames.BUILDCOMPATIBLENODE),
206            script,
207            arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_PUBLIC | arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_STATIC,
208            false
209        );
210    }
211
212    processComponent(
213        node: arkts.ClassDeclaration | arkts.StructDeclaration
214    ): arkts.ClassDeclaration | arkts.StructDeclaration {
215        const scopeInfo = this.scopeInfos[this.scopeInfos.length - 1];
216        const className = node.definition?.ident?.name;
217        if (!className || scopeInfo?.name !== className) {
218            return node;
219        }
220
221        arkts.GlobalInfo.getInfoInstance().add(className);
222
223        if (arkts.isStructDeclaration(node)) {
224            this.collectComponentMembers(node, className);
225        }
226
227        if (scopeInfo.isReusable) {
228            const currentStructInfo: arkts.StructInfo = arkts.GlobalInfo.getInfoInstance().getStructInfo(className);
229            currentStructInfo.isReusable = true;
230            arkts.GlobalInfo.getInfoInstance().setStructInfo(className, currentStructInfo);
231        }
232
233        this.componentInterfaceCollection.push(this.generateComponentInterface(className, node.modifiers));
234
235        const definition: arkts.ClassDefinition = node.definition!;
236        const newDefinitionBody: arkts.AstNode[] = [];
237        if (scopeInfo.isEntry) {
238            this.entryNames.push(className);
239            const entryWithStorage: arkts.ClassProperty | undefined =
240                findEntryWithStorageInClassAnnotations(definition);
241            if (!!entryWithStorage) {
242                newDefinitionBody.push(entryFactory.createEntryLocalStorageInClass(entryWithStorage));
243            }
244        }
245        const newDefinition: arkts.ClassDefinition = this.createNewDefinition(
246            node,
247            className,
248            definition,
249            newDefinitionBody
250        );
251
252        if (arkts.isStructDeclaration(node)) {
253            const _node = arkts.factory.createClassDeclaration(newDefinition);
254            _node.modifiers = node.modifiers;
255            return _node;
256        } else {
257            return arkts.factory.updateClassDeclaration(node, newDefinition);
258        }
259    }
260
261    createNewDefinition(
262        node: arkts.ClassDeclaration | arkts.StructDeclaration,
263        className: string,
264        definition: arkts.ClassDefinition,
265        newDefinitionBody: arkts.AstNode[]
266    ): arkts.ClassDefinition {
267        const staticMethodBody: arkts.AstNode[] = [];
268        const hasExportFlag =
269            (node.modifiers & arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_EXPORT) ===
270            arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_EXPORT;
271        if (hasExportFlag) {
272            const buildCompatibleNode: arkts.MethodDefinition = this.createStaticMethod(definition);
273            if (!!buildCompatibleNode) {
274                staticMethodBody.push(buildCompatibleNode);
275            }
276        }
277        return arkts.factory.updateClassDefinition(
278            definition,
279            definition.ident,
280            undefined,
281            undefined, // superTypeParams doen't work
282            definition.implements,
283            undefined,
284            arkts.factory.createTypeReference(
285                arkts.factory.createTypeReferencePart(
286                    arkts.factory.createIdentifier(CustomComponentNames.COMPONENT_CLASS_NAME),
287                    arkts.factory.createTSTypeParameterInstantiation([
288                        arkts.factory.createTypeReference(
289                            arkts.factory.createTypeReferencePart(arkts.factory.createIdentifier(className))
290                        ),
291                        arkts.factory.createTypeReference(
292                            arkts.factory.createTypeReferencePart(
293                                arkts.factory.createIdentifier(
294                                    `${CustomComponentNames.COMPONENT_INTERFACE_PREFIX}${className}`
295                                )
296                            )
297                        ),
298                    ])
299                )
300            ),
301            [
302                ...newDefinitionBody,
303                ...definition.body.map((st: arkts.AstNode) => factory.PreprocessClassPropertyModifier(st)),
304                ...staticMethodBody,
305            ],
306            definition.modifiers,
307            arkts.classDefinitionFlags(definition) | arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_FINAL
308        );
309    }
310
311    generateComponentInterface(name: string, modifiers: number): arkts.TSInterfaceDeclaration {
312        const interfaceNode = arkts.factory.createInterfaceDeclaration(
313            [],
314            arkts.factory.createIdentifier(getCustomComponentOptionsName(name)),
315            nullptr, // TODO: wtf
316            arkts.factory.createInterfaceBody([...(this.context.structMembers.get(name) || [])]),
317            false,
318            false
319        );
320        interfaceNode.modifiers = modifiers;
321        return interfaceNode;
322    }
323
324    collectComponentMembers(node: arkts.StructDeclaration, className: string): void {
325        const structInfo: arkts.StructInfo = arkts.GlobalInfo.getInfoInstance().getStructInfo(className);
326        if (!this.context.structMembers.has(className)) {
327            this.context.structMembers.set(className, []);
328        }
329        node.definition.body.map((it) => {
330            if (arkts.isClassProperty(it)) {
331                if (hasDecorator(it, DecoratorNames.PROVIDE)) {
332                    factory.processNoAliasProvideVariable(it);
333                }
334                this.context.structMembers.get(className)!.push(...this.createInterfaceInnerMember(it, structInfo));
335            }
336        });
337        arkts.GlobalInfo.getInfoInstance().setStructInfo(className, structInfo);
338    }
339
340    createInterfaceInnerMember(member: arkts.ClassProperty, structInfo: arkts.StructInfo): arkts.ClassProperty[] {
341        const originalName: string = expectName(member.key);
342        const newName: string = backingField(originalName);
343
344        const properties = collectPropertyDecorators(member);
345        const hasStateManagementType = properties.length > 0;
346        updateStructMetadata(structInfo, originalName, properties, member.modifiers, hasStateManagementType);
347
348        const originMember: arkts.ClassProperty = createOptionalClassProperty(
349            originalName,
350            member,
351            '',
352            arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_PUBLIC
353        );
354        if (member.annotations.length > 0 && !hasDecorator(member, DecoratorNames.BUILDER_PARAM)) {
355            const newMember: arkts.ClassProperty = createOptionalClassProperty(
356                newName,
357                member,
358                getStateManagementType(member),
359                arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_PUBLIC
360            );
361            return [originMember, newMember];
362        }
363        if (hasDecorator(member, DecoratorNames.BUILDER_PARAM) && !!originMember.typeAnnotation) {
364            originMember.typeAnnotation.setAnnotations([annotation('memo')]);
365        }
366        return [originMember];
367    }
368
369    registerMap(map: Map<string, StructMap>): void {
370        this.legacyStructMap = map;
371        this.hasLegacy = true;
372    }
373
374    processImport(node: arkts.ETSImportDeclaration): void {
375        const source = node.source?.str!;
376        const specifiers = node.specifiers;
377        if (this.legacyStructMap.has(source)) {
378            const structMap = this.legacyStructMap.get(source);
379            if (!structMap) {
380                return;
381            }
382            for (const specifier of specifiers) {
383                const name = specifier.local.name;
384                if (structMap[name]) {
385                    this.legacyCallMap.set(name, structMap[name]);
386                }
387            }
388        }
389    }
390
391    visitor(node: arkts.AstNode): arkts.AstNode {
392        this.enter(node);
393        const newNode = this.visitEachChild(node);
394        if (arkts.isEtsScript(newNode)) {
395            return this.processEtsScript(newNode);
396        }
397        if (arkts.isStructDeclaration(newNode) && this.isComponentStruct()) {
398            const updateNode = this.processComponent(newNode);
399            this.exit(newNode);
400            return updateNode;
401        }
402        if (!this.hasLegacy) {
403            return newNode;
404        }
405        if (arkts.isETSImportDeclaration(newNode)) {
406            this.processImport(newNode);
407        }
408        if (arkts.isCallExpression(newNode)) {
409            const ident = newNode.expression;
410            if (!(ident instanceof arkts.Identifier)) {
411                return newNode;
412            }
413            const className = ident.name;
414            if (this.legacyCallMap.has(className)) {
415                const path = this.legacyCallMap.get(className)!;
416                // const pathName = 'path/har1';
417                const args = newNode.arguments;
418                const context: InteropContext = {
419                    className: className,
420                    path: path,
421                    arguments: args && args.length === 1 && args[0] instanceof arkts.ObjectExpression
422                      ? args[0]
423                      : undefined
424                };
425                return generateTempCallFunction(context);
426            }
427        }
428        return newNode;
429    }
430}
431