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