From 0778606f0b026474d7c19f37ed4da648f221ac63 Mon Sep 17 00:00:00 2001 From: s00912778 Date: Tue, 15 Jul 2025 22:18:33 +0800 Subject: [PATCH] ]\support ConditionScope and ConditionBranch Signed-off-by: s00912778 Change-Id: Iac8cbb02051164bd949b217920e249491b1d2b06 --- arkui-plugins/common/predefines.ts | 6 + .../builder-lambda/if-else-in-content.ets | 40 +++ .../builder-lambda/switch-case-in-content.ets | 41 +++ .../builder-lambda/if-else-in-content.test.ts | 287 ++++++++++++++++++ .../switch-case-in-content.test.ts | 244 +++++++++++++++ .../optional-builder-param.test.ts | 80 ++++- .../prop-ref/state-to-propref.test.ts | 24 +- .../decorators/prop/state-to-prop.test.ts | 23 +- .../reusable/reusable-complex.test.ts | 22 +- .../decorators/state/state-basic-type.test.ts | 4 - .../builder-lambda-translators/factory.ts | 244 ++++++++++++--- .../builder-lambda-translators/utils.ts | 35 ++- .../ui-plugins/checked-transformer.ts | 2 +- .../ui-plugins/struct-translators/factory.ts | 126 ++++---- koala-wrapper/src/arkts-api/index.ts | 9 +- 15 files changed, 1050 insertions(+), 137 deletions(-) create mode 100644 arkui-plugins/test/demo/mock/builder-lambda/if-else-in-content.ets create mode 100644 arkui-plugins/test/demo/mock/builder-lambda/switch-case-in-content.ets create mode 100644 arkui-plugins/test/ut/ui-plugins/builder-lambda/if-else-in-content.test.ts create mode 100644 arkui-plugins/test/ut/ui-plugins/builder-lambda/switch-case-in-content.test.ts diff --git a/arkui-plugins/common/predefines.ts b/arkui-plugins/common/predefines.ts index 6101ba6ba..bfe8a0313 100644 --- a/arkui-plugins/common/predefines.ts +++ b/arkui-plugins/common/predefines.ts @@ -41,6 +41,7 @@ export const CUSTOM_DIALOG_CONTROLLER_SOURCE_NAME: string = 'arkui.component.cus 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 const ARKUI_BUILDER_SOURCE_NAME: string = 'arkui.component.builder'; export enum ModuleType { HAR = 'har', @@ -195,6 +196,11 @@ export enum NavigationNames { INTEGRATED_HSP = 'integratedHsp', } +export enum ConditionNames { + CONDITION_SCOPE = 'ConditionScope', + CONDITION_BRANCH = 'ConditionBranch', +} + export const RESOURCE_TYPE: Record = { color: 10001, float: 10002, diff --git a/arkui-plugins/test/demo/mock/builder-lambda/if-else-in-content.ets b/arkui-plugins/test/demo/mock/builder-lambda/if-else-in-content.ets new file mode 100644 index 000000000..c9f98dbe9 --- /dev/null +++ b/arkui-plugins/test/demo/mock/builder-lambda/if-else-in-content.ets @@ -0,0 +1,40 @@ +/* + * 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 { Text, Column, Component } from "@ohos.arkui.component" + +@Component +struct IfElse { + build() { + Column() { + if (true) { + if (false) { + Text('if-if') + } else if (true) { + Text('if-elseIf') + } else { + Text('if-else') + } + } else if (false) { + Text('elseIf') + } else { + Text('else') + return; + Text('after-return') + } + Text('hello world') + } + } +} \ No newline at end of file diff --git a/arkui-plugins/test/demo/mock/builder-lambda/switch-case-in-content.ets b/arkui-plugins/test/demo/mock/builder-lambda/switch-case-in-content.ets new file mode 100644 index 000000000..8944b5e0a --- /dev/null +++ b/arkui-plugins/test/demo/mock/builder-lambda/switch-case-in-content.ets @@ -0,0 +1,41 @@ +/* + * 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 { Text, Column, Component } from "@ohos.arkui.component" + +@Component +struct SwitchCase { + num: string = '1'; + + build() { + Column() { + switch (this.num) { + case '0': + Text('case 0') + case '1': + Text('case 1') + break; + Text('after break') + case '2': + Text('case 2') + return; + Text('after return') + default: + Text('default') + } + Text('hello world') + } + } +} \ No newline at end of file diff --git a/arkui-plugins/test/ut/ui-plugins/builder-lambda/if-else-in-content.test.ts b/arkui-plugins/test/ut/ui-plugins/builder-lambda/if-else-in-content.test.ts new file mode 100644 index 000000000..bf60c883d --- /dev/null +++ b/arkui-plugins/test/ut/ui-plugins/builder-lambda/if-else-in-content.test.ts @@ -0,0 +1,287 @@ +/* + * 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 { memoNoRecheck, recheck, uiNoRecheck } from '../../../utils/plugins'; +import { BuildConfig, PluginTestContext } from '../../../utils/shared-types'; +import { uiTransform } from '../../../../ui-plugins'; +import { Plugins } from '../../../../common/plugin-context'; + +const BUILDER_LAMBDA_DIR_PATH: string = 'builder-lambda'; + +const buildConfig: BuildConfig = mockBuildConfig(); +buildConfig.compileFiles = [ + path.resolve(getRootPath(), MOCK_ENTRY_DIR_PATH, BUILDER_LAMBDA_DIR_PATH, 'if-else-in-content.ets'), +]; + +const pluginTester = new PluginTester('test if-else conditions in builder lambda call', buildConfig); + +const parsedTransform: Plugins = { + name: 'if-else', + parsed: uiTransform().parsed, +}; + +const expectedUIScript: string = ` +import { ConditionScope as ConditionScope } from \"arkui.component.builder\"; +import { ConditionBranch as ConditionBranch } from \"arkui.component.builder\"; +import { memo as memo } from \"arkui.stateManagement.runtime\"; +import { CustomComponent as CustomComponent } from \"arkui.component.customComponent\"; +import { Text as Text, Column as Column, Component as Component } from \"@ohos.arkui.component\"; +function main() {} +@Component() final struct IfElse extends CustomComponent { + public __initializeStruct(initializers: (__Options_IfElse | undefined), @memo() content: ((()=> void) | undefined)): void {} + public __updateStruct(initializers: (__Options_IfElse | undefined)): void {} + @memo() public build() { + Column(undefined, undefined, @memo() (() => { + ConditionScope(@memo() (() => { + if (true) { + ConditionBranch(@memo() (() => { + ConditionScope(@memo() (() => { + if (false) { + ConditionBranch(@memo() (() => { + Text(undefined, \"if-if\", undefined, undefined); + })); + } else { + ConditionScope(@memo() (() => { + if (true) { + ConditionBranch(@memo() (() => { + Text(undefined, \"if-elseIf\", undefined, undefined); + })); + } else { + ConditionBranch(@memo() (() => { + Text(undefined, \"if-else\", undefined, undefined); + })); + } + })) + } + })); + })); + } else { + ConditionScope(@memo() (() => { + if (false) { + ConditionBranch(@memo() (() => { + Text(undefined, \"elseIf\", undefined, undefined); + })); + } else { + ConditionBranch(@memo() (() => { + Text(undefined, \"else\", undefined, undefined); + })); + return; + Text(undefined, \"after-return\", undefined, undefined); + } + })) + } + })); + Text(undefined, \"hello world\", undefined, undefined); + })); + } + private constructor() {} +} +@Component() export interface __Options_IfElse { +} +`; + +function testUITransformer(this: PluginTestContext): void { + expect(parseDumpSrc(this.scriptSnapshot ?? '')).toBe(parseDumpSrc(expectedUIScript)); +} + +const expectedMemoScript: string = ` +import { __memo_context_type as __memo_context_type, __memo_id_type as __memo_id_type } from \"arkui.stateManagement.runtime\"; +import { ConditionScope as ConditionScope } from \"arkui.component.builder\"; +import { ConditionBranch as ConditionBranch } from \"arkui.component.builder\"; +import { memo as memo } from \"arkui.stateManagement.runtime\"; +import { CustomComponent as CustomComponent } from \"arkui.component.customComponent\"; +import { Text as Text, Column as Column, Component as Component } from \"@ohos.arkui.component\"; +function main() {} +@Component() final struct IfElse extends CustomComponent { + public __initializeStruct(initializers: (__Options_IfElse | undefined), @memo() content: (((__memo_context: __memo_context_type, __memo_id: __memo_id_type)=> void) | undefined)): void {} + public __updateStruct(initializers: (__Options_IfElse | undefined)): void {} + @memo() public build(__memo_context: __memo_context_type, __memo_id: __memo_id_type) { + const __memo_scope = __memo_context.scope(((__memo_id) + ()), 0); + if (__memo_scope.unchanged) { + __memo_scope.cached; + return; + } + Column(__memo_context, ((__memo_id) + ()), undefined, undefined, @memo() ((__memo_context: __memo_context_type, __memo_id: __memo_id_type) => { + const __memo_scope = __memo_context.scope(((__memo_id) + ()), 0); + if (__memo_scope.unchanged) { + __memo_scope.cached; + return; + } + ConditionScope(__memo_context, ((__memo_id) + ()), @memo() ((__memo_context: __memo_context_type, __memo_id: __memo_id_type) => { + const __memo_scope = __memo_context.scope(((__memo_id) + ()), 0); + if (__memo_scope.unchanged) { + __memo_scope.cached; + return; + } + if (true) { + ConditionBranch(__memo_context, ((__memo_id) + ()), @memo() ((__memo_context: __memo_context_type, __memo_id: __memo_id_type) => { + const __memo_scope = __memo_context.scope(((__memo_id) + ()), 0); + if (__memo_scope.unchanged) { + __memo_scope.cached; + return; + } + ConditionScope(__memo_context, ((__memo_id) + ()), @memo() ((__memo_context: __memo_context_type, __memo_id: __memo_id_type) => { + const __memo_scope = __memo_context.scope(((__memo_id) + ()), 0); + if (__memo_scope.unchanged) { + __memo_scope.cached; + return; + } + if (false) { + ConditionBranch(__memo_context, ((__memo_id) + ()), @memo() ((__memo_context: __memo_context_type, __memo_id: __memo_id_type) => { + const __memo_scope = __memo_context.scope(((__memo_id) + ()), 0); + if (__memo_scope.unchanged) { + __memo_scope.cached; + return; + } + Text(__memo_context, ((__memo_id) + ()), undefined, \"if-if\", undefined, undefined); + { + __memo_scope.recache(); + return; + } + })); + } else { + ConditionScope(__memo_context, ((__memo_id) + ()), @memo() ((__memo_context: __memo_context_type, __memo_id: __memo_id_type) => { + const __memo_scope = __memo_context.scope(((__memo_id) + ()), 0); + if (__memo_scope.unchanged) { + __memo_scope.cached; + return; + } + if (true) { + ConditionBranch(__memo_context, ((__memo_id) + ()), @memo() ((__memo_context: __memo_context_type, __memo_id: __memo_id_type) => { + const __memo_scope = __memo_context.scope(((__memo_id) + ()), 0); + if (__memo_scope.unchanged) { + __memo_scope.cached; + return; + } + Text(__memo_context, ((__memo_id) + ()), undefined, \"if-elseIf\", undefined, undefined); + { + __memo_scope.recache(); + return; + } + })); + } else { + ConditionBranch(__memo_context, ((__memo_id) + ()), @memo() ((__memo_context: __memo_context_type, __memo_id: __memo_id_type) => { + const __memo_scope = __memo_context.scope(((__memo_id) + ()), 0); + if (__memo_scope.unchanged) { + __memo_scope.cached; + return; + } + Text(__memo_context, ((__memo_id) + ()), undefined, \"if-else\", undefined, undefined); + { + __memo_scope.recache(); + return; + } + })); + } + { + __memo_scope.recache(); + return; + } + })) + } + { + __memo_scope.recache(); + return; + } + })); + { + __memo_scope.recache(); + return; + } + })); + } else { + ConditionScope(__memo_context, ((__memo_id) + ()), @memo() ((__memo_context: __memo_context_type, __memo_id: __memo_id_type) => { + const __memo_scope = __memo_context.scope(((__memo_id) + ()), 0); + if (__memo_scope.unchanged) { + __memo_scope.cached; + return; + } + if (false) { + ConditionBranch(__memo_context, ((__memo_id) + ()), @memo() ((__memo_context: __memo_context_type, __memo_id: __memo_id_type) => { + const __memo_scope = __memo_context.scope(((__memo_id) + ()), 0); + if (__memo_scope.unchanged) { + __memo_scope.cached; + return; + } + Text(__memo_context, ((__memo_id) + ()), undefined, \"elseIf\", undefined, undefined); + { + __memo_scope.recache(); + return; + } + })); + } else { + ConditionBranch(__memo_context, ((__memo_id) + ()), @memo() ((__memo_context: __memo_context_type, __memo_id: __memo_id_type) => { + const __memo_scope = __memo_context.scope(((__memo_id) + ()), 0); + if (__memo_scope.unchanged) { + __memo_scope.cached; + return; + } + Text(__memo_context, ((__memo_id) + ()), undefined, \"else\", undefined, undefined); + { + __memo_scope.recache(); + return; + } + })); + return; + Text(__memo_context, ((__memo_id) + ()), undefined, \"after-return\", undefined, undefined); + } + { + __memo_scope.recache(); + return; + } + })) + } + { + __memo_scope.recache(); + return; + } + })); + Text(__memo_context, ((__memo_id) + ()), undefined, \"hello world\", undefined, undefined); + { + __memo_scope.recache(); + return; + } + })); + { + __memo_scope.recache(); + return; + } + } + private constructor() {} +} +@Component() export interface __Options_IfElse { +} +`; + +function testMemoTransformer(this: PluginTestContext): void { + expect(parseDumpSrc(this.scriptSnapshot ?? '')).toBe(parseDumpSrc(expectedMemoScript)); +} + +pluginTester.run( + 'test if-else', + [parsedTransform, uiNoRecheck, memoNoRecheck, recheck], + { + 'checked:ui-no-recheck': [testUITransformer], + 'checked:memo-no-recheck': [testMemoTransformer], + }, + { + stopAfter: 'checked', + } +); diff --git a/arkui-plugins/test/ut/ui-plugins/builder-lambda/switch-case-in-content.test.ts b/arkui-plugins/test/ut/ui-plugins/builder-lambda/switch-case-in-content.test.ts new file mode 100644 index 000000000..9559e935a --- /dev/null +++ b/arkui-plugins/test/ut/ui-plugins/builder-lambda/switch-case-in-content.test.ts @@ -0,0 +1,244 @@ +/* + * 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 { memoNoRecheck, recheck, uiNoRecheck } from '../../../utils/plugins'; +import { BuildConfig, PluginTestContext } from '../../../utils/shared-types'; +import { uiTransform } from '../../../../ui-plugins'; +import { Plugins } from '../../../../common/plugin-context'; + +const BUILDER_LAMBDA_DIR_PATH: string = 'builder-lambda'; + +const buildConfig: BuildConfig = mockBuildConfig(); +buildConfig.compileFiles = [ + path.resolve(getRootPath(), MOCK_ENTRY_DIR_PATH, BUILDER_LAMBDA_DIR_PATH, 'switch-case-in-content.ets'), +]; + +const pluginTester = new PluginTester('test switch-case conditions in builder lambda call', buildConfig); + +const parsedTransform: Plugins = { + name: 'switch-case', + parsed: uiTransform().parsed, +}; + +const expectedUIScript: string = ` +import { ConditionScope as ConditionScope } from \"arkui.component.builder\"; +import { ConditionBranch as ConditionBranch } from \"arkui.component.builder\"; +import { memo as memo } from \"arkui.stateManagement.runtime\"; +import { CustomComponent as CustomComponent } from \"arkui.component.customComponent\"; +import { Text as Text, Column as Column, Component as Component } from \"@ohos.arkui.component\"; +function main() {} +@Component() final struct SwitchCase extends CustomComponent { + public __initializeStruct(initializers: (__Options_SwitchCase | undefined), @memo() content: ((()=> void) | undefined)): void { + this.__backing_num = ((({let gensym___ = initializers; + (((gensym___) == (null)) ? undefined : gensym___.num)})) ?? (\"1\")); + } + public __updateStruct(initializers: (__Options_SwitchCase | undefined)): void {} + private __backing_num?: string; + public get num(): string { + return (this.__backing_num as string); + } + public set num(value: string) { + this.__backing_num = value; + } + @memo() public build() { + Column(undefined, undefined, @memo() (() => { + ConditionScope(@memo() (() => { + switch (this.num) { + case \"0\": { + ConditionBranch(@memo() (() => { + Text(undefined, \"case 0\", undefined, undefined); + })); + } + case \"1\": { + ConditionBranch(@memo() (() => { + Text(undefined, \"case 1\", undefined, undefined); + })); + break; + Text(undefined, \"after break\", undefined, undefined); + } + case \"2\": { + ConditionBranch(@memo() (() => { + Text(undefined, \"case 2\", undefined, undefined); + })); + return; + Text(undefined, \"after return\", undefined, undefined); + } + default: { + ConditionBranch(@memo() (() => { + Text(undefined, \"default\", undefined, undefined); + })); + } + } + })); + Text(undefined, \"hello world\", undefined, undefined); + })); + } + private constructor() {} +} +@Component() export interface __Options_SwitchCase { + set num(num: (string | undefined)) + get num(): (string | undefined) +} +`; + +function testUITransformer(this: PluginTestContext): void { + expect(parseDumpSrc(this.scriptSnapshot ?? '')).toBe(parseDumpSrc(expectedUIScript)); +} + +const expectedMemoScript: string = ` +import { __memo_context_type as __memo_context_type, __memo_id_type as __memo_id_type } from \"arkui.stateManagement.runtime\"; +import { ConditionScope as ConditionScope } from \"arkui.component.builder\"; +import { ConditionBranch as ConditionBranch } from \"arkui.component.builder\"; +import { memo as memo } from \"arkui.stateManagement.runtime\"; +import { CustomComponent as CustomComponent } from \"arkui.component.customComponent\"; +import { Text as Text, Column as Column, Component as Component } from \"@ohos.arkui.component\"; +function main() {} +@Component() final struct SwitchCase extends CustomComponent { + public __initializeStruct(initializers: (__Options_SwitchCase | undefined), @memo() content: (((__memo_context: __memo_context_type, __memo_id: __memo_id_type)=> void) | undefined)): void { + this.__backing_num = ((({let gensym___ = initializers; + (((gensym___) == (null)) ? undefined : gensym___.num)})) ?? (\"1\")); + } + public __updateStruct(initializers: (__Options_SwitchCase | undefined)): void {} + private __backing_num?: string; + public get num(): string { + return (this.__backing_num as string); + } + public set num(value: string) { + this.__backing_num = value; + } + @memo() public build(__memo_context: __memo_context_type, __memo_id: __memo_id_type) { + const __memo_scope = __memo_context.scope(((__memo_id) + ()), 0); + if (__memo_scope.unchanged) { + __memo_scope.cached; + return; + } + Column(__memo_context, ((__memo_id) + ()), undefined, undefined, @memo() ((__memo_context: __memo_context_type, __memo_id: __memo_id_type) => { + const __memo_scope = __memo_context.scope(((__memo_id) + ()), 0); + if (__memo_scope.unchanged) { + __memo_scope.cached; + return; + } + ConditionScope(__memo_context, ((__memo_id) + ()), @memo() ((__memo_context: __memo_context_type, __memo_id: __memo_id_type) => { + const __memo_scope = __memo_context.scope(((__memo_id) + ()), 0); + if (__memo_scope.unchanged) { + __memo_scope.cached; + return; + } + switch (this.num) { + case \"0\": { + ConditionBranch(__memo_context, ((__memo_id) + ()), @memo() ((__memo_context: __memo_context_type, __memo_id: __memo_id_type) => { + const __memo_scope = __memo_context.scope(((__memo_id) + ()), 0); + if (__memo_scope.unchanged) { + __memo_scope.cached; + return; + } + Text(__memo_context, ((__memo_id) + ()), undefined, \"case 0\", undefined, undefined); + { + __memo_scope.recache(); + return; + } + })); + } + case \"1\": { + ConditionBranch(__memo_context, ((__memo_id) + ()), @memo() ((__memo_context: __memo_context_type, __memo_id: __memo_id_type) => { + const __memo_scope = __memo_context.scope(((__memo_id) + ()), 0); + if (__memo_scope.unchanged) { + __memo_scope.cached; + return; + } + Text(__memo_context, ((__memo_id) + ()), undefined, \"case 1\", undefined, undefined); + { + __memo_scope.recache(); + return; + } + })); + break; + Text(__memo_context, ((__memo_id) + ()), undefined, \"after break\", undefined, undefined); + } + case \"2\": { + ConditionBranch(__memo_context, ((__memo_id) + ()), @memo() ((__memo_context: __memo_context_type, __memo_id: __memo_id_type) => { + const __memo_scope = __memo_context.scope(((__memo_id) + ()), 0); + if (__memo_scope.unchanged) { + __memo_scope.cached; + return; + } + Text(__memo_context, ((__memo_id) + ()), undefined, \"case 2\", undefined, undefined); + { + __memo_scope.recache(); + return; + } + })); + return; + Text(__memo_context, ((__memo_id) + ()), undefined, \"after return\", undefined, undefined); + } + default: { + ConditionBranch(__memo_context, ((__memo_id) + ()), @memo() ((__memo_context: __memo_context_type, __memo_id: __memo_id_type) => { + const __memo_scope = __memo_context.scope(((__memo_id) + ()), 0); + if (__memo_scope.unchanged) { + __memo_scope.cached; + return; + } + Text(__memo_context, ((__memo_id) + ()), undefined, \"default\", undefined, undefined); + { + __memo_scope.recache(); + return; + } + })); + } + } + { + __memo_scope.recache(); + return; + } + })); + Text(__memo_context, ((__memo_id) + ()), undefined, \"hello world\", undefined, undefined); + { + __memo_scope.recache(); + return; + } + })); + { + __memo_scope.recache(); + return; + } + } + private constructor() {} +} +@Component() export interface __Options_SwitchCase { + set num(num: (string | undefined)) + get num(): (string | undefined) +} +`; + +function testMemoTransformer(this: PluginTestContext): void { + expect(parseDumpSrc(this.scriptSnapshot ?? '')).toBe(parseDumpSrc(expectedMemoScript)); +} + +pluginTester.run( + 'test switch-case', + [parsedTransform, uiNoRecheck, memoNoRecheck, recheck], + { + 'checked:ui-no-recheck': [testUITransformer], + 'checked:memo-no-recheck': [testMemoTransformer], + }, + { + stopAfter: 'checked', + } +); diff --git a/arkui-plugins/test/ut/ui-plugins/decorators/builder-param/optional-builder-param.test.ts b/arkui-plugins/test/ut/ui-plugins/decorators/builder-param/optional-builder-param.test.ts index 7cbf498a8..1a32d8c7a 100644 --- a/arkui-plugins/test/ut/ui-plugins/decorators/builder-param/optional-builder-param.test.ts +++ b/arkui-plugins/test/ut/ui-plugins/decorators/builder-param/optional-builder-param.test.ts @@ -38,6 +38,8 @@ const parsedTransform: Plugins = { }; const expectedUIScript: string = ` +import { ConditionScope as ConditionScope } from "arkui.component.builder"; +import { ConditionBranch as ConditionBranch } from "arkui.component.builder"; import { memo as memo } from "arkui.stateManagement.runtime"; import { CustomComponent as CustomComponent } from "arkui.component.customComponent"; import { Component as Component, Entry as Entry, Builder as Builder, BuilderParam as BuilderParam, Column as Column, Text as Text, Row as Row } from "@kit.ArkUI"; @@ -80,12 +82,20 @@ function main() {} @memo() public build() { Row(undefined, undefined, @memo() (() => { - if (this.customBuilderParam2) { - (this.customBuilderParam2 as (()=> void))(); - } - if (this.customBuilderParam2) { - this.customBuilderParam2!(); - } + ConditionScope(@memo() (() => { + if (this.customBuilderParam2) { + ConditionBranch(@memo() (() => { + (this.customBuilderParam2 as (()=> void))(); + })); + } + })); + ConditionScope(@memo() (() => { + if (this.customBuilderParam2) { + ConditionBranch(@memo() (() => { + this.customBuilderParam2!(); + })); + } + })); this.customBuilderParam1(); })); } @@ -136,6 +146,8 @@ function main() {} const expectedMemoScript: string = ` import { __memo_context_type as __memo_context_type, __memo_id_type as __memo_id_type } from "arkui.stateManagement.runtime"; +import { ConditionScope as ConditionScope } from "arkui.component.builder"; +import { ConditionBranch as ConditionBranch } from "arkui.component.builder"; import { memo as memo } from "arkui.stateManagement.runtime"; import { CustomComponent as CustomComponent } from "arkui.component.customComponent"; import { Component as Component, Entry as Entry, Builder as Builder, BuilderParam as BuilderParam, Column as Column, Text as Text, Row as Row } from "@kit.ArkUI"; @@ -197,12 +209,56 @@ function main() {} __memo_scope.cached; return; } - if (this.customBuilderParam2) { - (this.customBuilderParam2 as ((__memo_context: __memo_context_type, __memo_id: __memo_id_type)=> void))(__memo_context, ((__memo_id) + (241913892))); - } - if (this.customBuilderParam2) { - this.customBuilderParam2!(__memo_context, ((__memo_id) + (137225318))); - } + ConditionScope(__memo_context, ((__memo_id) + ()), @memo() ((__memo_context: __memo_context_type, __memo_id: __memo_id_type) => { + const __memo_scope = __memo_context.scope(((__memo_id) + ()), 0); + if (__memo_scope.unchanged) { + __memo_scope.cached; + return; + } + if (this.customBuilderParam2) { + ConditionBranch(__memo_context, ((__memo_id) + ()), @memo() ((__memo_context: __memo_context_type, __memo_id: __memo_id_type) => { + const __memo_scope = __memo_context.scope(((__memo_id) + ()), 0); + if (__memo_scope.unchanged) { + __memo_scope.cached; + return; + } + (this.customBuilderParam2 as ((__memo_context: __memo_context_type, __memo_id: __memo_id_type)=> void))(__memo_context, ((__memo_id) + ())); + { + __memo_scope.recache(); + return; + } + })); + } + { + __memo_scope.recache(); + return; + } + })); + ConditionScope(__memo_context, ((__memo_id) + ()), @memo() ((__memo_context: __memo_context_type, __memo_id: __memo_id_type) => { + const __memo_scope = __memo_context.scope(((__memo_id) + ()), 0); + if (__memo_scope.unchanged) { + __memo_scope.cached; + return; + } + if (this.customBuilderParam2) { + ConditionBranch(__memo_context, ((__memo_id) + ()), @memo() ((__memo_context: __memo_context_type, __memo_id: __memo_id_type) => { + const __memo_scope = __memo_context.scope(((__memo_id) + ()), 0); + if (__memo_scope.unchanged) { + __memo_scope.cached; + return; + } + this.customBuilderParam2!(__memo_context, ((__memo_id) + ())); + { + __memo_scope.recache(); + return; + } + })); + } + { + __memo_scope.recache(); + return; + } + })); this.customBuilderParam1(__memo_context, ((__memo_id) + (211301233))); { __memo_scope.recache(); diff --git a/arkui-plugins/test/ut/ui-plugins/decorators/prop-ref/state-to-propref.test.ts b/arkui-plugins/test/ut/ui-plugins/decorators/prop-ref/state-to-propref.test.ts index 80ffe476f..1cc5db07d 100644 --- a/arkui-plugins/test/ut/ui-plugins/decorators/prop-ref/state-to-propref.test.ts +++ b/arkui-plugins/test/ut/ui-plugins/decorators/prop-ref/state-to-propref.test.ts @@ -44,10 +44,14 @@ import { STATE_MGMT_FACTORY as STATE_MGMT_FACTORY } from "arkui.stateManagement. import { IPropRefDecoratedVariable as IPropRefDecoratedVariable } from "arkui.stateManagement.decorator"; -import { memo as memo } from "arkui.stateManagement.runtime"; - import { ButtonAttribute as ButtonAttribute } from "arkui.component.button"; +import { ConditionScope as ConditionScope } from "arkui.component.builder"; + +import { ConditionBranch as ConditionBranch } from "arkui.component.builder"; + +import { memo as memo } from "arkui.stateManagement.runtime"; + import { CustomComponent as CustomComponent } from "arkui.component.customComponent"; import { Component as Component, Text as Text, Button as Button, Column as Column, ClickEvent as ClickEvent } from "@ohos.arkui.component"; @@ -93,11 +97,17 @@ function main() {} @memo() public build() { Column(undefined, undefined, @memo() (() => { - if (((this.count) > (0))) { - Text(undefined, (((("You have") + (this.count))) + ("Nuggets left")), undefined, undefined); - } else { - Text(undefined, "Game over!", undefined, undefined); - } + ConditionScope(@memo() (() => { + if (((this.count) > (0))) { + ConditionBranch(@memo() (() => { + Text(undefined, (((("You have") + (this.count))) + ("Nuggets left")), undefined, undefined); + })); + } else { + ConditionBranch(@memo() (() => { + Text(undefined, "Game over!", undefined, undefined); + })); + } + })); Button(@memo() ((instance: ButtonAttribute): void => { instance.onClick(((e: ClickEvent) => { this.count -= this.costOfOneAttempt; diff --git a/arkui-plugins/test/ut/ui-plugins/decorators/prop/state-to-prop.test.ts b/arkui-plugins/test/ut/ui-plugins/decorators/prop/state-to-prop.test.ts index 9ee9f8d89..fb8f9e78c 100644 --- a/arkui-plugins/test/ut/ui-plugins/decorators/prop/state-to-prop.test.ts +++ b/arkui-plugins/test/ut/ui-plugins/decorators/prop/state-to-prop.test.ts @@ -44,10 +44,13 @@ import { STATE_MGMT_FACTORY as STATE_MGMT_FACTORY } from "arkui.stateManagement. import { IPropDecoratedVariable as IPropDecoratedVariable } from "arkui.stateManagement.decorator"; -import { memo as memo } from "arkui.stateManagement.runtime"; - import { ButtonAttribute as ButtonAttribute } from "arkui.component.button"; +import { ConditionScope as ConditionScope } from "arkui.component.builder"; + +import { ConditionBranch as ConditionBranch } from "arkui.component.builder"; + +import { memo as memo } from "arkui.stateManagement.runtime"; import { CustomComponent as CustomComponent } from "arkui.component.customComponent"; @@ -94,11 +97,17 @@ function main() {} @memo() public build() { Column(undefined, undefined, @memo() (() => { - if (((this.count) > (0))) { - Text(undefined, (((("You have") + (this.count))) + ("Nuggets left")), undefined, undefined); - } else { - Text(undefined, "Game over!", undefined, undefined); - } + ConditionScope(@memo() (() => { + if (((this.count) > (0))) { + ConditionBranch(@memo() (() => { + Text(undefined, (((("You have") + (this.count))) + ("Nuggets left")), undefined, undefined); + })); + } else { + ConditionBranch(@memo() (() => { + Text(undefined, "Game over!", undefined, undefined); + })); + } + })); Button(@memo() ((instance: ButtonAttribute): void => { instance.onClick(((e: ClickEvent) => { this.count -= this.costOfOneAttempt; diff --git a/arkui-plugins/test/ut/ui-plugins/decorators/reusable/reusable-complex.test.ts b/arkui-plugins/test/ut/ui-plugins/decorators/reusable/reusable-complex.test.ts index 21861a469..f741b2dea 100644 --- a/arkui-plugins/test/ut/ui-plugins/decorators/reusable/reusable-complex.test.ts +++ b/arkui-plugins/test/ut/ui-plugins/decorators/reusable/reusable-complex.test.ts @@ -45,6 +45,10 @@ import { STATE_MGMT_FACTORY as STATE_MGMT_FACTORY } from "arkui.stateManagement. import { IStateDecoratedVariable as IStateDecoratedVariable } from "arkui.stateManagement.decorator"; +import { ConditionScope as ConditionScope } from "arkui.component.builder"; + +import { ConditionBranch as ConditionBranch } from "arkui.component.builder"; + import { ButtonAttribute as ButtonAttribute } from "arkui.component.button"; import { memo as memo } from "arkui.stateManagement.runtime"; @@ -112,13 +116,17 @@ class Message { })); return; }), "Hello", undefined, undefined); - if (this.display) { - Child._instantiateImpl(undefined, (() => { - return new Child(); - }), { - message: new Message("Child"), - }, "Child", undefined); - } + ConditionScope(@memo() (() => { + if (this.display) { + ConditionBranch(@memo() (() => { + Child._instantiateImpl(undefined, (() => { + return new Child(); + }), { + message: new Message("Child"), + }, "Child", undefined); + })); + } + })); })); } diff --git a/arkui-plugins/test/ut/ui-plugins/decorators/state/state-basic-type.test.ts b/arkui-plugins/test/ut/ui-plugins/decorators/state/state-basic-type.test.ts index 5eaa66172..61b13a9fa 100644 --- a/arkui-plugins/test/ut/ui-plugins/decorators/state/state-basic-type.test.ts +++ b/arkui-plugins/test/ut/ui-plugins/decorators/state/state-basic-type.test.ts @@ -164,10 +164,6 @@ function main() {} `; function testParsedAndCheckedTransformer(this: PluginTestContext): void { - console.log("[testParsedAndCheckedTransformer] this: ", this); - console.log("[testParsedAndCheckedTransformer] parseDumpSrc(this.scriptSnapshot ?? ''): ", parseDumpSrc(this.scriptSnapshot ?? '')); - console.log("[testParsedAndCheckedTransformer] parseDumpSrc(expectedScript): ", parseDumpSrc(expectedScript)); - expect(parseDumpSrc(this.scriptSnapshot ?? '')).toBe(parseDumpSrc(expectedScript)); } diff --git a/arkui-plugins/ui-plugins/builder-lambda-translators/factory.ts b/arkui-plugins/ui-plugins/builder-lambda-translators/factory.ts index 1339faaf4..17af3f52a 100644 --- a/arkui-plugins/ui-plugins/builder-lambda-translators/factory.ts +++ b/arkui-plugins/ui-plugins/builder-lambda-translators/factory.ts @@ -21,6 +21,7 @@ import { isDecoratorAnnotation, removeAnnotationByName, forEachArgWithParam, + annotation, } from '../../common/arkts-utils'; import { BuilderLambdaDeclInfo, @@ -43,10 +44,22 @@ import { builderLambdaType, BuilderLambdaSecondLastArgInfo, buildSecondLastArgInfo, + checkShouldBreakFromStatement, + checkIsWithInIfConditionScope, + BuilderLambdaConditionBranchInfo, + BuilderLambdaChainingCallArgInfo, } from './utils'; -import { isDecoratorIntrinsicAnnotation } from '../property-translators/utils'; +import { hasDecorator, isDecoratorIntrinsicAnnotation } from '../property-translators/utils'; import { factory as PropertyFactory } from '../property-translators/factory'; -import { AnimationNames, BindableDecl, DecoratorIntrinsicNames, DecoratorNames } from '../../common/predefines'; +import { factory as UIFactory } from '../ui-factory'; +import { + AnimationNames, + ARKUI_BUILDER_SOURCE_NAME, + BindableDecl, + ConditionNames, + DecoratorIntrinsicNames, + DecoratorNames, +} from '../../common/predefines'; import { ImportCollector } from '../../common/import-collector'; import { addMemoAnnotation, collectMemoableInfoInParameter } from '../../collectors/memo-collectors/utils'; import { factory as MemoCollectFactory } from '../../collectors/memo-collectors/factory'; @@ -95,24 +108,37 @@ export class factory { /* * transform arguments in style node. */ - static getTransformedStyle(call: arkts.CallExpression): arkts.Expression[] { + static getTransformedStyle(call: arkts.CallExpression): BuilderLambdaChainingCallArgInfo[] { const decl = arkts.getDecl(call.expression); if (!decl || !arkts.isMethodDefinition(decl)) { - return [...call.arguments]; - } - const type: arkts.AstNode | undefined = arkts.isEtsParameterExpression(decl.scriptFunction.params[0]) - ? decl.scriptFunction.params[0].type?.clone() - : undefined; - if ( - type && - arkts.isTypeNode(type) && - hasBindableProperty(type, BindableDecl.BINDABLE) && - isDoubleDollarCall(call.arguments[0]) - ) { - const bindableArg: arkts.Expression = (call.arguments[0] as arkts.CallExpression).arguments[0]; - return [factory.updateBindableStyleArguments(bindableArg), ...call.arguments.slice(1)]; + return call.arguments.map((arg) => ({ arg })); } - return [...call.arguments]; + const argInfo: BuilderLambdaChainingCallArgInfo[] = []; + const args = call.arguments; + const params = decl.scriptFunction.params; + const isTrailingCall = call.isTrailingCall; + forEachArgWithParam( + args, + params, + (arg, param, index) => { + const _param = param as arkts.ETSParameterExpression; + let hasBindable: boolean = false; + let isDoubleDollar: boolean = false; + if (index === 0 && !!arg) { + const type = _param.type?.clone(); + hasBindable = !!type && hasBindableProperty(type, BindableDecl.BINDABLE); + isDoubleDollar = isDoubleDollarCall(arg); + } + if (hasBindable && isDoubleDollar && !!arg) { + const bindableArg: arkts.Expression = (arg as arkts.CallExpression).arguments[0]; + argInfo.push({ arg: factory.updateBindableStyleArguments(bindableArg) }); + } else if (!!arg) { + argInfo.push({ arg, hasBuilder: hasDecorator(_param, DecoratorNames.BUILDER) }); + } + }, + { isTrailingCall } + ); + return argInfo; } /* @@ -133,7 +159,7 @@ export class factory { */ static createStyleLambdaBody(lambdaBody: arkts.AstNode, callInfo: InstanceCallInfo): arkts.CallExpression { if (!callInfo.isReceiver) { - const newArgs: arkts.Expression[] = factory.getTransformedStyle(callInfo.call); + const argInfos: BuilderLambdaChainingCallArgInfo[] = factory.getTransformedStyle(callInfo.call); return arkts.factory.createCallExpression( arkts.factory.createMemberExpression( lambdaBody, @@ -143,11 +169,11 @@ export class factory { false ), undefined, - newArgs.map((arg) => { - if (arkts.isArrowFunctionExpression(arg)) { - return this.processArgArrowFunction(arg); + argInfos.map((info) => { + if (arkts.isArrowFunctionExpression(info.arg)) { + return this.processArgArrowFunction(info.arg, info.hasBuilder); } - return arg; + return info.arg; }) ); } else { @@ -249,14 +275,17 @@ export class factory { * If a builder lambda's argument is an arrow function, * then transform any builder lambda in the function body. */ - static processArgArrowFunction(arg: arkts.ArrowFunctionExpression): arkts.ArrowFunctionExpression { + static processArgArrowFunction( + arg: arkts.ArrowFunctionExpression, + hasBuilder?: boolean + ): arkts.ArrowFunctionExpression { const func: arkts.ScriptFunction = arg.scriptFunction; const updateFunc = arkts.factory.updateScriptFunction( func, !!func.body && arkts.isBlockStatement(func.body) ? arkts.factory.updateBlock( func.body, - func.body.statements.map((st) => this.updateContentBodyInBuilderLambda(st)) + func.body.statements.map((st) => this.updateContentBodyInBuilderLambda(st, hasBuilder)) ) : undefined, arkts.FunctionSignature.createFunctionSignature( @@ -349,7 +378,7 @@ export class factory { return fallback; } if (arkts.isArrowFunctionExpression(arg)) { - const newNode = this.processArgArrowFunction(arg); + const newNode = this.processArgArrowFunction(arg, canAddMemo); if (canAddMemo) { addMemoAnnotation(newNode); } @@ -414,29 +443,106 @@ export class factory { } /** - * update if-else in trailing lambda contents in a builder lambda call. + * update if-else in a builder lambda call's arguments. */ - static updateIfElseContentBodyInBuilderLambda(statement: arkts.AstNode): arkts.AstNode { + static updateIfElseContentBodyInBuilderLambda(statement: arkts.AstNode, shouldWrap?: boolean): arkts.AstNode { if (arkts.isIfStatement(statement)) { const alternate = !!statement.alternate - ? this.updateIfElseContentBodyInBuilderLambda(statement.alternate) + ? this.updateIfElseContentBodyInBuilderLambda(statement.alternate, shouldWrap) : statement.alternate; - const consequence = this.updateIfElseContentBodyInBuilderLambda(statement.consequent); - return arkts.factory.updateIfStatement(statement, statement.test, consequence!, alternate); + const consequence = this.updateIfElseContentBodyInBuilderLambda(statement.consequent, shouldWrap); + const newStatement = arkts.factory.updateIfStatement(statement, statement.test, consequence!, alternate); + return !shouldWrap || checkIsWithInIfConditionScope(statement) + ? newStatement + : this.wrapConditionToBlock([newStatement], ConditionNames.CONDITION_SCOPE); } if (arkts.isBlockStatement(statement)) { - return arkts.factory.updateBlock( - statement, - statement.statements.map((st) => this.updateContentBodyInBuilderLambda(st)) - ); + let { statements, breakIndex } = this.updateConditionBranchInScope(statement.statements, shouldWrap); + if (!!shouldWrap && checkIsWithInIfConditionScope(statement)) { + const beforeBreak = this.wrapConditionToBlock( + breakIndex > 0 ? statements.slice(0, breakIndex) : statements, + ConditionNames.CONDITION_BRANCH + ); + const afterBreak = breakIndex > 0 ? statements.slice(breakIndex) : []; + statements = [beforeBreak, ...afterBreak]; + } + return arkts.factory.updateBlock(statement, statements); + } + return statement; + } + + /** + * update switch-case in a builder lambda call's arguments. + */ + static updateSwitchCaseContentBodyInBuilderLambda( + statement: T, + shouldWrap?: boolean + ): T { + if (arkts.isSwitchStatement(statement)) { + const cases = statement.cases.map((c) => this.updateSwitchCaseContentBodyInBuilderLambda(c, shouldWrap)); + const newStatement = arkts.factory.updateSwitchStatement(statement, statement.discriminant, cases); + return (!shouldWrap + ? newStatement + : this.wrapConditionToBlock([newStatement], ConditionNames.CONDITION_SCOPE)) as arkts.AstNode as T; + } + if (arkts.isSwitchCaseStatement(statement)) { + let { statements, breakIndex } = this.updateConditionBranchInScope(statement.consequent, shouldWrap); + if (shouldWrap) { + const beforeBreak = this.wrapConditionToBlock( + breakIndex > 0 ? statements.slice(0, breakIndex) : statements, + ConditionNames.CONDITION_BRANCH + ); + const afterBreak = breakIndex > 0 ? statements.slice(breakIndex) : []; + statements = [beforeBreak, ...afterBreak]; + } + return arkts.factory.updateSwitchCaseStatement(statement, statement.test, statements) as T; } return statement; } + /** + * update ConditionBranch in an if-else or swith-case body. + * @internal + */ + static updateConditionBranchInScope( + statements: readonly arkts.Statement[], + shouldWrap?: boolean + ): BuilderLambdaConditionBranchInfo { + let breakIndex = statements.length - 1; + const newStatements = statements.map((st, index) => { + if (checkShouldBreakFromStatement(st)) { + breakIndex = index; + } + return this.updateContentBodyInBuilderLambda(st, shouldWrap); + }); + return { statements: newStatements, breakIndex }; + } + + /** + * wrap `ConditionScope` or `ConditionBranch` builder function to the block statements. + */ + static wrapConditionToBlock(statements: readonly arkts.AstNode[], condition: ConditionNames): arkts.AstNode { + const contentArg = arkts.factory.createArrowFunction( + UIFactory.createScriptFunction({ + body: arkts.factory.createBlock(statements), + flags: arkts.Es2pandaScriptFunctionFlags.SCRIPT_FUNCTION_FLAGS_ARROW, + modifiers: arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_NONE, + }) + ); + addMemoAnnotation(contentArg); + const newCall = arkts.factory.createCallExpression(arkts.factory.createIdentifier(condition), undefined, [ + contentArg, + ]); + arkts.NodeCache.getInstance().collect(newCall); + ImportCollector.getInstance().collectSource(condition, ARKUI_BUILDER_SOURCE_NAME); + ImportCollector.getInstance().collectImport(condition); + return arkts.factory.createExpressionStatement(newCall); + } + /** * update trailing lambda contents in a builder lambda call. */ - static updateContentBodyInBuilderLambda(statement: arkts.Statement): arkts.Statement { + static updateContentBodyInBuilderLambda(statement: arkts.Statement, hasBuilder?: boolean): arkts.Statement { if ( arkts.isExpressionStatement(statement) && arkts.isCallExpression(statement.expression) && @@ -448,7 +554,10 @@ export class factory { ); } if (arkts.isIfStatement(statement)) { - return this.updateIfElseContentBodyInBuilderLambda(statement); + return this.updateIfElseContentBodyInBuilderLambda(statement, hasBuilder); + } + if (arkts.isSwitchStatement(statement)) { + return this.updateSwitchCaseContentBodyInBuilderLambda(statement, hasBuilder); } return statement; @@ -679,4 +788,67 @@ export class factory { ) ); } + + /** + * create a `@Builder` function with `@Builder` trailing lambda parameter. + */ + static createBuilderWithTrailingLambdaDecl( + name: string, + returnTypeAnnotation: arkts.TypeNode + ): arkts.MethodDefinition { + const key = arkts.factory.createIdentifier(name); + const modifiers = + arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_STATIC | + arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_DECLARE | + arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_EXPORT; + const param = arkts.factory.createParameterDeclaration( + arkts.factory.createIdentifier( + BuilderLambdaNames.CONTENT_PARAM_NAME, + arkts.factory.createFunctionType( + arkts.factory.createFunctionSignature( + undefined, + [], + arkts.factory.createPrimitiveType(arkts.Es2pandaPrimitiveType.PRIMITIVE_TYPE_VOID), + false + ), + arkts.Es2pandaScriptFunctionFlags.SCRIPT_FUNCTION_FLAGS_ARROW + ) + ), + undefined + ); + param.annotations = [annotation(DecoratorNames.BUILDER)]; + arkts.NodeCache.getInstance().collect(param); + const method = UIFactory.createMethodDefinition({ + key, + kind: arkts.Es2pandaMethodDefinitionKind.METHOD_DEFINITION_KIND_NONE, + function: { + key, + params: [param], + returnTypeAnnotation, + flags: arkts.Es2pandaScriptFunctionFlags.SCRIPT_FUNCTION_FLAGS_NONE, + modifiers, + annotations: [annotation(DecoratorNames.BUILDER)], + }, + modifiers, + }); + arkts.NodeCache.getInstance().collect(method); + return method; + } + + /** + * add following declared methods at header file: + * - `@Builder function ConditionScope(@Builder content: () => void): void;` + * - `@Builder function ConditionBranch(@Builder content: () => void): void;` + */ + static addConditionBuilderDecls(): arkts.MethodDefinition[] { + const conditionScope = this.createBuilderWithTrailingLambdaDecl( + ConditionNames.CONDITION_SCOPE, + arkts.factory.createPrimitiveType(arkts.Es2pandaPrimitiveType.PRIMITIVE_TYPE_VOID) + ); + const conditionBranch = this.createBuilderWithTrailingLambdaDecl( + ConditionNames.CONDITION_BRANCH, + arkts.factory.createPrimitiveType(arkts.Es2pandaPrimitiveType.PRIMITIVE_TYPE_VOID) + ); + return [conditionScope, conditionBranch]; + } } diff --git a/arkui-plugins/ui-plugins/builder-lambda-translators/utils.ts b/arkui-plugins/ui-plugins/builder-lambda-translators/utils.ts index 48405da31..711dcfb4a 100644 --- a/arkui-plugins/ui-plugins/builder-lambda-translators/utils.ts +++ b/arkui-plugins/ui-plugins/builder-lambda-translators/utils.ts @@ -45,6 +45,16 @@ export type BuilderLambdaReusableArgInfo = { export type BuilderLambdaSecondLastArgInfo = BuilderLambdaArgInfo & BuilderLambdaReusableArgInfo; +export type BuilderLambdaConditionBranchInfo = { + statements: readonly arkts.Statement[]; + breakIndex: number; +}; + +export type BuilderLambdaChainingCallArgInfo = { + arg: arkts.Expression; + hasBuilder?: boolean; +}; + export function buildSecondLastArgInfo( type: arkts.Identifier | undefined, isFunctionCall: boolean @@ -84,7 +94,7 @@ export function builderLambdaArgumentName(annotation: arkts.AnnotationUsage): st return property.value.str; } -export function isBuilderLambda(node: arkts.AstNode, nodeDecl?:arkts.AstNode | undefined): boolean { +export function isBuilderLambda(node: arkts.AstNode, nodeDecl?: arkts.AstNode | undefined): boolean { const builderLambdaCall: arkts.AstNode | undefined = getDeclForBuilderLambda(node, nodeDecl); if (!builderLambdaCall) { return arkts.isCallExpression(node) && node.arguments.length > 0 && isBuilderLambda(node.arguments[0]); @@ -176,7 +186,10 @@ export function getDeclForBuilderLambdaMethodDecl(node: arkts.AstNode): arkts.As return undefined; } -export function getDeclForBuilderLambda(node: arkts.AstNode, nodeDecl?:arkts.AstNode | undefined): arkts.AstNode | undefined { +export function getDeclForBuilderLambda( + node: arkts.AstNode, + nodeDecl?: arkts.AstNode | undefined +): arkts.AstNode | undefined { if (!node || !arkts.isCallExpression(node)) { return undefined; } @@ -207,7 +220,10 @@ export function getDeclForBuilderLambda(node: arkts.AstNode, nodeDecl?:arkts.Ast return undefined; } -export function isBuilderLambdaCall(node: arkts.CallExpression | arkts.Identifier, nodeDecl?:arkts.AstNode | undefined): boolean { +export function isBuilderLambdaCall( + node: arkts.CallExpression | arkts.Identifier, + nodeDecl?: arkts.AstNode | undefined +): boolean { let decl = nodeDecl; if (decl === undefined) { const expr = arkts.isIdentifier(node) ? node : node.expression; @@ -543,3 +559,16 @@ export function collectComponentAttributeImport(type: arkts.TypeNode | undefined ImportCollector.getInstance().collectImport(attributeName); } } + +export function checkIsWithInIfConditionScope(statement: arkts.AstNode): boolean { + if (!statement.parent) { + return false; + } + return arkts.isBlockStatement(statement) && arkts.isIfStatement(statement.parent); +} + +export function checkShouldBreakFromStatement(statement: arkts.AstNode): boolean { + return ( + arkts.isReturnStatement(statement) || arkts.isBreakStatement(statement) || arkts.isContinueStatement(statement) + ); +} diff --git a/arkui-plugins/ui-plugins/checked-transformer.ts b/arkui-plugins/ui-plugins/checked-transformer.ts index dd51fd255..8fe41f30c 100644 --- a/arkui-plugins/ui-plugins/checked-transformer.ts +++ b/arkui-plugins/ui-plugins/checked-transformer.ts @@ -185,7 +185,7 @@ export class CheckedTransformer extends AbstractVisitor { entryFactory.addMemoToEntryWrapperClassMethods(node); return node; } else if (arkts.isClassDeclaration(node)) { - return structFactory.transformNormalClass(node); + return structFactory.transformNormalClass(node, this.externalSourceName); } else if (arkts.isCallExpression(node)) { return structFactory.transformCallExpression(node, this.projectConfig, this.resourceInfo); } else if (arkts.isMethodDefinition(node) && isForEachDecl(node, this.externalSourceName)) { diff --git a/arkui-plugins/ui-plugins/struct-translators/factory.ts b/arkui-plugins/ui-plugins/struct-translators/factory.ts index a9d420d21..1d7c8fc83 100644 --- a/arkui-plugins/ui-plugins/struct-translators/factory.ts +++ b/arkui-plugins/ui-plugins/struct-translators/factory.ts @@ -26,8 +26,9 @@ import { isCustomDialogControllerOptions, isKnownMethodDefinition, } from '../utils'; -import { factory as uiFactory } from '../ui-factory'; -import { factory as propertyFactory } from '../property-translators/factory'; +import { factory as UIFactory } from '../ui-factory'; +import { factory as PropertyFactory } from '../property-translators/factory'; +import { factory as BuilderLambdaFactory } from '../builder-lambda-translators/factory'; import { backingField, collect, filterDefined } from '../../common/arkts-utils'; import { classifyObservedTrack, @@ -72,7 +73,7 @@ import { ModuleType, StateManagementTypes, RESOURCE_TYPE, - CUSTOM_DIALOG_CONTROLLER_SOURCE_NAME, + ARKUI_BUILDER_SOURCE_NAME, } from '../../common/predefines'; import { ObservedTrackTranslator } from '../property-translators/observedTrack'; import { addMemoAnnotation } from '../../collectors/memo-collectors/utils'; @@ -110,7 +111,7 @@ export class factory { body, arkts.FunctionSignature.createFunctionSignature( undefined, - [uiFactory.createInitializersOptionsParameter(optionsTypeName), uiFactory.createContentParameter()], + [UIFactory.createInitializersOptionsParameter(optionsTypeName), UIFactory.createContentParameter()], arkts.factory.createPrimitiveType(arkts.Es2pandaPrimitiveType.PRIMITIVE_TYPE_VOID), false ), @@ -148,7 +149,7 @@ export class factory { } static updateBuilderType(builderNode: arkts.MethodDefinition): arkts.MethodDefinition { - const newType: arkts.TypeNode | undefined = uiFactory.createTypeReferenceFromString( + const newType: arkts.TypeNode | undefined = UIFactory.createTypeReferenceFromString( CustomDialogNames.CUSTOM_BUILDER ); if (builderNode.kind === arkts.Es2pandaMethodDefinitionKind.METHOD_DEFINITION_KIND_GET) { @@ -170,7 +171,7 @@ export class factory { param.initializer ); if (!!newParam) { - return uiFactory.updateMethodDefinition(builderNode, { function: { params: [newParam] } }); + return UIFactory.updateMethodDefinition(builderNode, { function: { params: [newParam] } }); } } return builderNode; @@ -197,7 +198,7 @@ export class factory { body, arkts.FunctionSignature.createFunctionSignature( undefined, - [uiFactory.createInitializersOptionsParameter(optionsTypeName)], + [UIFactory.createInitializersOptionsParameter(optionsTypeName)], arkts.factory.createPrimitiveType(arkts.Es2pandaPrimitiveType.PRIMITIVE_TYPE_VOID), false ), @@ -347,11 +348,11 @@ export class factory { * helper to create value parameter for AnimatableExtend methods */ static createAniExtendValueParam(): arkts.ETSParameterExpression { - const numberType = uiFactory.createTypeReferenceFromString('number'); + const numberType = UIFactory.createTypeReferenceFromString('number'); const AnimatableArithmeticType = arkts.factory.createTypeReference( arkts.factory.createTypeReferencePart( arkts.factory.createIdentifier(AnimationNames.ANIMATABLE_ARITHMETIC), - arkts.factory.createTSTypeParameterInstantiation([uiFactory.createTypeReferenceFromString('T')]) + arkts.factory.createTSTypeParameterInstantiation([UIFactory.createTypeReferenceFromString('T')]) ) ); return arkts.factory.createParameterDeclaration( @@ -368,7 +369,7 @@ export class factory { */ static createOrSetAniProperty(): arkts.MethodDefinition { const funcNameParam: arkts.ETSParameterExpression = arkts.factory.createParameterDeclaration( - arkts.factory.createIdentifier('functionName', uiFactory.createTypeReferenceFromString('string')), + arkts.factory.createIdentifier('functionName', UIFactory.createTypeReferenceFromString('string')), undefined ); const cbParam = arkts.factory.createParameterDeclaration( @@ -389,7 +390,7 @@ export class factory { return arkts.factory.createMethodDefinition( arkts.Es2pandaMethodDefinitionKind.METHOD_DEFINITION_KIND_METHOD, arkts.factory.createIdentifier(AnimationNames.CREATE_OR_SET_ANIMATABLEPROPERTY), - uiFactory.createScriptFunction({ + UIFactory.createScriptFunction({ typeParams: arkts.factory.createTypeParameterDeclaration( [arkts.factory.createTypeParameter(arkts.factory.createIdentifier('T'))], 0 @@ -466,7 +467,7 @@ export class factory { */ static transformNonPropertyMembersInClass(member: arkts.AstNode, isDecl?: boolean): arkts.AstNode { if (arkts.isMethodDefinition(member)) { - propertyFactory.addMemoToBuilderClassMethod(member); + PropertyFactory.addMemoToBuilderClassMethod(member); if (isKnownMethodDefinition(member, CustomComponentNames.COMPONENT_CONSTRUCTOR_ORI) && !isDecl) { return this.setStructConstructorToPrivate(member); } @@ -605,7 +606,7 @@ export class factory { const param: arkts.ETSParameterExpression = arkts.factory.createParameterDeclaration( arkts.factory.createIdentifier( CustomDialogNames.CONTROLLER, - uiFactory.createTypeReferenceFromString(CustomDialogNames.CUSTOM_DIALOG_CONTROLLER) + UIFactory.createTypeReferenceFromString(CustomDialogNames.CUSTOM_DIALOG_CONTROLLER) ), undefined ); @@ -739,7 +740,7 @@ export class factory { static tranformInterfaceBuildMember(node: arkts.TSInterfaceDeclaration): arkts.TSInterfaceDeclaration { const newBody: arkts.AstNode[] = node.body!.body.map((it) => { if (arkts.isMethodDefinition(it)) { - propertyFactory.addMemoToBuilderClassMethod(it); + PropertyFactory.addMemoToBuilderClassMethod(it); } return it; }); @@ -784,40 +785,51 @@ export class factory { return node; } - static transformNormalClass(node: arkts.ClassDeclaration): arkts.ClassDeclaration { + static transformETSGlobalClass(node: arkts.ClassDeclaration, externalSourceName?: string): arkts.ClassDeclaration { + if (!node.definition) { + return node; + } + 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( + member, + member.kind, + member.name, + factory.transformAnimatableExtend(member.scriptFunction), + member.modifiers, + false + ); + } + return member; + }); + let newStatements: arkts.AstNode[] = []; + if (externalSourceName === ARKUI_BUILDER_SOURCE_NAME) { + newStatements.push(...BuilderLambdaFactory.addConditionBuilderDecls()); + } + return arkts.factory.updateClassDeclaration( + node, + arkts.factory.updateClassDefinition( + node.definition, + node.definition.ident, + node.definition.typeParams, + node.definition.superTypeParams, + node.definition.implements, + undefined, + node.definition.super, + [...updatedBody, ...newStatements], + node.definition.modifiers, + arkts.classDefinitionFlags(node.definition) + ) + ); + } + + static transformNormalClass(node: arkts.ClassDeclaration, externalSourceName?: string): arkts.ClassDeclaration { if (!node.definition) { return node; } if (isEtsGlobalClass(node)) { - 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( - member, - member.kind, - member.name, - factory.transformAnimatableExtend(member.scriptFunction), - member.modifiers, - false - ); - } - return member; - }); - return arkts.factory.updateClassDeclaration( - node, - arkts.factory.updateClassDefinition( - node.definition, - node.definition.ident, - node.definition.typeParams, - node.definition.superTypeParams, - node.definition.implements, - undefined, - node.definition.super, - updatedBody, - node.definition.modifiers, - arkts.classDefinitionFlags(node.definition) - ) - ); + return this.transformETSGlobalClass(node, externalSourceName); } const newClassDef = factory.updateObservedTrackClassDef(node.definition); return arkts.factory.updateClassDeclaration(node, newClassDef); @@ -868,9 +880,9 @@ export class factory { definition: arkts.ClassDefinition, isObserved: boolean ): arkts.AstNode[] { - const watchMembers: arkts.AstNode[] = propertyFactory.createWatchMembers(); - const v1RenderIdMembers: arkts.AstNode[] = propertyFactory.createV1RenderIdMembers(); - const conditionalAddRef: arkts.MethodDefinition = propertyFactory.conditionalAddRef(); + const watchMembers: arkts.AstNode[] = PropertyFactory.createWatchMembers(); + const v1RenderIdMembers: arkts.AstNode[] = PropertyFactory.createV1RenderIdMembers(); + const conditionalAddRef: arkts.MethodDefinition = PropertyFactory.conditionalAddRef(); const getters: arkts.MethodDefinition[] = getGettersFromClassDecl(definition); const classScopeInfo: ClassScopeInfo = { isObserved: isObserved, @@ -891,7 +903,7 @@ export class factory { ); return [ ...[...watchMembers, ...v1RenderIdMembers, conditionalAddRef], - ...(classHasTrack ? [] : [propertyFactory.createMetaInObservedClass()]), + ...(classHasTrack ? [] : [PropertyFactory.createMetaInObservedClass()]), ...collect(...propertyMembers), ...nonClassPropertyOrGetter, ...classScopeInfo.getters, @@ -912,7 +924,7 @@ export class factory { arkts.factory.createTSAsExpression(param.identifier.clone(), param.type as arkts.TypeNode, false) ) ); - const numberType = uiFactory.createTypeReferenceFromString('number'); + const numberType = UIFactory.createTypeReferenceFromString('number'); const AnimatableArithmeticType = arkts.factory.createTypeReference( arkts.factory.createTypeReferencePart( arkts.factory.createIdentifier(AnimationNames.ANIMATABLE_ARITHMETIC), @@ -921,7 +933,7 @@ export class factory { ); ImportCollector.getInstance().collectImport(AnimationNames.ANIMATABLE_ARITHMETIC); return arkts.factory.createArrowFunction( - uiFactory.createScriptFunction({ + UIFactory.createScriptFunction({ body: arkts.factory.createBlock([assignmentExpr, ...originStatements]), params: [ arkts.factory.createParameterDeclaration( @@ -993,11 +1005,11 @@ export class factory { ) { return node; } - const referenceType = uiFactory.createComplexTypeFromStringAndTypeParameter('Array', [ + const referenceType = UIFactory.createComplexTypeFromStringAndTypeParameter('Array', [ argTypeParam.type.clone(), ]); const newArrowArg: arkts.ArrowFunctionExpression = arkts.factory.createArrowFunction( - uiFactory.createScriptFunction({ + UIFactory.createScriptFunction({ body: arkts.factory.createBlock([arkts.factory.createReturnStatement(node.arguments[0])]), returnTypeAnnotation: referenceType, flags: arkts.Es2pandaScriptFunctionFlags.SCRIPT_FUNCTION_FLAGS_ARROW, @@ -1018,12 +1030,12 @@ export class factory { if (!arkts.isEtsParameterExpression(paramFirst) || !paramFirst.type || !arkts.isTypeNode(paramFirst.type)) { return node; } - const script = uiFactory.updateScriptFunction(node.scriptFunction, { + const script = UIFactory.updateScriptFunction(node.scriptFunction, { params: [ arkts.factory.createParameterDeclaration( arkts.factory.createIdentifier( paramFirst.identifier.name, - uiFactory.createLambdaFunctionType([], paramFirst.type) + UIFactory.createLambdaFunctionType([], paramFirst.type) ), undefined ), @@ -1104,7 +1116,7 @@ export class factory { ) { return addMemoAnnotation( arkts.factory.createArrowFunction( - uiFactory.createScriptFunction({ + UIFactory.createScriptFunction({ body: arkts.factory.createBlock([ arkts.factory.createExpressionStatement( this.transformCustomDialogComponentCall(value, controllerInfo, gensymName) @@ -1129,7 +1141,7 @@ export class factory { ): arkts.CallExpression { if (value.arguments.length >= 2 && arkts.isArrowFunctionExpression(value.arguments[1])) { const originScript: arkts.ScriptFunction = value.arguments[1].scriptFunction; - const newScript: arkts.ScriptFunction = uiFactory.updateScriptFunction(originScript, { + const newScript: arkts.ScriptFunction = UIFactory.updateScriptFunction(originScript, { body: this.generateInstanceSetController(originScript.body, controllerInfo, gensymName), }); return arkts.factory.updateCallExpression(value, value.expression, value.typeArguments, [ @@ -1194,7 +1206,7 @@ export class factory { controllerInfo.isClassProperty ? generateThisBacking(controllerInfo.controllerName) : arkts.factory.createIdentifier(gensymName), - uiFactory.createTypeReferenceFromString(CustomDialogNames.CUSTOM_DIALOG_CONTROLLER), + UIFactory.createTypeReferenceFromString(CustomDialogNames.CUSTOM_DIALOG_CONTROLLER), false ), ] diff --git a/koala-wrapper/src/arkts-api/index.ts b/koala-wrapper/src/arkts-api/index.ts index 1b69fb0ac..e09dfff6d 100644 --- a/koala-wrapper/src/arkts-api/index.ts +++ b/koala-wrapper/src/arkts-api/index.ts @@ -64,9 +64,12 @@ export * from '../generated/peers/ArrayExpression'; export * from '../generated/peers/TryStatement'; export * from '../generated/peers/ETSNullType'; export * from '../generated/peers/ETSTuple'; -export * from '../generated/peers/ImportDeclaration' -export * from '../generated/peers/WhileStatement' -export * from '../generated/peers/BreakStatement' +export * from '../generated/peers/ImportDeclaration'; +export * from '../generated/peers/WhileStatement'; +export * from '../generated/peers/ContinueStatement'; +export * from '../generated/peers/BreakStatement'; +export * from '../generated/peers/SwitchCaseStatement'; +export * from '../generated/peers/SwitchStatement'; export * from './types'; export * from './utilities/private'; -- Gitee