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 */ 15import * as arkts from '@koalaui/libarkts'; 16import { getCommonPath } from '../path'; 17const common = require(getCommonPath()); 18const UniqueId = common.UniqueId; 19 20export enum RuntimeNames { 21 __CONTEXT = '__context', 22 __ID = '__id', 23 __KEY = '__key', 24 ANNOTATION_BUILDER = 'Builder', 25 ANNOTATION = 'memo', 26 ANNOTATION_ENTRY = 'memo_entry', 27 ANNOTATION_INTRINSIC = 'memo_intrinsic', 28 ANNOTATION_STABLE = 'memo_stable', 29 ANNOTATION_SKIP = 'memo_skip', 30 COMPUTE = 'compute', 31 CONTEXT = '__memo_context', 32 CONTEXT_TYPE = '__memo_context_type', 33 MEMO_IMPORT_NAME = 'arkui.stateManagement.runtime', 34 GENSYM = 'gensym%%_', 35 ID = '__memo_id', 36 ID_TYPE = '__memo_id_type', 37 INTERNAL_PARAMETER_STATE = 'param', 38 INTERNAL_SCOPE = 'scope', 39 INTERNAL_VALUE = 'cached', 40 INTERNAL_VALUE_NEW = 'recache', 41 INTERNAL_VALUE_OK = 'unchanged', 42 PARAMETER = '__memo_parameter', 43 SCOPE = '__memo_scope', 44 THIS = 'this', 45 VALUE = 'value', 46 EQUAL_T = '=t', 47} 48 49export interface ReturnTypeInfo { 50 node: arkts.TypeNode | undefined; 51 isMemo?: boolean; 52 isVoid?: boolean; 53 isStableThis?: boolean; 54} 55 56export interface ParamInfo { 57 ident: arkts.Identifier; 58 param: arkts.ETSParameterExpression; 59} 60 61export interface MemoInfo { 62 name?: string; 63 isMemo: boolean; 64} 65 66function baseName(path: string): string { 67 return path.replace(/^.*\/(.*)$/, '$1'); 68} 69 70export class PositionalIdTracker { 71 // Global for the whole program. 72 static callCount: number = 0; 73 74 // Set `stable` to true if you want to have more predictable values. 75 // For example for tests. 76 // Don't use it in production! 77 constructor(public filename: string, public stableForTests: boolean = false) { 78 if (stableForTests) PositionalIdTracker.callCount = 0; 79 } 80 81 sha1Id(callName: string, fileName: string): string { 82 const uniqId = new UniqueId(); 83 uniqId.addString('memo call uniqid'); 84 uniqId.addString(fileName); 85 uniqId.addString(callName); 86 uniqId.addI32(PositionalIdTracker.callCount++); 87 return uniqId.compute().substring(0, 7); 88 } 89 90 stringId(callName: string, fileName: string): string { 91 return `${PositionalIdTracker.callCount++}_${callName}_id_DIRNAME/${fileName}`; 92 } 93 94 id(callName: string = ''): arkts.NumberLiteral | arkts.StringLiteral { 95 const fileName = this.stableForTests ? baseName(this.filename) : this.filename; 96 97 const positionId = this.stableForTests ? this.stringId(callName, fileName) : this.sha1Id(callName, fileName); 98 99 return this.stableForTests 100 ? arkts.factory.createStringLiteral(positionId) 101 : arkts.factory.createNumericLiteral(parseInt(positionId, 16)); 102 } 103} 104 105export function isMemoAnnotation(node: arkts.AnnotationUsage, memoName: RuntimeNames): boolean { 106 return node.expr !== undefined && arkts.isIdentifier(node.expr) && node.expr.name === memoName; 107} 108 109export type MemoAstNode = 110 | arkts.ScriptFunction 111 | arkts.ETSParameterExpression 112 | arkts.ClassProperty 113 | arkts.TSTypeAliasDeclaration 114 | arkts.ETSFunctionType 115 | arkts.ArrowFunctionExpression 116 | arkts.ETSTypeReference 117 | arkts.VariableDeclaration; 118 119export function hasMemoAnnotation<T extends MemoAstNode>(node: T): boolean { 120 return node.annotations.some( 121 (it) => isMemoAnnotation(it, RuntimeNames.ANNOTATION) || isMemoAnnotation(it, RuntimeNames.ANNOTATION_BUILDER) 122 ); 123} 124 125export function hasMemoIntrinsicAnnotation<T extends MemoAstNode>(node: T): boolean { 126 return node.annotations.some((it) => isMemoAnnotation(it, RuntimeNames.ANNOTATION_INTRINSIC)); 127} 128 129export function hasMemoEntryAnnotation<T extends MemoAstNode>(node: T): boolean { 130 return node.annotations.some((it) => isMemoAnnotation(it, RuntimeNames.ANNOTATION_ENTRY)); 131} 132 133export function hasMemoStableAnnotation(node: arkts.ClassDefinition): boolean { 134 return node.annotations.some((it) => isMemoAnnotation(it, RuntimeNames.ANNOTATION_STABLE)); 135} 136 137export function hasMemoSkipAnnotation(node: arkts.ETSParameterExpression): boolean { 138 return node.annotations.some((it) => isMemoAnnotation(it, RuntimeNames.ANNOTATION_SKIP)); 139} 140 141export function removeMemoAnnotation<T extends MemoAstNode>(node: T): T { 142 const newAnnotations: arkts.AnnotationUsage[] = node.annotations.filter( 143 (it) => !isMemoAnnotation(it, RuntimeNames.ANNOTATION) && !isMemoAnnotation(it, RuntimeNames.ANNOTATION_STABLE) 144 ); 145 if (arkts.isEtsParameterExpression(node)) { 146 node.annotations = newAnnotations; 147 return node; 148 } 149 return node.setAnnotations(newAnnotations) as T; 150} 151 152/** 153 * TODO: 154 * @deprecated 155 */ 156export function isSyntheticReturnStatement(node: arkts.AstNode): boolean { 157 return isIfStatementWithSyntheticReturn(node) || isSimpleSyntheticReturn(node) || isSyntheticReturnInBlock(node); 158} 159 160function isIfStatementWithSyntheticReturn(node: arkts.AstNode): boolean { 161 return ( 162 arkts.isIfStatement(node) && 163 !!node.test && 164 arkts.isMemberExpression(node.test) && 165 arkts.isIdentifier(node.test.object) && 166 node.test.object.name === RuntimeNames.SCOPE && 167 arkts.isIdentifier(node.test.property) && 168 node.test.property.name === RuntimeNames.INTERNAL_VALUE_OK && 169 (arkts.isBlockStatement(node.consequent) || arkts.isReturnStatement(node.consequent)) 170 ); 171} 172 173function isSimpleSyntheticReturn(node: arkts.AstNode): boolean { 174 return ( 175 arkts.isReturnStatement(node) && 176 !!node.argument && 177 arkts.isMemberExpression(node.argument) && 178 arkts.isIdentifier(node.argument.object) && 179 node.argument.object.name === RuntimeNames.SCOPE && 180 arkts.isIdentifier(node.argument.property) && 181 node.argument.property.name === RuntimeNames.INTERNAL_VALUE 182 ); 183} 184 185function isSyntheticReturnInBlock(node: arkts.AstNode): boolean { 186 if (!arkts.isBlockStatement(node) || node.statements.length !== 2) { 187 return false; 188 } 189 if (!arkts.isReturnStatement(node.statements[1])) { 190 return false; 191 } 192 const isReturnThis: boolean = !!node.statements[1].argument && arkts.isThisExpression(node.statements[1].argument); 193 const isReturnVoid: boolean = node.statements[1].argument === undefined; 194 195 return ( 196 arkts.isMemberExpression(node.statements[0]) && 197 arkts.isIdentifier(node.statements[0].object) && 198 node.statements[0].object.name === RuntimeNames.SCOPE && 199 arkts.isIdentifier(node.statements[0].property) && 200 node.statements[0].property.name === RuntimeNames.INTERNAL_VALUE && 201 (isReturnThis || isReturnVoid) 202 ); 203} 204 205/** 206 * TODO: 207 * @deprecated 208 */ 209export function isMemoParametersDeclaration(node: arkts.AstNode): boolean { 210 return ( 211 arkts.isVariableDeclaration(node) && 212 node.declarators.every((it) => it.name.name.startsWith(RuntimeNames.PARAMETER)) 213 ); 214} 215 216/** 217 * TODO: change this to TypeNodeGetType to check void type 218 */ 219export function isVoidType(typeNode: arkts.TypeNode | undefined): boolean { 220 return typeNode?.dumpSrc() === 'void'; 221} 222 223/** 224 * es2panda API is weird here 225 * 226 * @deprecated 227 */ 228export function castParameters(params: readonly arkts.Expression[]): arkts.ETSParameterExpression[] { 229 return params as arkts.ETSParameterExpression[]; 230} 231 232/** 233 * es2panda API is weird here 234 * 235 * @deprecated 236 */ 237export function castFunctionExpression(value: arkts.Expression | undefined): arkts.FunctionExpression { 238 return value as unknown as arkts.FunctionExpression; 239} 240 241/** 242 * es2panda API is weird here 243 * 244 * @deprecated 245 */ 246export function castArrowFunctionExpression(value: arkts.Expression | undefined): arkts.ArrowFunctionExpression { 247 return value as unknown as arkts.ArrowFunctionExpression; 248} 249 250/** 251 * es2panda API is weird here 252 * 253 * @deprecated 254 */ 255export function castIdentifier(value: arkts.AstNode | undefined): arkts.Identifier { 256 return value as unknown as arkts.Identifier; 257} 258 259/** 260 * es2panda API is weird here 261 * 262 * @deprecated 263 */ 264export function castOverloadsToMethods(overloads: arkts.AstNode[]): readonly arkts.MethodDefinition[] { 265 return overloads as unknown as readonly arkts.MethodDefinition[]; 266} 267 268export function isStandaloneArrowFunction(node: arkts.AstNode): node is arkts.ArrowFunctionExpression { 269 if (!arkts.isArrowFunctionExpression(node)) return false; 270 271 // handling anonymous arrow function call 272 if (!!node.parent && arkts.isCallExpression(node.parent) && node.parent.expression.peer === node.peer) { 273 return true; 274 } 275 276 return ( 277 !!node.parent && 278 !arkts.isVariableDeclarator(node.parent) && 279 !arkts.isClassProperty(node.parent) && 280 !(arkts.isCallExpression(node.parent) && node.parent.expression) 281 ); 282} 283 284export function isFunctionProperty(node: arkts.AstNode): node is arkts.Property { 285 return ( 286 arkts.isProperty(node) && 287 !!node.key && 288 arkts.isIdentifier(node.key) && 289 !!node.value && 290 arkts.isArrowFunctionExpression(node.value) 291 ); 292} 293 294export function isThisAttributeAssignment(node: arkts.AstNode): node is arkts.AssignmentExpression { 295 if (!arkts.isAssignmentExpression(node)) { 296 return false; 297 } 298 if (!node.left || !node.right) { 299 return false; 300 } 301 const isAssignOperator = node.operatorType === arkts.Es2pandaTokenType.TOKEN_TYPE_PUNCTUATOR_SUBSTITUTION; 302 const isThisAttribute = 303 arkts.isMemberExpression(node.left) && 304 arkts.isThisExpression(node.left.object) && 305 arkts.isIdentifier(node.left.property); 306 return isAssignOperator && isThisAttribute; 307} 308 309export function findThisAttribute(node: arkts.AstNode): arkts.Identifier | undefined { 310 if (!arkts.isMemberExpression(node) || !arkts.isIdentifier(node.property)) { 311 return undefined; 312 } 313 return node.property; 314} 315 316export function isMemoThisAttribute(node: arkts.Identifier, value: arkts.ArrowFunctionExpression): boolean { 317 let isMemo: boolean = isMemoArrowFunction(value); 318 if (isMemo) { 319 return true; 320 } 321 const decl: arkts.AstNode | undefined = getDeclResolveAlias(node); 322 if (!decl) { 323 return false; 324 } 325 if (arkts.isClassProperty(decl)) { 326 isMemo ||= isMemoClassProperty(decl); 327 } else if (arkts.isMethodDefinition(decl)) { 328 isMemo ||= isMemoDeclaredMethod(decl); 329 } 330 return isMemo; 331} 332 333export function isMemoClassProperty(node: arkts.ClassProperty): boolean { 334 let isMemo = findMemoFromTypeAnnotation(node.typeAnnotation); 335 if (node.value) { 336 isMemo ||= 337 arkts.isArrowFunctionExpression(node.value) && 338 (hasMemoAnnotation(node.value) || hasMemoIntrinsicAnnotation(node.value)); 339 } 340 isMemo ||= hasMemoAnnotation(node) || hasMemoIntrinsicAnnotation(node); 341 return isMemo; 342} 343 344export function isMemoMethodDefinition(node: arkts.MethodDefinition): boolean { 345 return ( 346 hasMemoAnnotation(node.scriptFunction) || 347 hasMemoIntrinsicAnnotation(node.scriptFunction) || 348 hasMemoEntryAnnotation(node.scriptFunction) 349 ); 350} 351 352export function isMemoArrowFunction(node: arkts.ArrowFunctionExpression): boolean { 353 return hasMemoAnnotation(node) || hasMemoIntrinsicAnnotation(node); 354} 355 356export function isMemoTSTypeAliasDeclaration(node: arkts.TSTypeAliasDeclaration): boolean { 357 let isMemo = findMemoFromTypeAnnotation(node.typeAnnotation); 358 isMemo ||= hasMemoAnnotation(node) || hasMemoIntrinsicAnnotation(node); 359 return isMemo; 360} 361 362export function isMemoETSParameterExpression(param: arkts.ETSParameterExpression): boolean { 363 const type = param.identifier.typeAnnotation; 364 if (!type) { 365 return false; 366 } 367 let isMemo: boolean = hasMemoAnnotation(param) || hasMemoIntrinsicAnnotation(param); 368 isMemo ||= findMemoFromTypeAnnotation(type); 369 let decl: arkts.AstNode | undefined; 370 if ( 371 arkts.isETSTypeReference(type) && 372 !!type.part && 373 !!type.part.name && 374 !!(decl = getDeclResolveAlias(type.part.name)) 375 ) { 376 if (arkts.isTSTypeAliasDeclaration(decl)) { 377 isMemo ||= hasMemoAnnotation(decl) || hasMemoIntrinsicAnnotation(decl); 378 isMemo ||= findMemoFromTypeAnnotation(decl.typeAnnotation); 379 return isMemo; 380 } 381 } 382 return isMemo; 383} 384 385export function isMemoVariableDeclaration(node: arkts.VariableDeclaration): boolean { 386 return hasMemoAnnotation(node) || hasMemoIntrinsicAnnotation(node); 387} 388 389export function isMemoVariableDeclarator(node: arkts.VariableDeclarator): boolean { 390 let isMemo: boolean = false; 391 if (!!node.name.typeAnnotation) { 392 isMemo ||= findMemoFromTypeAnnotation(node.name.typeAnnotation); 393 } 394 if (!!node.initializer && arkts.isArrowFunctionExpression(node.initializer)) { 395 isMemo ||= isMemoArrowFunction(node.initializer); 396 } 397 if (!!node.parent && arkts.isVariableDeclaration(node.parent)) { 398 isMemo ||= isMemoVariableDeclaration(node.parent); 399 } 400 return isMemo; 401} 402 403export function isMemoProperty(node: arkts.Property, value: arkts.ArrowFunctionExpression): boolean { 404 let isMemo: boolean = isMemoArrowFunction(value); 405 if (isMemo) { 406 return true; 407 } 408 let decl: arkts.AstNode | undefined; 409 if (!node.key || !(decl = getDeclResolveAlias(node.key))) { 410 return false; 411 } 412 if (arkts.isMethodDefinition(decl)) { 413 isMemo ||= isMemoDeclaredMethod(decl); 414 } 415 return isMemo; 416} 417 418export function isMemoDeclaredMethod(decl: arkts.MethodDefinition): boolean { 419 if ( 420 decl.kind === arkts.Es2pandaMethodDefinitionKind.METHOD_DEFINITION_KIND_GET && 421 findMemoFromTypeAnnotation(decl.scriptFunction.returnTypeAnnotation) 422 ) { 423 return true; 424 } 425 return !hasMemoEntryAnnotation(decl.scriptFunction) && isMemoMethodDefinition(decl); 426} 427 428export function isDeclaredMethodWithMemoParams(decl: arkts.MethodDefinition): boolean { 429 if (decl.kind === arkts.Es2pandaMethodDefinitionKind.METHOD_DEFINITION_KIND_GET) { 430 return false; 431 } 432 return decl.scriptFunction.params.some((param) => { 433 return arkts.isEtsParameterExpression(param) && isMemoETSParameterExpression(param); 434 }); 435} 436 437export function isMemoDeclaredIdentifier(decl: arkts.Identifier): boolean { 438 if (findMemoFromTypeAnnotation(decl.typeAnnotation)) { 439 return true; 440 } 441 if (!!decl.parent && arkts.isVariableDeclarator(decl.parent)) { 442 return isMemoVariableDeclarator(decl.parent); 443 } 444 return false; 445} 446 447export function isMemoDeclaredClassProperty(decl: arkts.ClassProperty): boolean { 448 return isMemoClassProperty(decl); 449} 450 451export function findMemoFromTypeAnnotation(typeAnnotation: arkts.AstNode | undefined): boolean { 452 if (!typeAnnotation) { 453 return false; 454 } 455 if (arkts.isETSTypeReference(typeAnnotation) && !!typeAnnotation.part && !!typeAnnotation.part.name) { 456 let decl: arkts.AstNode | undefined = arkts.getDecl(typeAnnotation.part.name); 457 if (!decl || !arkts.isTSTypeAliasDeclaration(decl)) { 458 return false; 459 } 460 let isMemo: boolean = hasMemoAnnotation(decl) || hasMemoIntrinsicAnnotation(decl); 461 if (!isMemo && !!decl.typeAnnotation) { 462 isMemo = findMemoFromTypeAnnotation(decl.typeAnnotation); 463 } 464 return isMemo; 465 } else if (arkts.isETSFunctionType(typeAnnotation)) { 466 return hasMemoAnnotation(typeAnnotation) || hasMemoIntrinsicAnnotation(typeAnnotation); 467 } else if (arkts.isETSUnionType(typeAnnotation)) { 468 return typeAnnotation.types.some( 469 (type) => arkts.isETSFunctionType(type) && (hasMemoAnnotation(type) || hasMemoIntrinsicAnnotation(type)) 470 ); 471 } 472 return false; 473} 474 475export function findReturnTypeFromTypeAnnotation( 476 typeAnnotation: arkts.AstNode | undefined 477): arkts.TypeNode | undefined { 478 if (!typeAnnotation) { 479 return undefined; 480 } 481 if (arkts.isETSTypeReference(typeAnnotation) && !!typeAnnotation.part && !!typeAnnotation.part.name) { 482 let decl: arkts.AstNode | undefined = arkts.getDecl(typeAnnotation.part.name); 483 if (!!decl && arkts.isTSTypeAliasDeclaration(decl)) { 484 return findReturnTypeFromTypeAnnotation(decl.typeAnnotation); 485 } 486 return undefined; 487 } 488 if (arkts.isETSFunctionType(typeAnnotation)) { 489 return typeAnnotation.returnType; 490 } 491 if (arkts.isETSUnionType(typeAnnotation)) { 492 return typeAnnotation.types.find((type) => arkts.isETSFunctionType(type))?.returnType; 493 } 494 return undefined; 495} 496 497export function getDeclResolveAlias(node: arkts.AstNode): arkts.AstNode | undefined { 498 const decl = arkts.getDecl(node); 499 if (!!decl && !!decl.parent && arkts.isIdentifier(decl) && arkts.isVariableDeclarator(decl.parent)) { 500 if (!!decl.parent.initializer && arkts.isIdentifier(decl.parent.initializer)) { 501 return getDeclResolveAlias(decl.parent.initializer); 502 } 503 if (!!decl.parent.initializer && arkts.isMemberExpression(decl.parent.initializer)) { 504 return getDeclResolveAlias(decl.parent.initializer.property); 505 } 506 } 507 return decl; 508} 509 510export function mayAddLastReturn(node: arkts.BlockStatement): boolean { 511 return ( 512 node.statements.length === 0 || 513 (!arkts.isReturnStatement(node.statements[node.statements.length - 1]) && 514 !arkts.isThrowStatement(node.statements[node.statements.length - 1])) 515 ); 516} 517 518export function fixGensymParams(params: ParamInfo[], body: arkts.BlockStatement): number { 519 let gensymParamsCount = 0; 520 for (let i = 0; i < params.length; i++) { 521 if (params[i].ident.name.startsWith(RuntimeNames.GENSYM)) { 522 if (gensymParamsCount >= body.statements.length) { 523 throw new Error(`Expected ${params[i].ident.name} replacement to original parameter`); 524 } 525 const declaration = body.statements[gensymParamsCount]; 526 if (!arkts.isVariableDeclaration(declaration)) { 527 throw new Error(`Expected ${params[i].ident.name} replacement to original parameter`); 528 } 529 if (!arkts.isIdentifier(declaration.declarators[0].name)) { 530 throw new Error(`Expected ${params[i].ident.name} replacement to original parameter`); 531 } 532 params[i].ident = declaration.declarators[0].name; 533 gensymParamsCount++; 534 } 535 } 536 return gensymParamsCount; 537} 538 539export function isUnmemoizedInFunction(params?: readonly arkts.Expression[]): boolean { 540 const _params = params ?? []; 541 const first = _params.at(0); 542 const isContextAdded = 543 !!first && arkts.isEtsParameterExpression(first) && first.identifier.name === RuntimeNames.CONTEXT; 544 const second = _params.at(1); 545 const isIdAdded = !!second && arkts.isEtsParameterExpression(second) && second.identifier.name === RuntimeNames.ID; 546 return isContextAdded && isIdAdded; 547} 548 549export function buildReturnTypeInfo( 550 returnType: arkts.TypeNode | undefined, 551 isMemo?: boolean, 552 isStableThis?: boolean 553): ReturnTypeInfo { 554 const newReturnType = !!returnType 555 ? returnType.clone() 556 : arkts.factory.createPrimitiveType(arkts.Es2pandaPrimitiveType.PRIMITIVE_TYPE_VOID); 557 return { 558 node: newReturnType, 559 isMemo, 560 isVoid: isVoidType(newReturnType), 561 isStableThis, 562 }; 563} 564 565export function buildeParamInfos(parameters: readonly arkts.ETSParameterExpression[]): ParamInfo[] { 566 return [ 567 ...parameters 568 .filter((it) => !hasMemoSkipAnnotation(it)) 569 .map((it) => { 570 return { ident: it.identifier, param: it }; 571 }), 572 ]; 573} 574 575export function parametersBlockHasReceiver(params: readonly arkts.Expression[]): boolean { 576 return params.length > 0 && arkts.isEtsParameterExpression(params[0]) && isThisParam(params[0]); 577} 578 579export function parametrizedNodeHasReceiver(node: arkts.ScriptFunction | arkts.ETSFunctionType | undefined): boolean { 580 if (node === undefined) { 581 return false; 582 } 583 return parametersBlockHasReceiver(node.params); 584} 585 586function isThisParam(node: arkts.Expression | undefined): boolean { 587 if (node === undefined || !arkts.isEtsParameterExpression(node)) { 588 return false; 589 } 590 return node.identifier?.isReceiver ?? false; 591} 592