diff --git a/ets-tests/ets/pages/builder.ets b/ets-tests/ets/pages/builder.ets new file mode 100755 index 0000000000000000000000000000000000000000..99ca98f94e76d22d6bc862d6a7b597851329d77f --- /dev/null +++ b/ets-tests/ets/pages/builder.ets @@ -0,0 +1,35 @@ +/* + * 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. + */ + +class Params { + param: string = '' +} + +@Builder +function builder(params: Params) { + TestComponent() + .log(`builder:${params.param}`) +} + +@Entry +@Component +struct BuilderTest { + label: string = 'Hello' + build() { + Column() { + builder({ param: this.label }) + } + } +} diff --git a/ets-tests/src/Page.ts b/ets-tests/src/Page.ts index 380997ad63ef2d95de78a541e25b09310cf5a56c..27466c81102430a6860daffc1f9b0d5d937b3c79 100644 --- a/ets-tests/src/Page.ts +++ b/ets-tests/src/Page.ts @@ -19,6 +19,7 @@ import { Case2 } from "../build/generated/pages/case2" import { Case3 } from "../build/generated/pages/case3" import { InitializationFromParent } from "../build/generated/pages/InitializationFromParent" import { ObservableArray } from "../build/generated/pages/ObservableArray" +import { BuilderTest } from "../build/generated/pages/builder" import { StylesTest } from "../build/generated/pages/styles" import { LinkDecorator } from "../build/generated/pages/LinkDecorator" import { StorageLinkUpdateTest } from "../build/generated/pages/StorageLinkUpdateTest" @@ -28,6 +29,7 @@ const cases: Map = new Map() .set("Case2", Case2) .set("Case3", Case3) .set("InitializationFromParent", InitializationFromParent) + .set("Builder", BuilderTest) .set("Styles", StylesTest) .set("LinkDecorator", LinkDecorator) .set("ObservableArray", ObservableArray) diff --git a/ets-tests/src/entry.ts b/ets-tests/src/entry.ts index ce96e8f88ffb74c349311a376b2f5ce2ad2277df..2acc1a32927d55996c91054c0f226f10a81d6499 100644 --- a/ets-tests/src/entry.ts +++ b/ets-tests/src/entry.ts @@ -120,6 +120,17 @@ export function entry(control: AppControl) { Assert.equal(expected, actual, "ComponentLifeCycle: Case3 test is failed!") }) }) + suite("Builders", () => { + test("Object literal in @Builder", () => { + const expected = "builder:Hello\n" + const actual = control + .start() + .loadPage("Builder") + .stop() + .getLog() + Assert.equal(expected, actual) + }) + }) suite("Styles", () => { test("Extend+AnimatableExtend", () => { const expected = "extend:20\nanimatable-extend:80\n" diff --git a/incremental/compiler-plugin/src/function-transformer.ts b/incremental/compiler-plugin/src/function-transformer.ts index 7f37a8b6a210ee77501d52784dd90b26e939df35..d1eab74de3aa90cde7f3a5283b7c26179652add1 100644 --- a/incremental/compiler-plugin/src/function-transformer.ts +++ b/incremental/compiler-plugin/src/function-transformer.ts @@ -35,6 +35,7 @@ import { skipParenthesizedExpression, isThis, isThisStable, + getDeclarationsByNode, } from "./util" @@ -472,10 +473,24 @@ function foo1(p1, p2): R { } wrapInCompute(node: ts.ObjectLiteralExpression | ts.ArrowFunction | ts.AsExpression): ts.Expression { - if (ts.isAsExpression(node)) { - if (!ts.isObjectLiteralExpression(node.expression)) return node + if (ts.isAsExpression(node) && !ts.isObjectLiteralExpression(node.expression)) { + return node } - return wrapInCompute(node, idPlusKey(this.rewrite.positionalIdTracker)) + const id = idPlusKey(this.rewrite.positionalIdTracker) + if (ts.isObjectLiteralExpression(node) && ts.isCallExpression(node.parent)) { + // When an object literal is passed, ArkTS may fail to infer its type, + // so let's provide an explicit type argument + ts.Utils.setTypeChecker(this.typechecker) + const funcSym = ts.Utils.getSymbolOfCallExpression(node.parent) + const funcDecl = ts.Utils.getDeclaration(funcSym) + if (funcDecl && (ts.isFunctionDeclaration(funcDecl) || ts.isMethodDeclaration(funcDecl))) { + const computeTypeArgument = funcDecl.parameters[0].type + ///find out valueDecl's source file and its relative path + ///generate import stmt + return wrapInCompute(node, id, computeTypeArgument) + } + } + return wrapInCompute(node, id) } transformTransformedType(node: ts.TypeReferenceNode): ts.TypeReferenceNode { diff --git a/incremental/compiler-plugin/src/util.ts b/incremental/compiler-plugin/src/util.ts index de42c978dc5ef92399782db9a048a7a098274ee4..5533b382ac5946789b293c5731d662a71f1831ad 100644 --- a/incremental/compiler-plugin/src/util.ts +++ b/incremental/compiler-plugin/src/util.ts @@ -262,13 +262,13 @@ export class PositionalIdTracker { } } -export function wrapInCompute(node: ts.ConciseBody, id: ts.Expression): ts.Expression { +export function wrapInCompute(node: ts.ConciseBody, id: ts.Expression, typeArg?: ts.TypeNode): ts.Expression { return ts.factory.createCallExpression( ts.factory.createPropertyAccessExpression( runtimeIdentifier(RuntimeNames.CONTEXT), runtimeIdentifier(RuntimeNames.COMPUTE) ), - /*typeArguments*/ undefined, + typeArg ? [typeArg] : undefined, [ id, ts.factory.createArrowFunction( diff --git a/incremental/compiler-plugin/test/examples/builder-usage.ts b/incremental/compiler-plugin/test/examples/builder-usage.ts new file mode 100644 index 0000000000000000000000000000000000000000..db5d76a4641ee60a845685cd2d60943cd4733b01 --- /dev/null +++ b/incremental/compiler-plugin/test/examples/builder-usage.ts @@ -0,0 +1,21 @@ +/* + * 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 { builder } from "./builder" + +/** @memo */ +function useBuilder() { + builder({ param: "value" }); +} diff --git a/incremental/compiler-plugin/test/examples/builder.ts b/incremental/compiler-plugin/test/examples/builder.ts new file mode 100644 index 0000000000000000000000000000000000000000..a59191385a6cff44e2732b8c7ab56c0ea827d8e7 --- /dev/null +++ b/incremental/compiler-plugin/test/examples/builder.ts @@ -0,0 +1,27 @@ +/* + * 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. + */ + +export class Params { + param: string = '' +} + +/** @memo */ +export function builder(params: Params) { +} + +/** @memo */ +function useBuilder() { + builder({ param: "value" }); +} diff --git a/incremental/compiler-plugin/test/golden/examples/builder-usage.ts b/incremental/compiler-plugin/test/golden/examples/builder-usage.ts new file mode 100644 index 0000000000000000000000000000000000000000..81e4a505b59e10dd099d47d1edee7e796750f8cd --- /dev/null +++ b/incremental/compiler-plugin/test/golden/examples/builder-usage.ts @@ -0,0 +1,30 @@ +/* + * 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 { builder } from "./builder"; +import { __memo_context_type, __memo_id_type } from "./context.test"; +/** @memo */ +function useBuilder(__memo_context: __memo_context_type, __memo_id: __memo_id_type) { + const __memo_scope = __memo_context.scope(__memo_id + ("2___key_id_DIRNAME/builder-usage.ts" as __memo_id_type)); + if (__memo_scope.unchanged) { + __memo_scope.cached; + return; + } + builder(__memo_context, __memo_id + ("1___key_id_DIRNAME/builder-usage.ts" as __memo_id_type), __memo_context.compute(__memo_id + ("0___key_id_DIRNAME/builder-usage.ts" as __memo_id_type), () => ({ param: "value" }))); + { + __memo_scope.recache(); + return; + } +} + diff --git a/incremental/compiler-plugin/test/golden/examples/builder.ts b/incremental/compiler-plugin/test/golden/examples/builder.ts new file mode 100644 index 0000000000000000000000000000000000000000..470a603fe405de805f2d338cdc49c1b48cb1a100 --- /dev/null +++ b/incremental/compiler-plugin/test/golden/examples/builder.ts @@ -0,0 +1,45 @@ +import { __memo_context_type, __memo_id_type } from "./context.test"; +/* + * 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. + */ +export class Params { + param: string = ''; +} +/** @memo */ +export function builder(__memo_context: __memo_context_type, __memo_id: __memo_id_type, params: Params) { + const __memo_scope = __memo_context.scope(__memo_id + ("0___key_id_DIRNAME/builder.ts" as __memo_id_type), 1); + const __memo_parameter_params = __memo_scope.param(0, params); + if (__memo_scope.unchanged) { + __memo_scope.cached; + return; + } + { + __memo_scope.recache(); + return; + } +} +/** @memo */ +function useBuilder(__memo_context: __memo_context_type, __memo_id: __memo_id_type) { + const __memo_scope = __memo_context.scope(__memo_id + ("3___key_id_DIRNAME/builder.ts" as __memo_id_type)); + if (__memo_scope.unchanged) { + __memo_scope.cached; + return; + } + builder(__memo_context, __memo_id + ("2___key_id_DIRNAME/builder.ts" as __memo_id_type), __memo_context.compute(__memo_id + ("1___key_id_DIRNAME/builder.ts" as __memo_id_type), () => ({ param: "value" }))); + { + __memo_scope.recache(); + return; + } +} + diff --git a/incremental/compiler-plugin/test/whole_file.test.ts b/incremental/compiler-plugin/test/whole_file.test.ts index 56d9801c0169cb5eac1dfc943886b735503cab8e..38961dcfcfb71e27eabb41a7067663f8dca619e6 100644 --- a/incremental/compiler-plugin/test/whole_file.test.ts +++ b/incremental/compiler-plugin/test/whole_file.test.ts @@ -39,3 +39,12 @@ suite("Local export as", () => { () => assertUnmemoizedEqualGolden("examples/localexported.ts") ) }) + +suite("@Builder", () => { + test("Object literal parameter, single file", + () => assertUnmemoizedEqualGolden("examples/builder.ts") + ) + test("Object literal parameter, multiple files", + () => assertUnmemoizedEqualGolden("examples/builder-usage.ts") + ) +})