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 * as path from 'path' 18import { getDeclarationsByNode, getDecorator, hasDecorator, id, isCallDecorator, isDecorator, prependComment, Void } from './ApiUtils' 19import { Importer } from "./Importer"; 20 21export const ComponentDecorator = "Component" 22export const EntryDecorator = "Entry" 23export const ReusableDecorator = "Reusable" 24export const LocalStoragePropertyName = "_entry_local_storage_" 25 26export const StateDecorator = "State" 27export const PropDecorator = "Prop" 28export const LinkDecorator = "Link" 29export const ObjectLinkDecorator = "ObjectLink" 30export const ProvideDecorator = "Provide" 31export const ConsumeDecorator = "Consume" 32export const StorageLinkDecorator = "StorageLink" 33export const StoragePropDecorator = "StorageProp" 34export const LocalStorageLinkDecorator = "LocalStorageLink" 35export const LocalStoragePropDecorator = "LocalStorageProp" 36export const BuilderParamDecorator = "BuilderParam" 37export const BuilderDecorator = "Builder" 38export const BuilderLambdaDecorator = "BuilderLambda" 39export const WatchDecorator = "Watch" 40export const CustomDialogDecorator = "CustomDialog" 41 42export const StylesDecorator = "Styles" 43export const ExtendDecorator = "Extend" 44export const AnimatableExtendDecorator = "AnimatableExtend" 45export const ArkCommonMethodInterface = "ArkCommonMethodInterface" 46export const ArkCommonMethodComponent = "ArkCommonMethodComponent" 47export const T_TypeParameter = "T" 48export const styledInstance = mangle("instance") 49export const CommonInstance = "CommonInstance" 50export const Instance = "Instance" 51export const DollarDollar = "$$" 52 53export enum SyncedPropertyConstructor { 54 propState = "propState", 55 objectLink = "objectLinkState" 56} 57 58export enum StateImplementation { 59 SyncedProperty = "SyncedProperty", 60 MutableState = "MutableState", 61} 62 63export enum RewriteNames { 64 Initializers = "initializers", 65 InitializeStruct = "__initializeStruct", 66 UpdateStruct = "__updateStruct", 67 ToRecord = "__toRecord", 68 ApplyStyle = "__applyStyle", 69 ApplyAnimatableExtend = "__applyAnimatableExtend" 70} 71 72export class NameTable { 73 structs: string[] = [] 74 dollars: string[] = [] 75} 76 77export class IssueTable { 78 newEtsWithProperty: number[] = [] 79 newEts: number[] = [] 80 builderLambda: number[] = [] 81} 82 83export class CallTable { 84 builderLambdas = new Map<ts.CallExpression, string>() 85 globalBuilderCalls = new Set<ts.CallExpression>() 86 legacyCalls = new Set<ts.CallExpression>() 87 structCalls = new Set<ts.CallExpression>() 88 lazyCalls = new Set<ts.Identifier>() 89} 90 91export function prependMemoComment<T extends ts.Node>(node: T): T { 92 return prependComment(node, "* @memo ") 93} 94 95export function prependMemoStable<T extends ts.Node>(node: T): T { 96 return prependComment(node, "* @memo:stable ") 97} 98 99export function prependMemoCommentIfArkoala<T extends ts.Node>(node: T, importer: Importer): T { 100 return importer.isArkoala() 101 ? prependMemoComment(node) 102 : node 103} 104 105export function prependDoubleLineMemoComment<T extends ts.Node>(node: T): T { 106 return prependComment(prependComment(node, ""), "* @memo ") 107} 108 109export function relativeToSource(sourcePath: string, sourceDir: string): string { 110 return path.relative(sourceDir, sourcePath) 111} 112 113export function emitStartupFile(entryFile: string, sourceDir: string, destinationDir: string) { 114 const absoluteSourceDir = path.resolve(sourceDir) 115 const absoluteEntryFile = path.resolve(entryFile) 116 const relativeEntryFile = path.relative(absoluteSourceDir, absoluteEntryFile) 117 if (!relativeEntryFile.endsWith(".ets")) return 118 119 const importPath = relativeEntryFile.substring(0, relativeEntryFile.length - 4) 120 console.log("IMPORT FROM: " + importPath) 121 122 const mainFile = path.join(destinationDir, "main.ts") 123 console.log("STARTUP: " + mainFile) 124 125} 126 127export function typeParameterExtendsType(name: string, superName: string, superArguments: string[]) { 128 return ts.factory.createTypeParameterDeclaration( 129 undefined, 130 id(name), 131 ts.factory.createTypeReferenceNode( 132 id(superName), 133 superArguments.map( 134 it => ts.factory.createTypeReferenceNode( 135 id(it) 136 ) 137 ) 138 ), 139 undefined 140 ) 141} 142 143export function mangleIfBuild(name: ts.PropertyName): ts.PropertyName { 144 if (!ts.isIdentifier(name)) return name 145 const stringName = ts.idText(name) 146 if (stringName === "build") { 147 return id(mangle("build")) 148 } else { 149 return name 150 } 151} 152 153export function isNamedDeclaration(node: ts.Node): node is ts.NamedDeclaration { 154 return ("name" in node) 155} 156 157/** @returns true if the given node represents an identifier */ 158export function isTextIdentifier(node: ts.Node): node is ts.Identifier | ts.PrivateIdentifier { 159 return ts.isIdentifier(node) || ts.isPrivateIdentifier(node) 160} 161 162/** @returns text identifier from the specified node */ 163export function idTextOrError(node: ts.Node): string { 164 if (isTextIdentifier(node)) return ts.idText(node) 165 throw new Error("Expected an identifier, got: " + ts.SyntaxKind[node.kind]) 166} 167 168export function asString(node: ts.Node | undefined): string { 169 if (node === undefined) return "undefined node" 170 if (isTextIdentifier(node)) return ts.idText(node) 171 if (ts.isEtsComponentExpression(node)) return `EtsComponentExpression(${ts.idText(node.expression as ts.Identifier)}` 172 if (isNamedDeclaration(node)) { 173 if (node.name === undefined) { 174 return `${ts.SyntaxKind[node.kind]}(undefined name)` 175 } else { 176 return `${ts.SyntaxKind[node.kind]}(${asString(node.name)})` 177 } 178 } else { 179 return `${ts.SyntaxKind[node.kind]}` 180 } 181} 182 183export function adaptorName(original: string): string { 184 return `Ark${original}` 185} 186 187export function adaptorComponentName(original: string): string { 188 return `Ark${original}Component` 189} 190 191export function etsAttributeName(original: string): string { 192 const attribute = `${original}Attribute` 193 return attribute.startsWith("Lazy") ? attribute.substring(4) : attribute 194} 195 196export function backingField(originalName: string): string { 197 return mangle(`backing_${originalName}`) 198} 199 200export function backingFieldName(originalName: ts.Identifier | ts.PrivateIdentifier): ts.Identifier { 201 return id(backingField(ts.idText(originalName))) 202} 203 204export function adaptorEtsName(name: ts.Identifier): ts.Identifier { 205 return id(adaptorName(ts.idText(name))) 206} 207 208export function adaptorEtsAttributeName(name: ts.Identifier): ts.Identifier { 209 return id(etsAttributeName(ts.idText(name))) 210} 211 212export function adaptorClassName(name: ts.Identifier): ts.Identifier 213export function adaptorClassName(name: undefined): undefined 214export function adaptorClassName(name: ts.Identifier | undefined): ts.Identifier | undefined 215export function adaptorClassName(name: ts.Identifier | undefined): ts.Identifier | undefined { 216 if (!name) return undefined 217 return id(adaptorComponentName(ts.idText(name))) 218} 219 220export function customDialogImplName(name: ts.Identifier | undefined): ts.Identifier | undefined { 221 if (!name) return undefined 222 return id(ts.idText(name) + "Impl") 223} 224 225export function deduceBuilderLambdaName(functionDeclaration: ts.FunctionDeclaration): string { 226 const decorator = getDecorator(functionDeclaration, BuilderLambdaDecorator) 227 if (!decorator) throw new Error("Expected a decorator") 228 229 if (ts.isCallExpression(decorator.expression)) { 230 const name = decorator.expression.arguments[0]! 231 if (ts.isIdentifier(name)) { 232 return ts.idText(name) 233 } 234 if (ts.isStringLiteral(name)) { 235 return name.text 236 } 237 } 238 throw new Error("Unexpected decorator kind: " + asString(decorator.expression)) 239} 240 241export function findBuilderLambdaRedirect(typechecker: ts.TypeChecker, node: ts.Node): string | undefined { 242 if (!ts.isCallExpression(node)) return undefined 243 const func = node.expression 244 if (!ts.isIdentifier(func)) return undefined 245 246 const declarations = getDeclarationsByNode(typechecker, func) 247 248 if (declarations.length == 0) return undefined 249 const firstDeclaration = declarations[0] 250 if (!firstDeclaration) return undefined 251 if (!isBuilderLambda(firstDeclaration)) return undefined 252 253 return deduceBuilderLambdaName(firstDeclaration as ts.FunctionDeclaration) 254} 255 256export function isBuilderLambdaCall(callTable: CallTable, node: ts.CallExpression): boolean { 257 const originalNode = ts.getOriginalNode(node) as ts.CallExpression 258 return callTable.builderLambdas.has(originalNode) 259} 260 261export function isStructCall(callTable: CallTable, node: ts.CallExpression): boolean { 262 const originalNode = ts.getOriginalNode(node) as ts.CallExpression 263 return callTable.structCalls.has(originalNode) 264} 265 266export function deduceProvideConsumeName(property: ts.PropertyDeclaration, name: string): string { 267 const decorator = getDecorator(property, name) 268 if (!decorator) throw new Error("Expected a decorator") 269 270 // @Provide foo 271 if (ts.isIdentifier(decorator.expression)) { 272 return idTextOrError(property.name) 273 } 274 // @Provide("bar") foo && @Provide({ allowOverride: 'bar'}) bar 275 if (ts.isCallExpression(decorator.expression)) { 276 const arg = decorator.expression.arguments[0]! 277 if (ts.isIdentifier(arg)) { 278 return ts.idText(arg) 279 } 280 if (ts.isStringLiteral(arg)) { 281 return arg.text 282 } 283 // @Provide({ allowOverride: 'bar'}) bar 284 if (ts.isObjectLiteralExpression(arg)) { 285 const propertiesName = (arg as ts.ObjectLiteralExpression).properties 286 .filter( 287 (property) => 288 ts.isPropertyAssignment(property) && 289 property.name.getText() === "allowOverride" 290 ) 291 if (propertiesName.length > 0) { 292 const arg = propertiesName[0] as ts.PropertyAssignment 293 if (ts.isStringLiteral(arg.initializer)) { 294 return arg.initializer.text 295 } 296 } 297 } 298 } 299 300 throw new Error("Unexpected decorator kind: " + asString(decorator.expression)) 301} 302 303export function deduceProvideName(property: ts.PropertyDeclaration) { 304 return deduceProvideConsumeName(property, ProvideDecorator) 305} 306 307export function deduceConsumeName(property: ts.PropertyDeclaration) { 308 return deduceProvideConsumeName(property, ConsumeDecorator) 309} 310 311export function getAnnotationArgument(decorator: ts.Decorator): ts.Identifier | undefined { 312 if (!ts.isCallExpression(decorator.expression)) return undefined 313 const args = decorator.expression.arguments 314 if (args.length > 1) return undefined 315 const argument = args[0] 316 if (!ts.isIdentifier(argument)) return undefined 317 return argument 318} 319 320export function isExtend(decorator: ts.Decorator): boolean { 321 return isCallDecorator(decorator, ExtendDecorator) 322} 323 324export function isState(property: ts.PropertyDeclaration): boolean { 325 return hasDecorator(property, StateDecorator) 326} 327 328export function isLink(property: ts.PropertyDeclaration): boolean { 329 return hasDecorator(property, LinkDecorator) 330} 331 332export function isProp(property: ts.PropertyDeclaration): boolean { 333 return hasDecorator(property, PropDecorator) 334} 335 336export function isObjectLink(property: ts.PropertyDeclaration): boolean { 337 return hasDecorator(property, ObjectLinkDecorator) 338} 339 340export function isConsume(property: ts.PropertyDeclaration): boolean { 341 return hasDecorator(property, ConsumeDecorator) 342} 343 344export function isStorageProp(property: ts.PropertyDeclaration): boolean { 345 return hasDecorator(property, StoragePropDecorator) 346} 347 348export function isStorageLink(property: ts.PropertyDeclaration): boolean { 349 return hasDecorator(property, StorageLinkDecorator) 350} 351 352export function isLocalStorageProp(property: ts.PropertyDeclaration): boolean { 353 return hasDecorator(property, LocalStoragePropDecorator) 354} 355 356export function isLocalStorageLink(property: ts.PropertyDeclaration): boolean { 357 return hasDecorator(property, LocalStorageLinkDecorator) 358} 359 360export function isBuilder(property: ts.MethodDeclaration): boolean { 361 return hasDecorator(property, BuilderDecorator) 362} 363 364export function isBuilderParam(property: ts.PropertyDeclaration): boolean { 365 return hasDecorator(property, BuilderParamDecorator) 366} 367 368export function isProvide(property: ts.PropertyDeclaration): boolean { 369 return hasDecorator(property, ProvideDecorator) 370} 371 372export function dropBuilder(modifierLikes: readonly ts.ModifierLike[] | undefined): ts.ModifierLike[] | undefined { 373 return dropDecorators(modifierLikes, BuilderDecorator) 374} 375 376export function dropStylesLike(modifierLikes: readonly ts.ModifierLike[] | undefined): ts.ModifierLike[] | undefined { 377 return dropDecorators(modifierLikes, StylesDecorator, ExtendDecorator, AnimatableExtendDecorator) 378} 379 380export function dropDecorators(modifierLikes: readonly ts.ModifierLike[] | undefined, ...decorators: (string | undefined)[]): ts.ModifierLike[] | undefined { 381 const defined = decorators.filter((it): it is string => it !== undefined) 382 return modifierLikes?.filter(it => defined.every(decorator => !isDecorator(it, decorator))) 383} 384 385export function filterDecorators(node: ts.Node): readonly ts.Decorator[] | undefined { 386 return ts.getAllDecorators(node) 387} 388 389export function filterModifiers(node: ts.Node): readonly ts.Modifier[] | undefined { 390 if (!ts.canHaveModifiers(node)) { 391 return undefined 392 } 393 return ts.getModifiers(node) 394} 395 396export function contextLocalStateOf(name: string, initializer: ts.Expression, type?: ts.TypeNode): ts.Expression { 397 return ts.factory.createCallExpression( 398 id("contextLocalStateOf"), 399 type ? [type] : undefined, 400 [ 401 ts.factory.createStringLiteral(name), 402 ts.factory.createArrowFunction( 403 undefined, 404 undefined, 405 [], 406 undefined, 407 ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), 408 initializer 409 ) 410 ] 411 ) 412} 413 414export function isGlobalBuilder(node: ts.Node): boolean { 415 return ts.isFunctionDeclaration(node) && hasDecorator(node, BuilderDecorator) 416} 417 418export function isBuilderLambda(node: ts.Node): boolean { 419 return ts.isFunctionDeclaration(node) && hasDecorator(node, BuilderLambdaDecorator) 420} 421 422export function provideConsumeVariableName(decorator: string, prefix: string, property: ts.PropertyDeclaration): ts.Identifier { 423 if (!ts.isIdentifier(property.name) && !ts.isPrivateIdentifier(property.name)) { 424 throw new Error("Expected an identifier") 425 } 426 427 return id(prefix + deduceProvideConsumeName(property, decorator)) 428} 429 430export function consumeVariableName(consume: ts.PropertyDeclaration): ts.Identifier { 431 return provideConsumeVariableName(ConsumeDecorator, "__consume_", consume) 432} 433 434export function provideVariableName(provide: ts.PropertyDeclaration): ts.Identifier { 435 return provideConsumeVariableName(ProvideDecorator, "__provide_", provide) 436} 437 438export function createProvideInitializerField(provide: ts.PropertyDeclaration): ts.PropertyAssignment { 439 return ts.factory.createPropertyAssignment( 440 backingFieldName(provide.name as ts.Identifier), 441 provideVariableName(provide) 442 ) 443} 444 445export function createConsumeInitializerField(consume: ts.PropertyDeclaration): ts.PropertyAssignment { 446 return ts.factory.createPropertyAssignment( 447 backingFieldName(consume.name as ts.Identifier), 448 consumeVariableName(consume) 449 ) 450} 451 452export function filterDecoratedProperties(members: ts.NodeArray<ts.ClassElement>, decoratorName: string): ts.PropertyDeclaration[] { 453 return members.filter(property => ts.isPropertyDeclaration(property) && hasDecorator(property, decoratorName)) as ts.PropertyDeclaration[] 454} 455 456export function filterLinks(members: ts.NodeArray<ts.ClassElement>): ts.PropertyDeclaration[] { 457 return members.filter(it => 458 ts.isPropertyDeclaration(it) && isLink(it) 459 ) as ts.PropertyDeclaration[] 460} 461 462export function filterProps(members: ts.NodeArray<ts.ClassElement>): ts.PropertyDeclaration[] { 463 return members.filter(it => 464 ts.isPropertyDeclaration(it) && isProp(it) 465 ) as ts.PropertyDeclaration[] 466} 467 468export function filterObjectLinks(members: ts.NodeArray<ts.ClassElement>): ts.PropertyDeclaration[] { 469 return members.filter(it => 470 ts.isPropertyDeclaration(it) && isObjectLink(it) 471 ) as ts.PropertyDeclaration[] 472} 473 474export function filterConsumes(members: ts.NodeArray<ts.ClassElement>): ts.PropertyDeclaration[] { 475 return members.filter(it => 476 ts.isPropertyDeclaration(it) && isConsume(it) 477 ) as ts.PropertyDeclaration[] 478} 479 480export function filterProvides(members: ts.NodeArray<ts.ClassElement>): ts.PropertyDeclaration[] { 481 return members.filter(it => 482 ts.isPropertyDeclaration(it) && isProvide(it) 483 ) as ts.PropertyDeclaration[] 484} 485 486/** @returns a nullable accessor: `expression?.name` */ 487export function createNullableAccessor(expression: ts.Expression, name: string): ts.Expression { 488 return ts.factory.createPropertyAccessChain(expression, ts.factory.createToken(ts.SyntaxKind.QuestionDotToken), name) 489} 490 491/** @returns a not-null accessor: `expression!.name!` */ 492export function createNotNullAccessor(expression: ts.Expression, name: string): ts.Expression { 493 return ts.factory.createNonNullExpression(ts.factory.createPropertyAccessExpression(ts.factory.createNonNullExpression(expression), name)) 494} 495 496/** @returns a multiline block with the given statements */ 497export function createBlock(...statements: ts.Statement[]): ts.Block { 498 return ts.factory.createBlock(statements, true) 499} 500 501/** @returns a nullish coalescing expression with safe right part: `left ?? (right)` */ 502export function createNullishCoalescing(left: ts.Expression, right: ts.Expression): ts.Expression { 503 return ts.factory.createBinaryExpression(left, ts.factory.createToken(ts.SyntaxKind.QuestionQuestionToken), ts.factory.createParenthesizedExpression(right)) 504} 505 506export function createValueAccessor(expression: ts.Expression): ts.Expression { 507 return ts.factory.createPropertyAccessExpression(expression, "value") 508} 509 510export function filterDefined<T>(value: (T | undefined)[]): T[] { 511 return value.filter((it: T | undefined): it is T => it != undefined) 512} 513 514export function collect<T>(...value: (ReadonlyArray<T> | T | undefined)[]): T[] { 515 const empty: (T | undefined)[] = [] 516 return filterDefined(empty.concat(...value)) 517} 518 519export function initializers() { 520 return id(RewriteNames.Initializers) 521} 522 523export function isDefined<T>(value: T | undefined | null): value is T { 524 return value !== undefined && value !== null 525} 526 527export function extendsLikeFunctionName(propertyName: string, componentName: string): string { 528 return propertyName + "__" + componentName 529} 530 531export function hasLocalDeclaration(typechecker: ts.TypeChecker, node: ts.Identifier): boolean { 532 const declarations = getDeclarationsByNode(typechecker, node) 533 return (declarations.length == 1 && declarations[0].getSourceFile() == node.getSourceFile()) 534} 535 536export function isBuiltinComponentName(ctx: ts.TransformationContext, name: string) { 537 return ctx.getCompilerOptions().ets?.components.includes(name) 538} 539 540export function voidLambdaType() { 541 return ts.factory.createFunctionTypeNode( 542 undefined, 543 [], 544 Void() 545 ) 546} 547 548export function assertUnreachable(...args: never): never { 549 throw new Error(`never`) 550} 551 552export function throwError(message: string): never { 553 throw new Error(message) 554} 555 556// Produce a name outside of user space 557export function mangle(value: string): string { 558 return `__${value}` 559} 560 561export function buildBuilderArgument(): string { 562 return mangle("builder") 563} 564 565export function commonMethodComponentId(importer: Importer): ts.Identifier { 566 return id(importer.withAdaptorImport(ArkCommonMethodComponent)) 567} 568 569export function commonMethodComponentType(importer: Importer): ts.TypeReferenceNode { 570 return ts.factory.createTypeReferenceNode(importer.withAdaptorImport(ArkCommonMethodComponent)) 571} 572 573export function getSingleExpression(block?: ts.Block): ts.Expression | undefined { 574 const statement = getSingleStatement(block) 575 return statement && ts.isExpressionStatement(statement) ? statement.expression:undefined 576} 577 578export function getSingleStatement(block?: ts.Block): ts.Statement | undefined { 579 return block && block.statements.length === 1 ? block.statements[0] : undefined 580} 581