• 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 ts from '@koalaui/ets-tsc'
17import { AbstractVisitor } from './AbstractVisitor'
18import { CallExpressionCollector } from './CallExpressionCollector'
19import { EntryTracker } from './EntryTracker'
20import { Importer, isOhosImport } from './Importer'
21import {
22    adaptorClassName,
23    adaptorComponentName,
24    adaptorEtsAttributeName,
25    adaptorEtsName,
26    backingField,
27    backingFieldName,
28    buildBuilderArgument,
29    CallTable,
30    collect,
31    commonMethodComponentId,
32    commonMethodComponentType,
33    consumeVariableName,
34    contextLocalStateOf,
35    customDialogImplName,
36    deduceConsumeName,
37    deduceProvideName,
38    dropBuilder,
39    EntryDecorator,
40    filterConsumes,
41    filterDecorators,
42    filterDefined,
43    filterModifiers,
44    filterProvides,
45    getSingleStatement,
46    hasLocalDeclaration,
47    initializers,
48    isBuilderLambdaCall,
49    isBuiltinComponentName,
50    isDefined,
51    isGlobalBuilder,
52    isStructCall,
53    LocalStoragePropertyName,
54    mangleIfBuild,
55    NameTable,
56    prependDoubleLineMemoComment,
57    prependMemoComment,
58    prependMemoStable,
59    provideVariableName,
60    ReusableDecorator,
61    RewriteNames,
62    styledInstance,
63    voidLambdaType,
64    WatchDecorator
65} from './utils'
66import { BuilderParam, classifyProperty, PropertyTranslator, PropertyTranslatorContext } from './PropertyTranslators'
67import {
68    asIdentifier,
69    assignment,
70    createThisFieldAccess,
71    Export,
72    findDecoratorArguments,
73    findDecoratorLiterals,
74    findObjectPropertyValue,
75    getDeclarationsByNode,
76    getDecorator,
77    hasDecorator,
78    id,
79    isKnownIdentifier,
80    isStatic,
81    isUndefined,
82    ObjectType,
83    optionalParameter,
84    orUndefined,
85    parameter,
86    Private,
87    provideAnyTypeIfNone,
88    StringType,
89    undefinedValue,
90    Void
91} from './ApiUtils'
92import { StructOptions } from "./StructOptions"
93import { translateClass } from './ClassMemberTranslators'
94
95export class StructTransformer extends AbstractVisitor {
96    private structOptions: StructOptions
97    private propertyTranslatorContext: PropertyTranslatorContext
98    constructor(
99        sourceFile: ts.SourceFile,
100        ctx: ts.TransformationContext,
101        public typechecker: ts.TypeChecker,
102        public importer: Importer,
103        public nameTable: NameTable,
104        public entryTracker: EntryTracker,
105        public callTable: CallTable,
106        public extras?: ts.TransformerExtras
107    ) {
108        super(sourceFile, ctx)
109        this.importer.addAdaptorImport(adaptorComponentName("PageTransitionEnter"))
110        this.importer.addAdaptorImport(adaptorComponentName("PageTransitionExit"))
111        this.propertyTranslatorContext = new PropertyTranslatorContext(importer, sourceFile, extras)
112        this.structOptions = new StructOptions(this.propertyTranslatorContext, this.typechecker)
113    }
114
115    dropImportEtsExtension(node: ts.ImportDeclaration, oldLiteral: string): ts.ImportDeclaration {
116        if (!oldLiteral.endsWith(".ets")) return node
117        const newLiteral = oldLiteral.substring(0, oldLiteral.length - 4)
118        return ts.factory.updateImportDeclaration(
119            node,
120            node.modifiers,
121            node.importClause,
122            ts.factory.createStringLiteral(newLiteral),
123            undefined
124        )
125    }
126
127    translateImportDeclaration(node: ts.ImportDeclaration): ts.ImportDeclaration {
128        node = this.structOptions.addImport(node)
129        const oldModuleSpecifier = node.moduleSpecifier
130        if (!ts.isStringLiteral(oldModuleSpecifier)) return node
131        const oldLiteral = oldModuleSpecifier.text
132
133        if (isOhosImport(oldLiteral)) {
134            const oldDefaultName = node.importClause?.name ? ts.idText(node.importClause!.name) : ""
135            return this.importer.translateOhosImport(node, oldLiteral, oldDefaultName)
136        }
137
138        return this.dropImportEtsExtension(node, oldLiteral)
139    }
140
141    emitStartApplicationBody(name: string): ts.Block {
142        return ts.factory.createBlock(
143            [ts.factory.createReturnStatement(ts.factory.createCallExpression(
144                id(name),
145                undefined,
146                []
147            ))],
148            true
149        )
150    }
151
152    emitStartApplicationDeclaration(name: string): ts.Statement {
153        const koalaEntry = ts.factory.createFunctionDeclaration(
154            [Export()],
155            undefined,
156            id("KoalaEntry"),
157            undefined,
158            [],
159            undefined,
160            this.emitStartApplicationBody(name)
161        )
162        prependMemoComment(koalaEntry)
163        return koalaEntry
164    }
165
166    entryCode: ts.Statement[] = []
167    entryFile: string | undefined = undefined
168
169    prepareEntryCode(name: string) {
170        this.entryCode = [
171            this.emitStartApplicationDeclaration(name),
172        ]
173        this.entryFile = this.sourceFile.fileName
174    }
175
176    findContextLocalState(name: string, type: ts.TypeNode | undefined): ts.Expression {
177        const state = ts.factory.createCallExpression(
178            id(this.importer.withRuntimeImport("contextLocal")),
179            type ? [type] : undefined,
180            [ts.factory.createStringLiteral(name)]
181        )
182        return ts.factory.createAsExpression(
183            state,
184            ts.factory.createTypeReferenceNode(
185                id("MutableState"),
186                [type!]
187            )
188        )
189    }
190
191    createConsumeState(consume: ts.PropertyDeclaration): ts.Statement {
192        if (!ts.isIdentifier(consume.name) && !ts.isPrivateIdentifier(consume.name)) {
193            throw new Error("Expected an identifier")
194        }
195
196        return ts.factory.createVariableStatement(
197            undefined,
198            ts.factory.createVariableDeclarationList([
199                ts.factory.createVariableDeclaration(
200                    consumeVariableName(consume),
201                    undefined,
202                    undefined,
203                    this.findContextLocalState(deduceConsumeName(consume), consume.type)
204                )
205            ],
206                ts.NodeFlags.Const
207            )
208        )
209    }
210
211    createProvideState(provide: ts.PropertyDeclaration): ts.Statement {
212        this.importer.addAdaptorImport("contextLocalStateOf")
213
214        if (!ts.isIdentifier(provide.name) && !ts.isPrivateIdentifier(provide.name)) {
215            throw new Error("Expected an identifier")
216        }
217        if (!provide.initializer) {
218            throw new Error("Expected an initialization for @Provide " + deduceProvideName(provide))
219        }
220        return ts.factory.createVariableStatement(
221            undefined,
222            ts.factory.createVariableDeclarationList([
223                ts.factory.createVariableDeclaration(
224                    provideVariableName(provide),
225                    undefined,
226                    undefined,
227                    contextLocalStateOf(deduceProvideName(provide), provide.initializer!, provide.type)
228                )
229            ],
230                ts.NodeFlags.Const
231            )
232        )
233    }
234
235    createBuildProlog(
236        node: ts.StructDeclaration,
237        members?: ts.NodeArray<ts.ClassElement>,
238        propertyTranslators?: PropertyTranslator[],
239    ): ts.MethodDeclaration | undefined {
240
241        const propertyInitializationProcessors = propertyTranslators ?
242            filterDefined(propertyTranslators.map(it => it.translateToUpdate())) :
243            undefined
244
245        const watchHandlers = members ? this.translateWatchDecorators(members) : undefined
246
247        if (!propertyInitializationProcessors?.length &&
248            !watchHandlers?.length
249        ) return undefined
250
251        const body = ts.factory.createBlock(
252            collect(
253                propertyInitializationProcessors,
254                watchHandlers,
255            ),
256            true
257        )
258
259        const method = ts.factory.createMethodDeclaration(
260            undefined,
261            undefined,
262            id(RewriteNames.UpdateStruct),
263            undefined,
264            undefined,
265            [
266                parameter(initializers(), orUndefined(this.structOptions.createTypeReference(node)))
267            ],
268            Void(),
269            body
270        )
271
272        return prependMemoComment(method)
273    }
274
275    private createThisMethodCall(name: string | ts.Identifier | ts.PrivateIdentifier, args?: ReadonlyArray<ts.Expression>): ts.Expression {
276        return ts.factory.createCallChain(
277            createThisFieldAccess(name),
278            undefined,
279            undefined,
280            args
281        )
282    }
283
284    private createWatchCall(callName: string, stateName: string): ts.Statement {
285        return ts.factory.createExpressionStatement(this.createThisMethodCall(callName, [ts.factory.createStringLiteral(stateName)]))
286    }
287
288    translateWatchDecorators(members?: ts.NodeArray<ts.ClassElement>): ts.Statement[] {
289        const statements: ts.Statement[] = []
290        if (members && members.length) {
291            for (const property of members) {
292                if (ts.isPropertyDeclaration(property)) {
293                    if (ts.isIdentifier(property.name) || ts.isPrivateIdentifier(property.name)) {
294                        const name = ts.idText(property.name)
295                        const watches = findDecoratorLiterals(filterDecorators(property), WatchDecorator, 0)
296                        if (watches && watches.length) statements.push(
297                            ts.factory.createExpressionStatement(
298                                ts.factory.createCallExpression(
299                                    id(this.importer.withRuntimeImport("OnChange")),
300                                    undefined,
301                                    [
302                                        createThisFieldAccess(name),
303                                        ts.factory.createArrowFunction(
304                                            undefined,
305                                            undefined,
306                                            [parameter("_", property.type)], // Temporary workaround for es2panda
307                                            undefined,
308                                            ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
309                                            watches.length == 1
310                                                ? this.createThisMethodCall(watches[0], [ts.factory.createStringLiteral(name)])
311                                                : ts.factory.createBlock(watches.map(it => this.createWatchCall(it, name)))
312                                        ),
313                                    ]
314                                )
315                            )
316                        )
317                    }
318                }
319            }
320        }
321        return statements
322    }
323
324    propagateStructBuilder(node: ts.Block | undefined): ts.Block | undefined {
325        if (!node) return undefined
326        const singleStatement = getSingleStatement(node)
327        if (!singleStatement || !ts.isExpressionStatement(singleStatement)) return node
328        if (!ts.isCallExpression(singleStatement.expression)) return node
329
330        // TODO: check this is a builtin component call!!
331
332        const callExpression = singleStatement.expression
333        const name = callExpression.expression
334        if (!ts.isIdentifier(name)) return node
335        if (!callExpression.arguments?.[0]) return node
336        const firstArgument = callExpression.arguments[0]
337
338        let newFirstArgument
339        if (isUndefined(firstArgument)) {
340            newFirstArgument = id(buildBuilderArgument())
341        } else if (ts.isArrowFunction(firstArgument)) {
342            const firstArgumentBody = firstArgument.body
343            let componentName = ts.idText(name) + "Component"
344            if (!componentName.startsWith("Ark")) componentName = "Ark" + componentName
345            newFirstArgument = ts.factory.createArrowFunction(
346                undefined,
347                undefined,
348                [
349                    parameter(
350                        styledInstance,
351                        ts.factory.createTypeReferenceNode(componentName)
352                    )
353                ],
354                undefined,
355                ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
356                ts.factory.createBlock(
357                    [
358                        ts.isBlock(firstArgumentBody) ? firstArgumentBody : ts.factory.createExpressionStatement(firstArgumentBody),
359                        ts.factory.createExpressionStatement(
360                            ts.factory.createCallChain(
361                                id(buildBuilderArgument()),
362                                ts.factory.createToken(ts.SyntaxKind.QuestionDotToken),
363                                undefined,
364                                [id(styledInstance)]
365                            )
366                        )
367                    ],
368                    true // multiline
369                )
370            )
371        } else {
372            return node
373        }
374
375        return ts.factory.updateBlock(
376            node,
377            [
378                ts.factory.updateExpressionStatement(
379                    singleStatement,
380                    ts.factory.updateCallExpression(
381                        callExpression,
382                        callExpression.expression,
383                        callExpression.typeArguments,
384                        [
385                            newFirstArgument,
386                            ...callExpression.arguments.slice(1)
387                        ]
388                    )
389                )
390            ]
391        )
392
393    }
394
395    translateBuilder(node: ts.StructDeclaration, propertyTranslators: PropertyTranslator[], member: ts.ClassElement, isMainBuild: boolean): ts.MethodDeclaration {
396        if (!ts.isMethodDeclaration(member)) {
397            throw new Error("Expected member declaration, got: " + ts.SyntaxKind[member.kind])
398        }
399
400        const stateParameters = isMainBuild ? [
401            prependDoubleLineMemoComment(
402                parameter(
403                    buildBuilderArgument(),
404                    orUndefined(
405                        ts.factory.createFunctionTypeNode(
406                            undefined,
407                            [
408                                parameter(
409                                    styledInstance,
410                                    commonMethodComponentType(this.importer),
411                                )
412                            ],
413                            Void()
414                        )
415                    )
416                )
417            ),
418            prependDoubleLineMemoComment(
419                optionalParameter(
420                    "content",
421                    ts.factory.createFunctionTypeNode(
422                        [],
423                        [],
424                        ts.factory.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword)
425                    )
426                )
427            ),
428            optionalParameter(
429                "initializers",
430                this.structOptions.createTypeReference(node)
431            )
432        ] : []
433
434        const newBody = isMainBuild ? this.propagateStructBuilder(member.body) : member.body
435        const newMethod = ts.factory.updateMethodDeclaration(
436            member,
437            dropBuilder(member.modifiers),
438            member.asteriskToken,
439            mangleIfBuild(member.name),
440            member.questionToken,
441            member.typeParameters,
442            [
443                ...stateParameters,
444                ...member.parameters
445            ],
446            member.type,
447            newBody
448        )
449        return prependMemoComment(newMethod)
450    }
451
452    translateGlobalBuilder(func: ts.FunctionDeclaration): ts.FunctionDeclaration {
453        const newFunction = ts.factory.createFunctionDeclaration(
454            dropBuilder(func.modifiers),
455            func.asteriskToken,
456            func.name,
457            func.typeParameters,
458            func.parameters.map(it => provideAnyTypeIfNone(it)),
459            func.type,
460            func.body
461        )
462        return prependMemoComment(newFunction)
463    }
464
465    translateMemberFunction(method: ts.MethodDeclaration): ts.MethodDeclaration {
466        // TODO: nothing for now?
467        return method
468    }
469
470    translateStructMembers(structNode: ts.StructDeclaration, propertyTranslators: PropertyTranslator[]): ts.ClassElement[] {
471        const propertyMembers = propertyTranslators.map(translator =>
472            translator.translateMember()
473        )
474        const updateStruct = this.createBuildProlog(structNode, structNode.members, propertyTranslators)
475        const toRecord = this.allowReusable(structNode)
476            ? this.createToRecordMethod(structNode, propertyTranslators)
477            : undefined
478
479        // The rest of the struct members are translated here directly.
480        const restMembers = structNode.members.map(member => {
481            if (isKnownIdentifier(member.name, "build")) {
482                return this.translateBuilder(structNode, propertyTranslators, member, true)
483            } else if (hasDecorator(member, "Builder")) {
484                return this.translateBuilder(structNode, propertyTranslators, member, false)
485            } else if (isKnownIdentifier(member.name, "pageTransition")) {
486                return prependMemoComment(member)
487            } else if (ts.isMethodDeclaration(member)) {
488                return this.translateMemberFunction(member)
489            } else if (isStatic(member)) {
490                return member
491            } else {
492                return []
493            }
494        }).flat()
495        return collect(
496            ...propertyMembers,
497            updateStruct,
498            toRecord,
499            ...restMembers
500        )
501    }
502
503    translateComponentName(name: ts.Identifier | undefined): ts.Identifier | undefined {
504        if (!name) return undefined
505        // return id(adaptorName(ts.idText(name)))
506        return id(ts.idText(name))
507    }
508
509    /**
510     * @param name - a unique state name
511     * @returns a statement to initialize a context local state with new map
512     */
513    contextLocalStateMap(name: string): ts.Statement {
514        this.importer.addAdaptorImport("contextLocalStateOf")
515        return ts.factory.createExpressionStatement(contextLocalStateOf(name, ts.factory.createNewExpression(
516            id("Map"),
517            [StringType(), ObjectType()],
518            []
519        )))
520    }
521
522    /**
523     * @param source - a node to find named call expressions
524     * @returns an array of statements corresponding to the found expressions
525     */
526    collectContextLocals(source: ts.Node): ts.Statement[] {
527        const statements: ts.Statement[] = []
528        const collector = new CallExpressionCollector(this.sourceFile, this.ctx,
529            "ArkRadio",
530            "ArkCheckbox",
531            "ArkCheckboxGroup",
532        )
533        collector.visitor(source)
534        if (collector.isVisited("ArkRadio")) {
535            statements.push(this.contextLocalStateMap("contextLocalMapOfRadioGroups"))
536        }
537        if (collector.isVisited("ArkCheckbox") || collector.isVisited("ArkCheckboxGroup")) {
538            statements.push(this.contextLocalStateMap("contextLocalMapOfCheckboxGroups"))
539        }
540        return statements
541    }
542
543    topLevelMemoFunctions: ts.FunctionDeclaration[] = []
544    topLevelInitialization: ts.Statement[] = []
545
546    createTopLevelMemo(node: ts.StructDeclaration, impl: boolean): ts.FunctionDeclaration {
547        const className = this.translateComponentName(adaptorClassName(node.name))!
548        const functionName = impl ? customDialogImplName(node.name) : node.name
549
550        const factory = ts.factory.createArrowFunction(
551            undefined,
552            undefined,
553            [],
554            undefined,
555            ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
556            ts.factory.createNewExpression(
557                className,
558                undefined,
559                undefined
560            )
561        )
562
563        const provideVariables = filterProvides(node.members).map(it => this.createProvideState(it))
564        const consumeVariables = filterConsumes(node.members).map(it => this.createConsumeState(it))
565        const contextLocals = this.collectContextLocals(node)
566
567        const additionalStatements = [
568            ...provideVariables,
569            ...consumeVariables,
570            ...contextLocals,
571        ]
572
573        const updatedInitializers = this.structOptions.updatedInitializersValue(
574            node,
575            initializers()
576        )
577
578        const updatedInitializersId = ts.factory.createIdentifier("updatedInitializers")
579        const updatedInitializersDeclaration = ts.factory.createVariableStatement(
580            undefined,
581            ts.factory.createVariableDeclarationList(
582                [ts.factory.createVariableDeclaration(
583                    updatedInitializersId,
584                    undefined,
585                    this.structOptions.createTypeReference(node),
586                    updatedInitializers
587                )],
588                ts.NodeFlags.Const
589            )
590        )
591
592        const argList = [
593            impl ? undefinedValue() : id("style"),
594            factory,
595            impl ? undefinedValue() : id("content"),
596            updatedInitializersId
597        ]
598        if (this.allowReusable(node)) {
599            // pass ClassName as ReuseKey
600            argList.push(ts.factory.createStringLiteral(className.text))
601        }
602        const callInstantiate = ts.factory.createExpressionStatement(
603            ts.factory.createCallExpression(
604                ts.factory.createPropertyAccessExpression(
605                    className,
606                    id("_instantiate")
607                ),
608                undefined,
609                argList
610            )
611        )
612
613        const memoFunction = ts.factory.createFunctionDeclaration(
614            [Export()],
615            undefined,
616            functionName,
617            undefined,
618            impl ? [
619                optionalParameter(
620                    initializers(),
621                    this.structOptions.createTypeReference(node)
622                )
623            ]:
624            [
625                prependDoubleLineMemoComment(
626                    optionalParameter(
627                        "style",
628                        ts.factory.createFunctionTypeNode(
629                            undefined,
630                            [ts.factory.createParameterDeclaration(
631                                undefined,
632                                undefined,
633                                styledInstance,
634                                undefined,
635                                commonMethodComponentType(this.importer),
636                                undefined
637                            )],
638                            ts.factory.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword)
639                        ),
640                    )
641                ),
642                prependDoubleLineMemoComment(
643                    optionalParameter(
644                        "content",
645                        voidLambdaType(),
646                    )
647                ),
648                optionalParameter(
649                    initializers(),
650                    this.structOptions.createTypeReference(node),
651                )
652            ],
653            Void(),
654            ts.factory.createBlock(
655                [
656                    ...additionalStatements,
657                    updatedInitializersDeclaration,
658                    callInstantiate
659                ],
660                true
661            )
662        )
663
664        return prependMemoComment(memoFunction)
665    }
666
667    private createOptionsDeclaration(node: ts.StructDeclaration) {
668        this.topLevelInitialization.push(
669            this.structOptions.createDeclaration(node)
670        )
671    }
672
673    /*
674    Creates something like:
675        export function DialogExample(initializer: any = {}) {
676            return { build: bindCustomDialog(DialogExampleImpl, initializer), buildOptions: initializer };
677        }
678    */
679    createCustomDialogConstructor(node: ts.StructDeclaration) {
680        return ts.factory.createFunctionDeclaration(
681            [Export()],
682            undefined,
683            node.name,
684            undefined,
685            [
686                parameter(
687                    id("initializer"),
688                    this.structOptions.createTypeReference(node),
689                    ts.factory.createObjectLiteralExpression()
690                )
691            ],
692            undefined,
693            ts.factory.createBlock(
694                [
695                    ts.factory.createReturnStatement(
696                        ts.factory.createObjectLiteralExpression([
697                            ts.factory.createPropertyAssignment("build",
698                                ts.factory.createCallExpression(
699                                    id(this.importer.withAdaptorImport("bindCustomDialog")),
700                                    undefined,
701                                    [
702                                        customDialogImplName(node.name)!,
703                                        id("initializer")
704                                    ]
705                                )
706                            ),
707                            ts.factory.createPropertyAssignment("buildOptions",
708                                id("initializer"))
709                        ])
710                    )
711                ],
712                true
713            )
714        )
715
716    }
717
718    createInitializerMethod(
719        node: ts.StructDeclaration,
720        propertyTranslators: PropertyTranslator[]
721    ): ts.MethodDeclaration {
722        const parameters = [
723            prependDoubleLineMemoComment(
724                optionalParameter(
725                    "content",
726                    voidLambdaType(),
727                )
728            ),
729            optionalParameter(
730                initializers(),
731                this.structOptions.createTypeReference(node),
732            )
733        ]
734        const initializations = propertyTranslators
735            .map(it => it.translateToInitialization())
736            .filter(isDefined)
737        const buildParams = propertyTranslators.filter(it => it instanceof BuilderParam)
738        if (buildParams.length > 0) {
739            const field = createThisFieldAccess(backingField(buildParams[0].propertyName))
740            initializations.push(ts.factory.createIfStatement(
741                ts.factory.createBinaryExpression(
742                    ts.factory.createBinaryExpression(
743                        field,
744                        ts.factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken),
745                        undefinedValue()
746                    ),
747                    ts.factory.createToken(ts.SyntaxKind.AmpersandAmpersandToken),
748                    ts.factory.createBinaryExpression(
749                        id("content"),
750                        ts.factory.createToken(ts.SyntaxKind.ExclamationEqualsEqualsToken),
751                        undefinedValue()
752                    ),
753                ),
754                assignment(field, id("content"))
755            ))
756        }
757
758        return ts.factory.createMethodDeclaration(
759            undefined,
760            undefined,
761            RewriteNames.InitializeStruct,
762            undefined,
763            undefined,
764            parameters,
765            Void(),
766            ts.factory.createBlock(initializations, true)
767        )
768    }
769
770    /**
771     * Create __ToRecord override method in ReusableStruct to convert StructOption to Record<string, Object> for aboutToReuse
772     */
773    createToRecordMethod(node: ts.StructDeclaration, propertyTranslators: PropertyTranslator[]): ts.MethodDeclaration {
774        const structType = this.structOptions.createTypeReference(node)
775        const arg = initializers()
776        const argCasted = "_optionData"
777        const objectType = ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('Object'))
778
779        const castArg = ts.factory.createVariableStatement(
780            undefined, // modifiers
781            ts.factory.createVariableDeclarationList(
782                [
783                    ts.factory.createVariableDeclaration(
784                        argCasted, // variable name
785                        undefined, // type annotation
786                        undefined, // type
787                        ts.factory.createAsExpression(arg, structType) // initializer with type assertion
788                    )
789                ],
790                ts.NodeFlags.Const
791            )
792        )
793        const propAssignments = propertyTranslators
794            .map(it => it.translateToRecordEntry(ts.factory.createIdentifier(argCasted)))
795            .filter(isDefined)
796
797        const returnType = ts.factory.createTypeReferenceNode('Record', [ // return type = Record<string, Object>
798            ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
799            objectType
800        ])
801        return ts.factory.createMethodDeclaration(
802            [ts.factory.createModifier(ts.SyntaxKind.OverrideKeyword)],
803            undefined,
804            RewriteNames.ToRecord,
805            undefined,
806            undefined,
807            [parameter(arg, objectType, undefined)], // input = param: Object
808            returnType,
809            ts.factory.createBlock([
810                castArg,
811                ts.factory.createReturnStatement(ts.factory.createObjectLiteralExpression(propAssignments))
812            ], true)
813        )
814    }
815
816    createTopLevelInitialization(node: ts.StructDeclaration): ts.ExpressionStatement {
817        const routerPage = this.entryTracker.sourceFileToRoute(this.sourceFile)
818        return ts.factory.createExpressionStatement(
819            ts.factory.createCallExpression(
820                id(this.importer.withOhosImport("ohos.router", "registerArkuiEntry")),
821                undefined,
822                [
823                    id(ts.idText(node.name!)),
824                    ts.factory.createStringLiteral(routerPage),
825                ]
826            )
827        )
828    }
829
830    createEntryPointAlias(node: ts.StructDeclaration): ts.Statement {
831        return ts.factory.createVariableStatement(
832            [Export()],
833            ts.factory.createVariableDeclarationList(
834                [
835                    ts.factory.createVariableDeclaration(
836                        id("__Entry"),
837                        undefined,
838                        undefined,
839                        id(ts.idText(node.name!)
840                        )
841                    )
842                ],
843                ts.NodeFlags.Const
844            )
845        )
846    }
847
848    entryStorageValue(node: ts.Expression): ts.Expression | undefined {
849        if (ts.isObjectLiteralExpression(node)) {
850            const storage = findObjectPropertyValue(node, "storage")
851            return storage
852        }
853        return node
854    }
855
856    entryStorage(node: ts.Expression): ts.Expression | undefined {
857        const value = this.entryStorageValue(node)
858        if (value === undefined) return undefined
859
860        if (!ts.isStringLiteral(value)) {
861            console.log(`Warning: expected the storage value to be a string literal, got ${ts.SyntaxKind[value.kind]}`)
862            return undefined
863        }
864        return id(value.text)
865    }
866
867    translateStructToClass(node: ts.StructDeclaration): ts.ClassDeclaration {
868        const className = this.translateComponentName(adaptorClassName(node.name)) // TODO make me string for proper reuse
869        const baseClassName = this.allowReusable(node)
870            ? this.importer.withAdaptorImport("ArkReusableStruct")
871            : this.importer.withAdaptorImport("ArkStructBase")
872        this.createOptionsDeclaration(node)
873
874        let entryLocalStorage: ts.Expression | undefined = undefined
875
876        if (hasDecorator(node, "CustomDialog")) {
877            this.topLevelMemoFunctions.push(
878                this.createTopLevelMemo(node, true),
879                this.createCustomDialogConstructor(node)
880            )
881        } else {
882            this.topLevelMemoFunctions.push(
883                this.createTopLevelMemo(node, false)
884            )
885        }
886
887        if (hasDecorator(node, EntryDecorator)) {
888            if (!this.importer.isArkts()) {
889                this.topLevelInitialization.push(
890                    this.createTopLevelInitialization(node),
891                    this.createEntryPointAlias(node)
892                )
893            }
894            const args = findDecoratorArguments(filterDecorators(node), EntryDecorator, 0)
895            switch (args?.length) {
896                case 0:
897                    break
898                case 1:
899                    entryLocalStorage = this.entryStorage(args[0])
900                    break
901                default:
902                    throw new Error("Entry must have only one name, but got " + args?.length)
903            }
904
905            if (!node.name) throw new Error("Expected @Entry struct to have a name")
906            this.entryTracker.addEntry(ts.idText(node.name), this.sourceFile)
907        }
908
909        const inheritance = ts.factory.createHeritageClause(
910            ts.SyntaxKind.ExtendsKeyword,
911            [ts.factory.createExpressionWithTypeArguments(
912                id(baseClassName),
913                [
914                    ts.factory.createTypeReferenceNode(
915                        adaptorComponentName(ts.idText(node.name!)),
916                    ),
917                    this.structOptions.createTypeReference(node)
918                ]
919            )]
920        )
921
922        const entryLocalStorageProperty = ts.factory.createPropertyDeclaration(
923            [Private()],
924            LocalStoragePropertyName,
925            undefined,
926            undefined,
927            entryLocalStorage ?? ts.factory.createNewExpression(
928                id(this.importer.withAdaptorImport("LocalStorage")),
929                undefined,
930                []
931            )
932        )
933
934        const propertyTranslators = filterDefined(
935            node.members.map(it => classifyProperty(it, this.propertyTranslatorContext))
936        )
937
938        const translatedMembers = this.translateStructMembers(node, propertyTranslators)
939
940        const createdClass = ts.factory.createClassDeclaration(
941            filterModifiers(node),
942            className,
943            node.typeParameters,
944            [inheritance],
945            [
946                entryLocalStorageProperty,
947                this.createInitializerMethod(node, propertyTranslators),
948                ...translatedMembers,
949            ]
950        )
951
952        return prependMemoStable(createdClass)
953    }
954
955    findEtsAdaptorName(name: ts.LeftHandSideExpression): ts.LeftHandSideExpression {
956        if (ts.isIdentifier(name)) {
957            const newName = adaptorEtsName(name)
958            this.importer.addAdaptorImport(ts.idText(newName))
959            const attributeName = adaptorEtsAttributeName(name)
960            if (!ts.idText(attributeName).includes("Page")) {
961                this.importer.addAdaptorImport(ts.idText(attributeName))
962            }
963            return newName
964        } else {
965            return name
966        }
967    }
968
969    findEtsAdaptorClassName(name: ts.LeftHandSideExpression): ts.Identifier {
970        if (ts.isIdentifier(name)) {
971            const newName = adaptorClassName(name)
972            this.importer.addAdaptorImport(ts.idText(newName))
973            return newName
974        } else {
975            throw new Error("expected ETS name to be an Identifier, got: " + ts.SyntaxKind[name.kind])
976        }
977    }
978
979    createContentLambda(node: ts.EtsComponentExpression): ts.Expression {
980        if (!node.body?.statements || node.body.statements.length == 0) {
981            return undefinedValue()
982        }
983
984        const contentLambdaBody = ts.factory.createBlock(
985            node.body?.statements,
986            true
987        )
988
989        return ts.factory.createArrowFunction(
990            undefined,
991            undefined,
992            [],
993            undefined,
994            ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
995            contentLambdaBody
996        )
997    }
998
999    createEtsInstanceLambda(node: ts.EtsComponentExpression): ts.Expression {
1000        // Either a lambda or undefined literal.
1001        const instanceArgument = node.arguments[0]
1002
1003        if (isUndefined(instanceArgument)) {
1004            return instanceArgument
1005        }
1006
1007        return this.createInstanceLambda(node, this.findEtsAdaptorClassName(node.expression))
1008    }
1009
1010    createBuilderLambdaInstanceLambda(node: ts.EtsComponentExpression | ts.CallExpression, parameterTypeName?: ts.Identifier): ts.Expression {
1011        // Either a lambda or undefined literal.
1012        const instanceArgument = node.arguments[0]
1013
1014        if (isUndefined(instanceArgument)) {
1015            return instanceArgument
1016        }
1017
1018        return this.createInstanceLambda(node, undefined)
1019    }
1020
1021    createInstanceLambda(node: ts.EtsComponentExpression | ts.CallExpression, parameterTypeName?: ts.Identifier): ts.Expression {
1022        // Either a lambda or undefined literal.
1023        const instanceArgument = node.arguments[0]
1024
1025        if (isUndefined(instanceArgument)) {
1026            return instanceArgument
1027        }
1028
1029        const lambdaParameter = parameter(
1030            styledInstance,
1031            parameterTypeName ? ts.factory.createTypeReferenceNode(parameterTypeName) : undefined
1032        )
1033
1034        const instanceLambdaBody = ts.factory.createBlock(
1035            [
1036                ts.factory.createExpressionStatement(instanceArgument)
1037            ],
1038            true
1039        )
1040
1041        return ts.factory.createArrowFunction(
1042            undefined,
1043            undefined,
1044            [lambdaParameter],
1045            undefined,
1046            ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
1047            instanceLambdaBody
1048        )
1049    }
1050
1051    translateEtsComponent(node: ts.EtsComponentExpression, newName: ts.LeftHandSideExpression): ts.CallExpression {
1052        const newArguments = [
1053            this.createEtsInstanceLambda(node),
1054            this.createContentLambda(node),
1055            ...node.arguments.slice(1)
1056        ]
1057
1058        return ts.factory.createCallExpression(
1059            newName,
1060            node.typeArguments,
1061            newArguments
1062        )
1063    }
1064
1065    translateBuiltinEtsComponent(node: ts.EtsComponentExpression): ts.CallExpression {
1066        const newName = this.findEtsAdaptorName(node.expression)
1067        return this.translateEtsComponent(node, newName)
1068    }
1069
1070    translateUserEtsComponent(node: ts.EtsComponentExpression): ts.CallExpression {
1071        return this.translateEtsComponent(node, node.expression)
1072    }
1073
1074    transformBuilderLambdaCall(node: ts.CallExpression): ts.CallExpression {
1075        const originalCall = ts.getOriginalNode(node) as ts.CallExpression
1076        const newName = this.callTable.builderLambdas.get(originalCall)
1077        if (!newName) return node
1078
1079        const newArguments = [
1080            this.createBuilderLambdaInstanceLambda(node),
1081            ...node.arguments.slice(1)
1082        ]
1083
1084        return ts.factory.updateCallExpression(
1085            node,
1086            id(newName),
1087            node.typeArguments,
1088            newArguments
1089        )
1090    }
1091
1092    addCastToInitializer(node: ts.CallExpression, originalInitializer: ts.Expression): ts.Expression | undefined {
1093        if (!originalInitializer) return undefined
1094        if (isUndefined(originalInitializer)) return originalInitializer
1095
1096        return ts.factory.createAsExpression(
1097            originalInitializer,
1098            this.structOptions.createTypeReference(
1099                asIdentifier(node.expression)
1100            )
1101        )
1102    }
1103
1104    transformStructCall(node: ts.CallExpression): ts.CallExpression {
1105        let initializer = this.addCastToInitializer(node, node.arguments[1])
1106
1107        const newArguments = [
1108            this.createInstanceLambda(node, commonMethodComponentId(this.importer)),
1109            undefinedValue(),
1110            initializer
1111        ].filter(isDefined)
1112
1113        return ts.factory.updateCallExpression(
1114            node,
1115            node.expression,
1116            node.typeArguments,
1117            newArguments
1118        )
1119    }
1120
1121    // This is a heuristics to understand if the given property call
1122    // is a style setting call.
1123    isStyleSettingMethodCall(node: ts.CallExpression): boolean {
1124        const property = node.expression
1125        if (!property || !ts.isPropertyAccessExpression(property)) return false
1126        const name = property.name
1127        if (!name || !ts.isIdentifier(name)) return false
1128
1129        const declarations = getDeclarationsByNode(this.typechecker, name)
1130
1131        // TODO: handle multiple declarations
1132        const declaration = declarations[0]
1133
1134        if (!declaration || !ts.isMethodDeclaration(declaration)) return false
1135        const returnType = declaration.type
1136        if (!returnType || !ts.isTypeReferenceNode(returnType)) return false
1137        const returnTypeName = returnType.typeName
1138        if (!returnTypeName || !ts.isIdentifier(returnTypeName)) return false
1139        const parent = declaration.parent
1140        if (!parent || !ts.isClassDeclaration(parent)) return false
1141        const parentName = parent.name
1142        if (!parentName || !ts.isIdentifier(parentName)) return false
1143        const parentNameString = ts.idText(parentName)
1144
1145        const ohosDeclaredClass =
1146            parentNameString.endsWith("Attribute") ||
1147            parentNameString == "CommonMethod"
1148
1149        return ohosDeclaredClass
1150    }
1151
1152    // TODO: Somehow eTS compiler produces style setting methods with a type parameter.
1153    fixEmptyTypeArgs(node: ts.CallExpression): ts.CallExpression {
1154        if (this.isStyleSettingMethodCall(node)) {
1155            return ts.factory.updateCallExpression(node, node.expression, undefined, node.arguments)
1156        }
1157        return node
1158    }
1159
1160    importIfEnum(node: ts.PropertyAccessExpression): ts.PropertyAccessExpression {
1161        const name = node.expression
1162        if (!ts.isIdentifier(name)) return node
1163        const receiverDeclarations = getDeclarationsByNode(this.typechecker, node.expression)
1164        const anyDeclaration = receiverDeclarations[0]
1165        if (anyDeclaration && ts.isEnumDeclaration(anyDeclaration)) {
1166            this.importer.addAdaptorImport(ts.idText(name))
1167        }
1168
1169        // Just return the node itself.
1170        return node
1171    }
1172
1173    appendTopLevelMemoFunctions(file: ts.SourceFile): ts.SourceFile {
1174        return ts.factory.updateSourceFile(file,
1175            [...file.statements, ...this.topLevelMemoFunctions, ...this.topLevelInitialization],
1176            file.isDeclarationFile,
1177            file.referencedFiles,
1178            file.typeReferenceDirectives,
1179            file.hasNoDefaultLib,
1180            file.libReferenceDirectives
1181        )
1182    }
1183
1184    isDollarFieldAccess(node: ts.Expression): boolean {
1185        if (!ts.isPropertyAccessExpression(node)) return false
1186        const name = node.name
1187        if (!name) return false
1188        if (!ts.isIdentifier(name)) return false
1189
1190        const receiver = node.expression
1191        if (!receiver) return false
1192        if (receiver.kind != ts.SyntaxKind.ThisKeyword) return false
1193
1194        const nameString = ts.idText(name)
1195        return nameString.startsWith("$")
1196    }
1197
1198    translateDollarFieldAccess(node: ts.PropertyAccessExpression): ts.PropertyAccessExpression {
1199        return ts.factory.createPropertyAccessExpression(
1200            node.expression,
1201            backingField(ts.idText(node.name).substring(1))
1202        )
1203    }
1204
1205    isDollarFieldAssignment(node: ts.PropertyAssignment): boolean {
1206        if (!ts.isPropertyAccessExpression(node.initializer)) return false
1207        return this.isDollarFieldAccess(node.initializer)
1208    }
1209
1210    translateDollarFieldAssignment(node: ts.PropertyAssignment): ts.PropertyAssignment {
1211        if (!ts.isIdentifier(node.name)) return node
1212
1213        const initializer = node.initializer
1214        if (this.isDollarFieldAccess(initializer)) {
1215            const newInitializer = this.translateDollarFieldAccess(initializer as ts.PropertyAccessExpression)
1216            return ts.factory.createPropertyAssignment(backingFieldName(node.name), newInitializer)
1217        }
1218
1219        return node
1220    }
1221
1222    isUserEts(node: ts.EtsComponentExpression): boolean {
1223        const nameId = node.expression as ts.Identifier
1224        const name = ts.idText(nameId)
1225
1226        // Special handling for synthetic names
1227        if (this.callTable.lazyCalls.has(nameId)) return false
1228
1229        if (isBuiltinComponentName(this.ctx, name) &&
1230            !hasLocalDeclaration(this.typechecker, nameId)
1231        ) return false
1232
1233        return true
1234    }
1235
1236    allowReusable(node: ts.StructDeclaration) : boolean {
1237        return this.importer.isArkts() && hasDecorator(node, ReusableDecorator)
1238    }
1239
1240    visitor(beforeChildren: ts.Node): ts.Node {
1241        const node = this.visitEachChild(beforeChildren)
1242        if (ts.isStructDeclaration(node)) {
1243            return this.translateStructToClass(node)
1244        } else if (ts.isClassDeclaration(node)) {
1245            return translateClass(node, this.propertyTranslatorContext)
1246        } else if (isGlobalBuilder(node)) {
1247            return this.translateGlobalBuilder(node as ts.FunctionDeclaration)
1248        } else if (ts.isEtsComponentExpression(node)) {
1249            if (this.isUserEts(node)) {
1250                return this.translateUserEtsComponent(node)
1251            } else {
1252                return this.translateBuiltinEtsComponent(node)
1253            }
1254        } else if (ts.isImportDeclaration(node)) {
1255            const newNode = this.translateImportDeclaration(node)
1256            return this.visitEachChild(newNode)
1257        } else if (ts.isCallExpression(node) && isBuilderLambdaCall(this.callTable, node)) {
1258            return this.transformBuilderLambdaCall(node as ts.CallExpression)
1259        } else if (ts.isCallExpression(node) && isStructCall(this.callTable, node)) {
1260            return this.transformStructCall(node)
1261        } else if (ts.isCallExpression(node)) {
1262            return this.fixEmptyTypeArgs(node)
1263        } else if (ts.isPropertyAssignment(node) && this.isDollarFieldAssignment(node)) {
1264            return this.translateDollarFieldAssignment(node)
1265        } else if (ts.isSourceFile(node)) {
1266            return this.appendTopLevelMemoFunctions(node)
1267        }
1268        return node
1269    }
1270}
1271