diff --git a/arkui-plugins/common/predefines.ts b/arkui-plugins/common/predefines.ts index f9608f0481cd14199ed91a685cfbe3f1da8a45ac..97baf7f1cb2179af9185e01be5b0aa6855ea5003 100644 --- a/arkui-plugins/common/predefines.ts +++ b/arkui-plugins/common/predefines.ts @@ -39,6 +39,7 @@ export const MEMO_IMPORT_SOURCE_NAME: string = 'arkui.stateManagement.runtime'; export const CUSTOM_COMPONENT_IMPORT_SOURCE_NAME: string = 'arkui.component.customComponent'; export const ENTRY_POINT_IMPORT_SOURCE_NAME: string = 'arkui.UserView'; export const ARKUI_COMPONENT_COMMON_SOURCE_NAME: string = 'arkui.component.common'; +export const ARKUI_FOREACH_SOURCE_NAME: string = 'arkui.component.forEach'; export enum ModuleType { HAR = 'har', @@ -98,6 +99,10 @@ export enum EntryParamNames { ENTRY_ROUTE_NAME = 'routeName' } +export enum InnerComponentNames { + FOR_EACH = 'ForEach', +} + export enum DecoratorNames { STATE = 'State', STORAGE_LINK = 'StorageLink', diff --git a/arkui-plugins/test/demo/mock/component/for-each.ets b/arkui-plugins/test/demo/mock/component/for-each.ets new file mode 100644 index 0000000000000000000000000000000000000000..acb51f102a13823e022524bfa36daaf55e4ddcf8 --- /dev/null +++ b/arkui-plugins/test/demo/mock/component/for-each.ets @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Component, Text, WrappedBuilder, Column, ForEach } from "@kit.ArkUI" + +interface Person { + name: string; + age: number +} + +class AB { + per: string = 'hello'; + bar: Array = new Array('xx', 'yy', 'zz') +} + +@Component +struct ImportStruct { + arr: string[] = ['a', 'b', 'c'] + getArray() { + return new Array({ name: 'LiHua', age:25 } as Person, { name: 'Amy', age:18 } as Person) + } + + build() { + Column() { + ForEach(this.arr, (item: string) => { + Text(item) + }) + ForEach(this.getArray(), (item: Person) => { + Text(item.name) + }) + ForEach((new AB()).bar, (item: string) => { + Text(item) + }) + } + } +} \ No newline at end of file diff --git a/arkui-plugins/test/ut/ui-plugins/component/for-each.test.ts b/arkui-plugins/test/ut/ui-plugins/component/for-each.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..1cc5055cdaec3aeb6cbf9c3e73dbd162e5da3a74 --- /dev/null +++ b/arkui-plugins/test/ut/ui-plugins/component/for-each.test.ts @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as path from 'path'; +import { PluginTester } from '../../../utils/plugin-tester'; +import { mockBuildConfig } from '../../../utils/artkts-config'; +import { getRootPath, MOCK_ENTRY_DIR_PATH } from '../../../utils/path-config'; +import { parseDumpSrc } from '../../../utils/parse-string'; +import { structNoRecheck, recheck, uiNoRecheck } from '../../../utils/plugins'; +import { BuildConfig, PluginTestContext } from '../../../utils/shared-types'; +import { uiTransform } from '../../../../ui-plugins'; +import { Plugins } from '../../../../common/plugin-context'; + +const COMPONENT_DIR_PATH: string = 'component'; + +const buildConfig: BuildConfig = mockBuildConfig(); +buildConfig.compileFiles = [ + path.resolve(getRootPath(), MOCK_ENTRY_DIR_PATH, COMPONENT_DIR_PATH, 'for-each.ets'), +]; + +const pluginTester = new PluginTester('test ForEach component transformation', buildConfig); + +const parsedTransform: Plugins = { + name: 'for-each', + parsed: uiTransform().parsed +}; + +const expectedScript: string = ` +import { memo as memo } from "arkui.stateManagement.runtime"; + +import { LayoutCallback as LayoutCallback } from "arkui.component.customComponent"; + +import { CustomComponentV2 as CustomComponentV2 } from "arkui.component.customComponent"; + +import { CustomComponent as CustomComponent } from "arkui.component.customComponent"; + +import { Component as Component, Text as Text, WrappedBuilder as WrappedBuilder, Column as Column, ForEach as ForEach } from "@kit.ArkUI"; + +function main() {} + +interface Person { + set name(name: string) + + get name(): string + set age(age: number) + + get age(): number + +} + +class AB { + public per: string = "hello"; + + public bar: Array = new Array("xx", "yy", "zz"); + + public constructor() {} + +} + +@Component() final struct ImportStruct extends CustomComponent { + public __initializeStruct(initializers: __Options_ImportStruct | undefined, @memo() content: (()=> void) | undefined): void { + this.__backing_arr = ((({let gensym___244068973 = initializers; + (((gensym___244068973) == (null)) ? undefined : gensym___244068973.arr)})) ?? (["a", "b", "c"])); + } + + public __updateStruct(initializers: __Options_ImportStruct | undefined): void {} + + private __backing_arr?: Array; + + public get arr(): Array { + return (this.__backing_arr as Array); + } + + public set arr(value: Array) { + this.__backing_arr = value; + } + + public getArray() { + return new Array(({ + name: "LiHua", + age: 25, + } as Person), ({ + name: "Amy", + age: 18, + } as Person)); + } + + @memo() public build() { + Column(undefined, undefined, @memo() (() => { + ForEach(((): Array => { + return this.arr; + }), ((item: string) => { + Text(undefined, item, undefined, undefined); + })); + ForEach(((): Array => { + return this.getArray(); + }), ((item: Person) => { + Text(undefined, item.name, undefined, undefined); + })); + ForEach(((): Array => { + return new AB().bar; + }), ((item: string) => { + Text(undefined, item, undefined, undefined); + })); + })); + } + + private constructor() {} + +} + +@Component() export interface __Options_ImportStruct { + set arr(arr: Array | undefined) + + get arr(): Array | undefined + +} +`; + +function testParsedAndCheckedTransformer(this: PluginTestContext): void { + expect(parseDumpSrc(this.scriptSnapshot ?? '')).toBe(parseDumpSrc(expectedScript)); +} + +pluginTester.run( + 'test ForEach component transformation', + [parsedTransform, uiNoRecheck, recheck], + { + 'checked:ui-no-recheck': [testParsedAndCheckedTransformer], + }, + { + stopAfter: 'checked', + } +); diff --git a/arkui-plugins/test/ut/ui-plugins/wrap-builder/wrap-builder-in-ui.test.ts b/arkui-plugins/test/ut/ui-plugins/wrap-builder/wrap-builder-in-ui.test.ts index c5d73e2160002bdbe5f4a69fea873d0a84ca2159..6b2311e303045e2f2c4d82d25d52d531727982b8 100644 --- a/arkui-plugins/test/ut/ui-plugins/wrap-builder/wrap-builder-in-ui.test.ts +++ b/arkui-plugins/test/ut/ui-plugins/wrap-builder/wrap-builder-in-ui.test.ts @@ -78,7 +78,9 @@ function main() {} public __updateStruct(initializers: __Options_ImportStruct | undefined): void {} @memo() public testBuilder() { - ForEach(globalBuilderArr, ((item: WrappedBuilder) => { + ForEach(((): Array> => { + return globalBuilderArr; + }), ((item: WrappedBuilder) => { item.builder("hello world", 39); })); } diff --git a/arkui-plugins/ui-plugins/checked-transformer.ts b/arkui-plugins/ui-plugins/checked-transformer.ts index 682c9b43ac0a9e70acf389130c2273822bd9dc7a..c4d58b03db0a47bed399c63627c91484fdb606bd 100644 --- a/arkui-plugins/ui-plugins/checked-transformer.ts +++ b/arkui-plugins/ui-plugins/checked-transformer.ts @@ -30,11 +30,11 @@ import { LogCollector } from '../common/log-collector'; import { CustomComponentScopeInfo, initResourceInfo, - isResourceNode, loadBuildJson, LoaderJson, ResourceInfo, ScopeInfoCollection, + isForEachDecl } from './struct-translators/utils'; import { collectCustomComponentScopeInfo, CustomComponentNames, isCustomComponentClass } from './utils'; import { findAndCollectMemoableNode } from '../collectors/memo-collectors/factory'; @@ -136,8 +136,10 @@ export class CheckedTransformer extends AbstractVisitor { return node; } else if (arkts.isClassDeclaration(node)) { return structFactory.transformNormalClass(node); - } else if (arkts.isCallExpression(node) && isResourceNode(node)) { - return structFactory.transformResource(node, this.projectConfig, this.resourceInfo); + } else if (arkts.isCallExpression(node)) { + return structFactory.transformCallExpression(node, this.projectConfig, this.resourceInfo); + } else if (arkts.isMethodDefinition(node) && isForEachDecl(node, this.externalSourceName)) { + return structFactory.AddArrowTypeForParameter(node); } else if (isArkUICompatible(node)) { return generateArkUICompatible(node as arkts.CallExpression); } else if (arkts.isTSInterfaceDeclaration(node)) { diff --git a/arkui-plugins/ui-plugins/struct-translators/factory.ts b/arkui-plugins/ui-plugins/struct-translators/factory.ts index 129e43f021d5e07421fd966b28839bf71497d96a..aca7d99d31eb026e06d89a597c39181792836983 100644 --- a/arkui-plugins/ui-plugins/struct-translators/factory.ts +++ b/arkui-plugins/ui-plugins/struct-translators/factory.ts @@ -45,6 +45,8 @@ import { preCheckResourceData, ResourceParameter, getResourceParams, + isResourceNode, + isForEachCall, } from './utils'; import { collectStateManagementTypeImport, hasDecorator, PropertyCache } from '../property-translators/utils'; import { ProjectConfig } from '../../common/plugin-context'; @@ -663,7 +665,7 @@ export class factory { return node; } if (isEtsGlobalClass(node)) { - const updatedBody = node.definition.body.map((member: arkts.AstNode) => { + const updatedBody = node.definition.body.map((member: arkts.AstNode) => { arkts.isMethodDefinition(member) && propertyFactory.addMemoToBuilderClassMethod(member); if (arkts.isMethodDefinition(member) && hasDecorator(member, DecoratorNames.ANIMATABLE_EXTEND)) { member = arkts.factory.updateMethodDefinition( @@ -677,7 +679,7 @@ export class factory { } return member; }); - return arkts.factory.updateClassDeclaration( + return arkts.factory.updateClassDeclaration( node, arkts.factory.updateClassDefinition( node.definition, @@ -851,4 +853,73 @@ export class factory { node.modifiers ); } + + /* + * add arrow function type to arguments of call expression. + */ + static transformCallArguments(node: arkts.CallExpression): arkts.CallExpression { + if (!arkts.isArrowFunctionExpression(node.arguments[1])) { + return node; + } + const argTypeParam: arkts.Expression = node.arguments[1].scriptFunction.params[0]; + if ( + !arkts.isEtsParameterExpression(argTypeParam) || + !argTypeParam.type || + !arkts.isTypeNode(argTypeParam.type) + ) { + return node; + } + const referenceType = uiFactory.createComplexTypeFromStringAndTypeParameter('Array', [ + argTypeParam.type.clone(), + ]); + const newArrowArg: arkts.ArrowFunctionExpression = arkts.factory.createArrowFunction( + uiFactory.createScriptFunction({ + body: arkts.factory.createBlock([arkts.factory.createReturnStatement(node.arguments[0])]), + returnTypeAnnotation: referenceType, + flags: arkts.Es2pandaScriptFunctionFlags.SCRIPT_FUNCTION_FLAGS_ARROW, + modifiers: arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_NONE, + }) + ); + return arkts.factory.updateCallExpression(node, node.expression, node.typeArguments, [ + newArrowArg, + ...node.arguments.slice(1), + ]); + } + + static AddArrowTypeForParameter(node: arkts.MethodDefinition): arkts.MethodDefinition { + if (node.scriptFunction.params.length < 2) { + return node; + } + const paramFirst = node.scriptFunction.params[0]; + if (!arkts.isEtsParameterExpression(paramFirst) || !paramFirst.type || !arkts.isTypeNode(paramFirst.type)) { + return node; + } + const script = uiFactory.updateScriptFunction(node.scriptFunction, { + params: [ + arkts.factory.createParameterDeclaration( + arkts.factory.createIdentifier( + paramFirst.identifier.name, + uiFactory.createLambdaFunctionType([], paramFirst.type) + ), + undefined + ), + ...node.scriptFunction.params.slice(1), + ], + }); + return arkts.factory.updateMethodDefinition(node, node.kind, node.name, script, node.modifiers, false); + } + + static transformCallExpression( + node: arkts.CallExpression, + projectConfig: ProjectConfig | undefined, + resourceInfo: ResourceInfo + ): arkts.CallExpression { + if (arkts.isCallExpression(node) && isResourceNode(node)) { + return this.transformResource(node, projectConfig, resourceInfo); + } + if (arkts.isCallExpression(node) && isForEachCall(node)) { + return this.transformCallArguments(node); + } + return node; + } } diff --git a/arkui-plugins/ui-plugins/struct-translators/utils.ts b/arkui-plugins/ui-plugins/struct-translators/utils.ts index 9be1a309cf9318ddfbc37e9ef781638767e06e23..bb3123e317cceecd15fc5ecf8ac7e5531876e5d1 100644 --- a/arkui-plugins/ui-plugins/struct-translators/utils.ts +++ b/arkui-plugins/ui-plugins/struct-translators/utils.ts @@ -20,12 +20,13 @@ import { CustomComponentInfo } from '../utils'; import { matchPrefix } from '../../common/arkts-utils'; import { ARKUI_IMPORT_PREFIX_NAMES, - DecoratorNames, Dollars, ModuleType, DefaultConfiguration, LogType, RESOURCE_TYPE, + InnerComponentNames, + ARKUI_FOREACH_SOURCE_NAME, } from '../../common/predefines'; import { DeclarationCollector } from '../../common/declaration-collector'; import { ProjectConfig } from '../../common/plugin-context'; @@ -106,6 +107,17 @@ export function isResourceNode(node: arkts.CallExpression, ignoreDecl: boolean = return true; } +export function isForEachCall(node: arkts.CallExpression): boolean { + if ( + arkts.isIdentifier(node.expression) && + node.expression.name === InnerComponentNames.FOR_EACH && + node.arguments.length >= 2 + ) { + return true; + } + return false; +} + /** * Read the content of file 'loader.json'. * @@ -494,3 +506,17 @@ export function isDynamicName(projectConfig: ProjectConfig): boolean { const uiTransformOptimization: boolean = !!projectConfig.uiTransformOptimization; return uiTransformOptimization ? uiTransformOptimization : isByteCodeHar; } + +/** + * Determine whether the node is ForEach method declaration. + * + * @param node method definition node. + * @param sourceName external source name. + */ +export function isForEachDecl(node: arkts.MethodDefinition, sourceName: string | undefined): boolean { + const isForEach: boolean = !!node.name && node.name.name === InnerComponentNames.FOR_EACH; + const isMethodDecl: boolean = + !!node.scriptFunction && + arkts.hasModifierFlag(node.scriptFunction, arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_DECLARE); + return isForEach && isMethodDecl && !!sourceName && sourceName === ARKUI_FOREACH_SOURCE_NAME; +} diff --git a/arkui-plugins/ui-plugins/ui-factory.ts b/arkui-plugins/ui-plugins/ui-factory.ts index 9e7d5fa3b5dd6cf6686b7ae9b0b1d3d64a601486..f6718b6522323180b54aec23ef1c741c2d59677b 100644 --- a/arkui-plugins/ui-plugins/ui-factory.ts +++ b/arkui-plugins/ui-plugins/ui-factory.ts @@ -150,6 +150,18 @@ export class factory { ); } + /** + * create complex type from string and type parameter, e.g. `Set` + */ + static createComplexTypeFromStringAndTypeParameter(name: string, params: arkts.TypeNode[]): arkts.TypeNode { + return arkts.factory.createTypeReference( + arkts.factory.createTypeReferencePart( + arkts.factory.createIdentifier(name), + arkts.factory.createTSTypeParameterInstantiation(params) + ) + ); + } + /** * create `() => `. If returnType is not given, then using `void`. */ @@ -259,9 +271,7 @@ export class factory { /** * create MethodDefinition with configurations. */ - static createMethodDefinition( - config: PartialNested - ): arkts.MethodDefinition { + static createMethodDefinition(config: PartialNested): arkts.MethodDefinition { const newFunc: arkts.ScriptFunction = factory.createScriptFunction({ ...config.function, key: config.key,