From fece0a9ad42ced61cd55d8e12c5aa583b72ce03a Mon Sep 17 00:00:00 2001 From: vadimdolgachev Date: Fri, 23 May 2025 16:58:54 +0700 Subject: [PATCH 1/6] Use proxy only for class objects --- incremental/compat/src/arkts/observable.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/incremental/compat/src/arkts/observable.ts b/incremental/compat/src/arkts/observable.ts index 81f0d7a1b..edf20df42 100644 --- a/incremental/compat/src/arkts/observable.ts +++ b/incremental/compat/src/arkts/observable.ts @@ -210,9 +210,11 @@ export function observableProxy(value: Value, parent?: ObservableHandler, } else if (value instanceof Date) { return ObservableDate(value, parent, observed) as Value } - if (PROXY_DISABLED) return value as Value // TODO: proxy the given object - return Proxy.create(value as Object, new CustomProxyHandler()) as Value + if (!PROXY_DISABLED && Type.of(value) instanceof ClassType) { + return Proxy.create(value as Object, new CustomProxyHandler()) as Value + } + return value as Value } class CustomProxyHandler extends DefaultProxyHandler { -- Gitee From 761d0170153e2b09b780ab5e03168b20a3933547 Mon Sep 17 00:00:00 2001 From: vadimdolgachev Date: Fri, 23 May 2025 20:29:52 +0700 Subject: [PATCH 2/6] Added CustomProxyHandler.invoke --- incremental/compat/src/arkts/observable.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/incremental/compat/src/arkts/observable.ts b/incremental/compat/src/arkts/observable.ts index edf20df42..4d148fe76 100644 --- a/incremental/compat/src/arkts/observable.ts +++ b/incremental/compat/src/arkts/observable.ts @@ -233,6 +233,12 @@ class CustomProxyHandler extends DefaultProxyHandler { } return super.set(target, name, value) } + + override invoke(target: T, method: Method, args: FixedArray): NullishType { + const observable = ObservableHandler.find(target) + if (observable) observable.onAccess() + return super.invoke(target, method, args) + } } function proxyChildrenOnly(array: T[], parent: ObservableHandler, observed?: boolean) { -- Gitee From bf6fc51633d188b1af1fc935c38fd65d6f74d85c Mon Sep 17 00:00:00 2001 From: vadimdolgachev Date: Fri, 23 May 2025 20:31:11 +0700 Subject: [PATCH 3/6] Added setter/getter function generation --- .../ui-plugins/src/component-transformer.ts | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) diff --git a/ui2abc/ui-plugins/src/component-transformer.ts b/ui2abc/ui-plugins/src/component-transformer.ts index d90f6a6c4..889661820 100644 --- a/ui2abc/ui-plugins/src/component-transformer.ts +++ b/ui2abc/ui-plugins/src/component-transformer.ts @@ -379,6 +379,27 @@ export class ComponentTransformer extends arkts.AbstractVisitor { result.push(this.rewriteStructToOptions(node)) } + private rewriteClass(clazz: arkts.ClassDeclaration): arkts.ClassDeclaration { + console.log(`rewriteClass=${clazz.definition?.ident?.name}`) + const classBody: arkts.Statement[] = [] + clazz.definition?.body.forEach((element) => + classBody.push(new CustomClassBodyRewriter(classBody).visitor(element))) + + return arkts.factory.createClassDeclaration( + arkts.factory.createClassDefinition( + clazz.definition!.ident, + clazz.definition?.typeParams, + clazz.definition?.superTypeParams, + clazz.definition?.implements ?? [], + clazz.definition?.ctor, + clazz.definition?.super, + classBody, + clazz.definition?.modifiers!, + clazz.definition?.modifierFlags! + ) + ) + } + visitor(node: arkts.AstNode): arkts.AstNode { if (arkts.isETSModule(node)) { return this.rewriteModule(node) @@ -415,3 +436,109 @@ function createVoidMethod(methodName: string, parameters: readonly arkts.Express false ) } + +class CustomClassBodyRewriter extends arkts.AbstractVisitor { + extraStmts: arkts.Statement[] = [] + isConstructorContext = false + + constructor(extra: arkts.Statement[]) { + super() + this.extraStmts = extra + } + + visitor(node: arkts.AstNode): arkts.AstNode { + if (arkts.isMethodDefinition(node) && node.isConstructor) { + this.isConstructorContext = true + const result = this.visitEachChild(node) + this.isConstructorContext = false + return result + } else if (arkts.isClassProperty(node)) { + this.generateGetterSetter(node, this.extraStmts) + return this.rewriteClassProperty(node) + } else if (this.isConstructorContext && arkts.isMemberExpression(node) && arkts.isIdentifier(node.property)) { + return generateThisBacking(this.getBackingPropName(node.property.name)) + } + + return this.visitEachChild(node) + } + + private getBackingPropName(propName: string): string { + return "__" + propName + } + + private rewriteClassProperty(property: arkts.ClassProperty) { + return arkts.factory.createClassProperty( + arkts.factory.createIdentifier(this.getBackingPropName(property.id?.name!)), + property.value, + property.typeAnnotation, + arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_PRIVATE, + false + ) + } + + private generateGetterSetter(property: arkts.ClassProperty, result: arkts.Statement[]) { + const origPropName = property.id?.name! + const backingPropName = this.getBackingPropName(origPropName) + + const getterFunction = arkts.factory.createScriptFunction( + arkts.factory.createBlockStatement([arkts.factory.createReturnStatement(generateThisBacking(backingPropName))]), + undefined, + [], + property.typeAnnotation, + true, + arkts.Es2pandaScriptFunctionFlags.SCRIPT_FUNCTION_FLAGS_METHOD + | arkts.Es2pandaScriptFunctionFlags.SCRIPT_FUNCTION_FLAGS_GETTER, + arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_PUBLIC, + arkts.factory.createIdentifier(origPropName), + [] + ) + + const setterFunction = arkts.factory.createScriptFunction( + arkts.factory.createBlockStatement([ + arkts.factory.createExpressionStatement( + arkts.factory.createAssignmentExpression( + generateThisBacking(backingPropName), + arkts.factory.createIdentifier("value"), + Es2pandaTokenType.TOKEN_TYPE_PUNCTUATOR_SUBSTITUTION + ) + ) + ]), + undefined, + [ + arkts.factory.createETSParameterExpression( + arkts.factory.createIdentifier("value"), + false, + undefined, + property.typeAnnotation! + ) + ], + undefined, + true, + arkts.Es2pandaScriptFunctionFlags.SCRIPT_FUNCTION_FLAGS_METHOD + | arkts.Es2pandaScriptFunctionFlags.SCRIPT_FUNCTION_FLAGS_SETTER + | arkts.Es2pandaScriptFunctionFlags.SCRIPT_FUNCTION_FLAGS_OVERLOAD, + arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_PUBLIC, + arkts.factory.createIdentifier(origPropName), + [] + ) + + const setter = arkts.factory.createMethodDefinition( + arkts.Es2pandaMethodDefinitionKind.METHOD_DEFINITION_KIND_SET, + arkts.factory.createIdentifier(origPropName), + arkts.factory.createFunctionExpression(arkts.factory.createIdentifier(origPropName), setterFunction), + arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_PUBLIC, + false + ) + + const getter = arkts.factory.createMethodDefinition( + arkts.Es2pandaMethodDefinitionKind.METHOD_DEFINITION_KIND_GET, + arkts.factory.createIdentifier(origPropName), + arkts.factory.createFunctionExpression(arkts.factory.createIdentifier(origPropName), getterFunction), + arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_PUBLIC, + false, + [setter] + ) + setter.parent = getter + result.push(getter) + } +} \ No newline at end of file -- Gitee From edc962edf0f539db399ddadbfa9fb5f64fab203d Mon Sep 17 00:00:00 2001 From: vadimdolgachev Date: Mon, 26 May 2025 17:59:59 +0700 Subject: [PATCH 4/6] Added collection of types used in the struct --- ui2abc/ui-plugins/src/struct-recorder.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ui2abc/ui-plugins/src/struct-recorder.ts b/ui2abc/ui-plugins/src/struct-recorder.ts index e225a971c..def93ce48 100644 --- a/ui2abc/ui-plugins/src/struct-recorder.ts +++ b/ui2abc/ui-plugins/src/struct-recorder.ts @@ -118,6 +118,7 @@ export class StructsResolver { export class StructTable { private structByName = new Map() + private usedTypeName = new Set() constructor(private fileName: string, private resolverToSync: StructsResolver|undefined) { if (!resolverToSync) { @@ -187,6 +188,14 @@ export class StructTable { let result = `{"structs": [ ${lines.join(",")} ]}` fs.writeFileSync(metaDatabase(this.fileName), result) } + + addUsedTypeName(name: string) { + this.usedTypeName.add(name) + } + + isUsedTypeName(typeName: string): boolean { + return this.usedTypeName.has(typeName) + } } export class StructRecorder extends arkts.AbstractVisitor { @@ -195,6 +204,9 @@ export class StructRecorder extends arkts.AbstractVisitor { } visitor(node: arkts.AstNode): arkts.AstNode { + if (arkts.isETSNewClassInstanceExpression(node) && arkts.isETSTypeReference(node.typeRef)) { + this.table.addUsedTypeName(node.typeRef.baseName?.name!) + } if (arkts.isETSModule(node)) { const result = this.visitEachChild(node) this.table.update() -- Gitee From bb7178c91fbac96d1a49711e57b70c03fd4f9121 Mon Sep 17 00:00:00 2001 From: vadimdolgachev Date: Mon, 26 May 2025 23:01:10 +0700 Subject: [PATCH 5/6] Fixed monitoring of custom classes --- ets-tests/ets/AllPages.ets | 2 + .../ets/pages/states/ObservableObject.ets | 39 ++++ ets-tests/ets/suites/LinkDecorator.ets | 4 +- ets-tests/ets/suites/ProvideConsume.ets | 46 ++--- ets-tests/ets/suites/StateManagement.ets | 10 +- incremental/compat/src/arkts/observable.ts | 20 ++ ui2abc/ui-plugins/src/class-transformer.ts | 192 ++++++++++++++++++ .../ui-plugins/src/component-transformer.ts | 167 ++++----------- 8 files changed, 324 insertions(+), 156 deletions(-) create mode 100644 ets-tests/ets/pages/states/ObservableObject.ets create mode 100644 ui2abc/ui-plugins/src/class-transformer.ts diff --git a/ets-tests/ets/AllPages.ets b/ets-tests/ets/AllPages.ets index ef1ef34fd..7dc4ef13e 100644 --- a/ets-tests/ets/AllPages.ets +++ b/ets-tests/ets/AllPages.ets @@ -16,6 +16,7 @@ import { InitializationFromParent } from "./pages/states/InitializationFromParen import { StateIncrement } from "./pages/states/StateIncrement" import { StateDecrement } from "./pages/states/StateDecrement" import { UndefinedStateVariable } from "./pages/states/UndefinedStateVariable" +import { ObservableObject } from "./pages/states/ObservableObject" import { ObservableDate } from "./pages/states/ObservableDate" import { ObservableArray } from "./pages/states/ObservableArray" import { ObservableDateArray } from "./pages/states/ObservableDateArray" @@ -86,6 +87,7 @@ function pageByName(name: string): void { case "InitializationFromParent": InitializationFromParent(); break case "LinkDecorator": LinkDecorator(); break case "UndefinedStateVariable": UndefinedStateVariable(); break + case "ObservableObject": ObservableObject(); break case "ObservableDate": ObservableDate(); break case "ObservableArray": ObservableArray(); break case "ObservableDateArray": ObservableDateArray(); break diff --git a/ets-tests/ets/pages/states/ObservableObject.ets b/ets-tests/ets/pages/states/ObservableObject.ets new file mode 100644 index 000000000..ce168411e --- /dev/null +++ b/ets-tests/ets/pages/states/ObservableObject.ets @@ -0,0 +1,39 @@ +/* + * 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 Person { + name: string + age: number + + constructor(name: string, age: number) { + this.name = name + this.age = age + } +} + +@Component +struct ObservableObject { + @State person: Person = new Person("Alice", 3) + + build() { + TestComponent({}) + .log(`name = ${this.person.name}, age = ${this.person.age}`) + + TestComponent({ id: 11 }).onChange(() => { this.person.age = 4 }) + TestComponent({ id: 12 }).onChange(() => { this.person.name = "Bob" }) + TestComponent({ id: 13 }).onChange(() => { this.person = new Person("Alice", 5) }) + TestComponent({ id: 14 }).onChange(() => { this.person.age = 6 }) + } +} diff --git a/ets-tests/ets/suites/LinkDecorator.ets b/ets-tests/ets/suites/LinkDecorator.ets index aa50ede99..f8733a9ed 100644 --- a/ets-tests/ets/suites/LinkDecorator.ets +++ b/ets-tests/ets/suites/LinkDecorator.ets @@ -200,8 +200,8 @@ export function linkDecoratorBehaviorTest(control: AppControl) { + "LinkSetChildComponent: \"text\"\n" + "LinkObjectChildComponent: 0\n" + "LinkSetChildComponent: 0\n" - + "LinkObjectChildComponent: {\"age\":18}\n" - + "LinkSetChildComponent: {\"age\":18}\n", + + "LinkObjectChildComponent: {\"__age\":18}\n" + + "LinkSetChildComponent: {\"__age\":18}\n", "Object binding did not synchronize correctly between parent and child") }) test("Boolean binding synchronization between parent and child", () => { diff --git a/ets-tests/ets/suites/ProvideConsume.ets b/ets-tests/ets/suites/ProvideConsume.ets index f52bb6cc6..ab7c847ca 100644 --- a/ets-tests/ets/suites/ProvideConsume.ets +++ b/ets-tests/ets/suites/ProvideConsume.ets @@ -175,12 +175,12 @@ export function provideConsumeTests(control: AppControl) { test("Support date, class, object, union types", () => { testPageOnLoad(control, "ProvideConsumeObject", "parent.date = 2025-4-1\n" - + "parent.objectValue = {\"age\":18}\n" - + "parent.classValue = {\"age\":18}\n" + + "parent.objectValue = {\"__age\":18}\n" + + "parent.classValue = {\"__age\":18}\n" + "parent.unionValue = \"simple\"\n" + "child.date = 2025-4-1\n" - + "child.objectValue = {\"age\":18}\n" - + "child.classValue = {\"age\":18}\n" + + "child.objectValue = {\"__age\":18}\n" + + "child.classValue = {\"__age\":18}\n" + "child.unionValue = \"simple\"\n") }) test("Two-way synchronization after changing Date variable", () => { @@ -190,8 +190,8 @@ export function provideConsumeTests(control: AppControl) { + "parent.classValue = {\"age\":18}\n" + "parent.unionValue = \"simple\"\n" + "child.date = 2025-4-1\n" - + "child.objectValue = {\"age\":18}\n" - + "child.classValue = {\"age\":18}\n" + + "child.objectValue = {\"__age\":18}\n" + + "child.classValue = {\"__age\":18}\n" + "child.unionValue = \"simple\"\n" + "child.date = 2025-3-1\n" @@ -223,14 +223,14 @@ export function provideConsumeTests(control: AppControl) { 'parent.classValue = {"age":18}', 'parent.unionValue = "simple"', 'child.date = 2025-4-1', - 'child.objectValue = {"age":18}', - 'child.classValue = {"age":18}', + 'child.objectValue = {"__age":18}', + 'child.classValue = {"__age":18}', 'child.unionValue = "simple"', - 'child.classValue = {"age":31}', - 'parent.classValue = {"age":31}', - 'child.classValue = {"age":22}', - 'parent.classValue = {"age":22}' + 'child.classValue = {"__age":31}', + 'parent.classValue = {"__age":31}', + 'child.classValue = {"__age":22}', + 'parent.classValue = {"__age":22}' ]) }) test("Two-way synchronization after changing attributes of Date", () => { @@ -240,8 +240,8 @@ export function provideConsumeTests(control: AppControl) { "parent.classValue = {\"age\":18}", "parent.unionValue = \"simple\"", "child.date = 2025-4-1", - "child.objectValue = {\"age\":18}", - "child.classValue = {\"age\":18}", + "child.objectValue = {\"__age\":18}", + "child.classValue = {\"__age\":18}", "child.unionValue = \"simple\"", "child.date = 2025-4-7", @@ -252,21 +252,21 @@ export function provideConsumeTests(control: AppControl) { }) // skip test as currently it is impossible to observe changes or class properties // waiting for ObjectProxy realization by panda runtime - test.expectFailure("Description of the problem", "Two-way synchronization after changing property of a class variable", () => { + test("Description of the problem", "Two-way synchronization after changing property of a class variable", () => { testPageOnChange(control, "ProvideConsumeObject", [15, 25], [ "parent.date = 2025-4-1", "parent.objectValue = {\"age\":18}", "parent.classValue = {\"age\":18}", "parent.unionValue = \"simple\"", "child.date = 2025-4-1", - "child.objectValue = {\"age\":18}", - "child.classValue = {\"age\":18}", + "child.objectValue = {\"__age\":18}", + "child.classValue = {\"__age\":18}", "child.unionValue = \"simple\"", - "child.classValue = {\"age\":15}", - "parent.classValue = {\"age\":15}", - "child.classValue = {\"age\":25}", - "parent.classValue = {\"age\":25}", + "child.classValue = {\"__age\":15}", + "parent.classValue = {\"__age\":15}", + "child.classValue = {\"__age\":25}", + "parent.classValue = {\"__age\":25}", ]) }) test("Two-way synchronization of union variable", () => { @@ -276,8 +276,8 @@ export function provideConsumeTests(control: AppControl) { "parent.classValue = {\"age\":18}", "parent.unionValue = \"simple\"", "child.date = 2025-4-1", - "child.objectValue = {\"age\":18}", - "child.classValue = {\"age\":18}", + "child.objectValue = {\"__age\":18}", + "child.classValue = {\"__age\":18}", "child.unionValue = \"simple\"", "child.unionValue = 125", diff --git a/ets-tests/ets/suites/StateManagement.ets b/ets-tests/ets/suites/StateManagement.ets index 6c8fc74d8..26ee9fa91 100644 --- a/ets-tests/ets/suites/StateManagement.ets +++ b/ets-tests/ets/suites/StateManagement.ets @@ -42,7 +42,15 @@ function stateManagementTests(control: AppControl) { + "value = 20\n", "@State decorated variable was not changed correctly or there were no re-renders") }) - test("Change value of observable date", () => { + test("Initialization from the parent component", () => { + testPageOnChange(control, "ObservableObject", [11, 12, 13, 14], + "name = Alice, age = 3\n" + + "name = Alice, age = 4\n" + + "name = Bob, age = 4\n" + + "name = Alice, age = 5\n" + + "name = Alice, age = 6\n") + }) + test.expectFailure("Change value of observable date", () => { testPageOnChange(control, "ObservableDate", [11, 12, 13, 14, 15, 16, 17], "date = 2020-2-29 17:0\n" + "date = 2025-5-1 13:15\n" diff --git a/incremental/compat/src/arkts/observable.ts b/incremental/compat/src/arkts/observable.ts index 4d148fe76..a432eb0cb 100644 --- a/incremental/compat/src/arkts/observable.ts +++ b/incremental/compat/src/arkts/observable.ts @@ -171,6 +171,20 @@ export class ObservableHandler implements Observable { } return false } + + // TODO: onGet, onSet: temporary methods for observing object classes. Remove after fixing DefaultProxyHandler in ArkTS + static onGet(parent: T) { + const observable = ObservableHandler.find(parent) + if (observable) { + observable.onAccess() + } + } + static onSet(parent: T) { + const observable = ObservableHandler.find(parent) + if (observable) { + observable.onModify() + } + } } /** @internal */ @@ -210,6 +224,12 @@ export function observableProxy(value: Value, parent?: ObservableHandler, } else if (value instanceof Date) { return ObservableDate(value, parent, observed) as Value } + + if (Type.of(value) instanceof ClassType) { + const handler = new ObservableHandler(parent) + ObservableHandler.installOn(value as Object, handler) + return value as Value + } // TODO: proxy the given object if (!PROXY_DISABLED && Type.of(value) instanceof ClassType) { return Proxy.create(value as Object, new CustomProxyHandler()) as Value diff --git a/ui2abc/ui-plugins/src/class-transformer.ts b/ui2abc/ui-plugins/src/class-transformer.ts new file mode 100644 index 000000000..3720311ff --- /dev/null +++ b/ui2abc/ui-plugins/src/class-transformer.ts @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2022-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 arkts from "@koalaui/libarkts" +import { Importer } from "./utils"; +import { generateThisBacking } from "./property-translators/utils"; +import { StructTable } from "./struct-recorder"; +import { Es2pandaTokenType } from "@koalaui/libarkts"; +import { ComponentStatementTransformer } from "./component-transformer"; + +export class ClassStatementTransformer implements ComponentStatementTransformer { + private structTable: StructTable + + constructor(structTable: StructTable) { + this.structTable = structTable + } + + collectImports(imports: Importer): void { + // TODO: Temporary for observing object classes. Remove after fixing DefaultProxyHandler in ArkTS + imports.add('ObservableHandler', '@koalaui/common') + } + + check(statement: arkts.Statement): boolean { + return arkts.isClassDeclaration(statement) + && this.structTable.isUsedTypeName(statement.definition?.ident?.name!) + } + + applyTransform(property: arkts.ClassDeclaration, result: arkts.Statement[]) { + result.push(this.rewriteCustomClass(property)) + } + + private rewriteCustomClass(clazz: arkts.ClassDeclaration): arkts.ClassDeclaration { + const classBody: arkts.Statement[] = [] + clazz.definition?.body.forEach((element) => + classBody.push(new CustomClassBodyRewriter(classBody).visitor(element))) + + return arkts.factory.createClassDeclaration( + arkts.factory.createClassDefinition( + clazz.definition!.ident, + clazz.definition?.typeParams, + clazz.definition?.superTypeParams, + clazz.definition?.implements ?? [], + clazz.definition?.ctor, + clazz.definition?.super, + classBody, + clazz.definition?.modifiers!, + clazz.definition?.modifierFlags! + ) + ) + } +} + +class CustomClassBodyRewriter extends arkts.AbstractVisitor { + private readonly extraStmts: arkts.Statement[] = [] + private isConstructorContext = false + + constructor(extra: arkts.Statement[]) { + super() + this.extraStmts = extra + } + + visitor(node: arkts.AstNode): arkts.AstNode { + if (arkts.isMethodDefinition(node) && node.isConstructor) { + this.isConstructorContext = true + const result = this.visitEachChild(node) + this.isConstructorContext = false + return result + } else if (arkts.isClassProperty(node)) { + this.generateGetterSetter(node, this.extraStmts) + return this.rewriteClassProperty(node) + } else if (this.isConstructorContext + && arkts.isMemberExpression(node) + && arkts.isThisExpression(node.object) + && arkts.isIdentifier(node.property)) { + return generateThisBacking(this.getBackingPropName(node.property.name)) + } + + return this.visitEachChild(node) + } + + private getBackingPropName(propName: string): string { + return "__" + propName + } + + private rewriteClassProperty(property: arkts.ClassProperty) { + return arkts.factory.createClassProperty( + arkts.factory.createIdentifier(this.getBackingPropName(property.id?.name!)), + property.value, + property.typeAnnotation, + arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_PRIVATE, + false + ) + } + + private createCallObservableHandler(methodName: "onGet" | "onSet"): arkts.ExpressionStatement { + return arkts.factory.createExpressionStatement( + arkts.factory.createCallExpression( + arkts.factory.createMemberExpression( + arkts.factory.createIdentifier("ObservableHandler"), + arkts.factory.createIdentifier(methodName), + arkts.Es2pandaMemberExpressionKind.MEMBER_EXPRESSION_KIND_PROPERTY_ACCESS, + false, + false + ), + [arkts.factory.createThisExpression()], + undefined + ) + ) + } + + private generateGetterSetter(property: arkts.ClassProperty, result: arkts.Statement[]) { + const origPropName = property.id?.name! + const backingPropName = this.getBackingPropName(origPropName) + + const getterFunction = arkts.factory.createScriptFunction( + arkts.factory.createBlockStatement([ + this.createCallObservableHandler("onGet"), + arkts.factory.createReturnStatement(generateThisBacking(backingPropName)) + ]), + undefined, + [], + property.typeAnnotation, + true, + arkts.Es2pandaScriptFunctionFlags.SCRIPT_FUNCTION_FLAGS_METHOD + | arkts.Es2pandaScriptFunctionFlags.SCRIPT_FUNCTION_FLAGS_GETTER, + arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_PUBLIC, + arkts.factory.createIdentifier(origPropName), + [] + ) + + const setterFunction = arkts.factory.createScriptFunction( + arkts.factory.createBlockStatement([ + arkts.factory.createExpressionStatement( + arkts.factory.createAssignmentExpression( + generateThisBacking(backingPropName), + arkts.factory.createIdentifier("value"), + Es2pandaTokenType.TOKEN_TYPE_PUNCTUATOR_SUBSTITUTION + ) + ), + this.createCallObservableHandler("onSet"), + ]), + undefined, + [ + arkts.factory.createETSParameterExpression( + arkts.factory.createIdentifier("value"), + false, + undefined, + property.typeAnnotation! + ) + ], + undefined, + true, + arkts.Es2pandaScriptFunctionFlags.SCRIPT_FUNCTION_FLAGS_METHOD + | arkts.Es2pandaScriptFunctionFlags.SCRIPT_FUNCTION_FLAGS_SETTER + | arkts.Es2pandaScriptFunctionFlags.SCRIPT_FUNCTION_FLAGS_OVERLOAD, + arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_PUBLIC, + arkts.factory.createIdentifier(origPropName), + [] + ) + + const setter = arkts.factory.createMethodDefinition( + arkts.Es2pandaMethodDefinitionKind.METHOD_DEFINITION_KIND_SET, + arkts.factory.createIdentifier(origPropName), + arkts.factory.createFunctionExpression(arkts.factory.createIdentifier(origPropName), setterFunction), + arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_PUBLIC, + false + ) + + const getter = arkts.factory.createMethodDefinition( + arkts.Es2pandaMethodDefinitionKind.METHOD_DEFINITION_KIND_GET, + arkts.factory.createIdentifier(origPropName), + arkts.factory.createFunctionExpression(arkts.factory.createIdentifier(origPropName), getterFunction), + arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_PUBLIC, + false, + [setter] + ) + setter.parent = getter + result.push(getter) + } +} \ No newline at end of file diff --git a/ui2abc/ui-plugins/src/component-transformer.ts b/ui2abc/ui-plugins/src/component-transformer.ts index 889661820..bd0e4f59d 100644 --- a/ui2abc/ui-plugins/src/component-transformer.ts +++ b/ui2abc/ui-plugins/src/component-transformer.ts @@ -18,9 +18,26 @@ import { CustomComponentNames, getCustomComponentOptionsName, Importer, - InternalAnnotations + InternalAnnotations, + ImportingTransformer } from "./utils"; -import { BuilderParamTransformer, ConsumeTransformer, LinkTransformer, LocalStorageLinkTransformer, LocalStoragePropTransformer, ObjectLinkTransformer, PropertyTransformer, PropTransformer, ProvideTransformer, StateTransformer, StorageLinkTransformer, StoragePropTransformer, PlainPropertyTransformer, fieldOf, isOptionBackedByProperty, isOptionBackedByPropertyName } from "./property-transformers"; +import { + BuilderParamTransformer, + ConsumeTransformer, + LinkTransformer, + LocalStorageLinkTransformer, + LocalStoragePropTransformer, + ObjectLinkTransformer, + PropertyTransformer, + PropTransformer, + ProvideTransformer, + StateTransformer, + StorageLinkTransformer, + StoragePropTransformer, + PlainPropertyTransformer, + fieldOf, + isOptionBackedByPropertyName +} from "./property-transformers"; import { annotation, isAnnotation } from "./common/arkts-utils"; import { DecoratorNames, DecoratorParameters, hasDecorator } from "./property-translators/utils"; import { @@ -37,6 +54,11 @@ export interface ComponentTransformerOptions { applicationInfo?: ApplicationInfo } +export interface ComponentStatementTransformer extends ImportingTransformer { + check(property: arkts.Statement): boolean + applyTransform(property: arkts.Statement, result: arkts.Statement[]): void +} + function computeOptionsName(clazz: arkts.ClassDeclaration): string { return clazz.definition?.typeParams?.params?.[1]?.name?.name ?? getCustomComponentOptionsName(clazz.definition?.ident?.name!) @@ -44,10 +66,16 @@ function computeOptionsName(clazz: arkts.ClassDeclaration): string { export class ComponentTransformer extends arkts.AbstractVisitor { private arkuiImport?: string + private callRewriter: StructCallRewriter + private statementsTransformers: ComponentStatementTransformer[] constructor(private imports: Importer, options?: ComponentTransformerOptions) { super() this.arkuiImport = options?.arkui + this.callRewriter = new StructCallRewriter(structTable) + this.statementsTransformers = [ + new ClassStatementTransformer(this.structTable) + ] } private transformStatements(statements: readonly arkts.Statement[]): arkts.Statement[] { @@ -68,7 +96,13 @@ export class ComponentTransformer extends arkts.AbstractVisitor { if (arkts.isETSStructDeclaration(statement)) { this.rewriteStruct(statement, result) } else { - result.push(statement) + const transformer = this.statementsTransformers.find(it => it.check(statement)) + if (transformer) { + transformer.collectImports(this.imports) + transformer.applyTransform(statement, result) + } else { + result.push(statement) + } } }) return result @@ -379,27 +413,6 @@ export class ComponentTransformer extends arkts.AbstractVisitor { result.push(this.rewriteStructToOptions(node)) } - private rewriteClass(clazz: arkts.ClassDeclaration): arkts.ClassDeclaration { - console.log(`rewriteClass=${clazz.definition?.ident?.name}`) - const classBody: arkts.Statement[] = [] - clazz.definition?.body.forEach((element) => - classBody.push(new CustomClassBodyRewriter(classBody).visitor(element))) - - return arkts.factory.createClassDeclaration( - arkts.factory.createClassDefinition( - clazz.definition!.ident, - clazz.definition?.typeParams, - clazz.definition?.superTypeParams, - clazz.definition?.implements ?? [], - clazz.definition?.ctor, - clazz.definition?.super, - classBody, - clazz.definition?.modifiers!, - clazz.definition?.modifierFlags! - ) - ) - } - visitor(node: arkts.AstNode): arkts.AstNode { if (arkts.isETSModule(node)) { return this.rewriteModule(node) @@ -435,110 +448,4 @@ function createVoidMethod(methodName: string, parameters: readonly arkts.Express arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_PROTECTED, false ) -} - -class CustomClassBodyRewriter extends arkts.AbstractVisitor { - extraStmts: arkts.Statement[] = [] - isConstructorContext = false - - constructor(extra: arkts.Statement[]) { - super() - this.extraStmts = extra - } - - visitor(node: arkts.AstNode): arkts.AstNode { - if (arkts.isMethodDefinition(node) && node.isConstructor) { - this.isConstructorContext = true - const result = this.visitEachChild(node) - this.isConstructorContext = false - return result - } else if (arkts.isClassProperty(node)) { - this.generateGetterSetter(node, this.extraStmts) - return this.rewriteClassProperty(node) - } else if (this.isConstructorContext && arkts.isMemberExpression(node) && arkts.isIdentifier(node.property)) { - return generateThisBacking(this.getBackingPropName(node.property.name)) - } - - return this.visitEachChild(node) - } - - private getBackingPropName(propName: string): string { - return "__" + propName - } - - private rewriteClassProperty(property: arkts.ClassProperty) { - return arkts.factory.createClassProperty( - arkts.factory.createIdentifier(this.getBackingPropName(property.id?.name!)), - property.value, - property.typeAnnotation, - arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_PRIVATE, - false - ) - } - - private generateGetterSetter(property: arkts.ClassProperty, result: arkts.Statement[]) { - const origPropName = property.id?.name! - const backingPropName = this.getBackingPropName(origPropName) - - const getterFunction = arkts.factory.createScriptFunction( - arkts.factory.createBlockStatement([arkts.factory.createReturnStatement(generateThisBacking(backingPropName))]), - undefined, - [], - property.typeAnnotation, - true, - arkts.Es2pandaScriptFunctionFlags.SCRIPT_FUNCTION_FLAGS_METHOD - | arkts.Es2pandaScriptFunctionFlags.SCRIPT_FUNCTION_FLAGS_GETTER, - arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_PUBLIC, - arkts.factory.createIdentifier(origPropName), - [] - ) - - const setterFunction = arkts.factory.createScriptFunction( - arkts.factory.createBlockStatement([ - arkts.factory.createExpressionStatement( - arkts.factory.createAssignmentExpression( - generateThisBacking(backingPropName), - arkts.factory.createIdentifier("value"), - Es2pandaTokenType.TOKEN_TYPE_PUNCTUATOR_SUBSTITUTION - ) - ) - ]), - undefined, - [ - arkts.factory.createETSParameterExpression( - arkts.factory.createIdentifier("value"), - false, - undefined, - property.typeAnnotation! - ) - ], - undefined, - true, - arkts.Es2pandaScriptFunctionFlags.SCRIPT_FUNCTION_FLAGS_METHOD - | arkts.Es2pandaScriptFunctionFlags.SCRIPT_FUNCTION_FLAGS_SETTER - | arkts.Es2pandaScriptFunctionFlags.SCRIPT_FUNCTION_FLAGS_OVERLOAD, - arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_PUBLIC, - arkts.factory.createIdentifier(origPropName), - [] - ) - - const setter = arkts.factory.createMethodDefinition( - arkts.Es2pandaMethodDefinitionKind.METHOD_DEFINITION_KIND_SET, - arkts.factory.createIdentifier(origPropName), - arkts.factory.createFunctionExpression(arkts.factory.createIdentifier(origPropName), setterFunction), - arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_PUBLIC, - false - ) - - const getter = arkts.factory.createMethodDefinition( - arkts.Es2pandaMethodDefinitionKind.METHOD_DEFINITION_KIND_GET, - arkts.factory.createIdentifier(origPropName), - arkts.factory.createFunctionExpression(arkts.factory.createIdentifier(origPropName), getterFunction), - arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_PUBLIC, - false, - [setter] - ) - setter.parent = getter - result.push(getter) - } } \ No newline at end of file -- Gitee From 16fe0cf6085036767ee1226a296635bc6210eca9 Mon Sep 17 00:00:00 2001 From: vadimdolgachev Date: Tue, 27 May 2025 14:16:19 +0700 Subject: [PATCH 6/6] Fixed after merge --- .../ets/pages/states/ObservableObject.ets | 2 ++ ets-tests/ets/suites/ProvideConsume.ets | 30 +++++++++---------- ets-tests/ets/suites/StateManagement.ets | 4 +-- incremental/compat/src/arkts/observable.ts | 11 ++++--- ui2abc/ui-plugins/src/class-transformer.ts | 15 +++------- .../ui-plugins/src/component-transformer.ts | 6 ++-- ui2abc/ui-plugins/src/struct-recorder.ts | 12 -------- 7 files changed, 31 insertions(+), 49 deletions(-) diff --git a/ets-tests/ets/pages/states/ObservableObject.ets b/ets-tests/ets/pages/states/ObservableObject.ets index ce168411e..c806cf611 100644 --- a/ets-tests/ets/pages/states/ObservableObject.ets +++ b/ets-tests/ets/pages/states/ObservableObject.ets @@ -13,6 +13,8 @@ * limitations under the License. */ +import { TestComponent } from '@ohos.arkui' + class Person { name: string age: number diff --git a/ets-tests/ets/suites/ProvideConsume.ets b/ets-tests/ets/suites/ProvideConsume.ets index ab7c847ca..3dc390ff0 100644 --- a/ets-tests/ets/suites/ProvideConsume.ets +++ b/ets-tests/ets/suites/ProvideConsume.ets @@ -186,8 +186,8 @@ export function provideConsumeTests(control: AppControl) { test("Two-way synchronization after changing Date variable", () => { testPageOnChange(control, "ProvideConsumeObject", [11, 21], "parent.date = 2025-4-1\n" - + "parent.objectValue = {\"age\":18}\n" - + "parent.classValue = {\"age\":18}\n" + + "parent.objectValue = {\"__age\":18}\n" + + "parent.classValue = {\"__age\":18}\n" + "parent.unionValue = \"simple\"\n" + "child.date = 2025-4-1\n" + "child.objectValue = {\"__age\":18}\n" @@ -202,12 +202,12 @@ export function provideConsumeTests(control: AppControl) { test("Two-way synchronization after changing object variable", () => { testPageOnChange(control, "ProvideConsumeObject", [13, 23], "parent.date = 2025-4-1\n" - + "parent.objectValue = {\"age\":18}\n" - + "parent.classValue = {\"age\":18}\n" + + "parent.objectValue = {\"__age\":18}\n" + + "parent.classValue = {\"__age\":18}\n" + "parent.unionValue = \"simple\"\n" + "child.date = 2025-4-1\n" - + "child.objectValue = {\"age\":18}\n" - + "child.classValue = {\"age\":18}\n" + + "child.objectValue = {\"__age\":18}\n" + + "child.classValue = {\"__age\":18}\n" + "child.unionValue = \"simple\"\n" + "child.objectValue = 125\n" @@ -219,8 +219,8 @@ export function provideConsumeTests(control: AppControl) { testPageOnChange(control, "ProvideConsumeObject", [14, 24], [ 'parent.date = 2025-4-1', - 'parent.objectValue = {"age":18}', - 'parent.classValue = {"age":18}', + 'parent.objectValue = {"__age":18}', + 'parent.classValue = {"__age":18}', 'parent.unionValue = "simple"', 'child.date = 2025-4-1', 'child.objectValue = {"__age":18}', @@ -236,8 +236,8 @@ export function provideConsumeTests(control: AppControl) { test("Two-way synchronization after changing attributes of Date", () => { testPageOnChange(control, "ProvideConsumeObject", [12, 22], [ "parent.date = 2025-4-1", - "parent.objectValue = {\"age\":18}", - "parent.classValue = {\"age\":18}", + "parent.objectValue = {\"__age\":18}", + "parent.classValue = {\"__age\":18}", "parent.unionValue = \"simple\"", "child.date = 2025-4-1", "child.objectValue = {\"__age\":18}", @@ -252,11 +252,11 @@ export function provideConsumeTests(control: AppControl) { }) // skip test as currently it is impossible to observe changes or class properties // waiting for ObjectProxy realization by panda runtime - test("Description of the problem", "Two-way synchronization after changing property of a class variable", () => { + test("Two-way synchronization after changing property of a class variable", () => { testPageOnChange(control, "ProvideConsumeObject", [15, 25], [ "parent.date = 2025-4-1", - "parent.objectValue = {\"age\":18}", - "parent.classValue = {\"age\":18}", + "parent.objectValue = {\"__age\":18}", + "parent.classValue = {\"__age\":18}", "parent.unionValue = \"simple\"", "child.date = 2025-4-1", "child.objectValue = {\"__age\":18}", @@ -272,8 +272,8 @@ export function provideConsumeTests(control: AppControl) { test("Two-way synchronization of union variable", () => { testPageOnChange(control, "ProvideConsumeObject", [16, 17, 26], [ "parent.date = 2025-4-1", - "parent.objectValue = {\"age\":18}", - "parent.classValue = {\"age\":18}", + "parent.objectValue = {\"__age\":18}", + "parent.classValue = {\"__age\":18}", "parent.unionValue = \"simple\"", "child.date = 2025-4-1", "child.objectValue = {\"__age\":18}", diff --git a/ets-tests/ets/suites/StateManagement.ets b/ets-tests/ets/suites/StateManagement.ets index 26ee9fa91..c45151135 100644 --- a/ets-tests/ets/suites/StateManagement.ets +++ b/ets-tests/ets/suites/StateManagement.ets @@ -42,7 +42,7 @@ function stateManagementTests(control: AppControl) { + "value = 20\n", "@State decorated variable was not changed correctly or there were no re-renders") }) - test("Initialization from the parent component", () => { + test("Change value of observable object", () => { testPageOnChange(control, "ObservableObject", [11, 12, 13, 14], "name = Alice, age = 3\n" + "name = Alice, age = 4\n" @@ -50,7 +50,7 @@ function stateManagementTests(control: AppControl) { + "name = Alice, age = 5\n" + "name = Alice, age = 6\n") }) - test.expectFailure("Change value of observable date", () => { + test("Change value of observable date", () => { testPageOnChange(control, "ObservableDate", [11, 12, 13, 14, 15, 16, 17], "date = 2020-2-29 17:0\n" + "date = 2025-5-1 13:15\n" diff --git a/incremental/compat/src/arkts/observable.ts b/incremental/compat/src/arkts/observable.ts index a432eb0cb..5de4bbe7f 100644 --- a/incremental/compat/src/arkts/observable.ts +++ b/incremental/compat/src/arkts/observable.ts @@ -225,15 +225,14 @@ export function observableProxy(value: Value, parent?: ObservableHandler, return ObservableDate(value, parent, observed) as Value } + // TODO: proxy the given object if (Type.of(value) instanceof ClassType) { - const handler = new ObservableHandler(parent) - ObservableHandler.installOn(value as Object, handler) + if (!PROXY_DISABLED) { + return Proxy.create(value as Object, new CustomProxyHandler()) as Value + } + ObservableHandler.installOn(value as Object, new ObservableHandler(parent)) return value as Value } - // TODO: proxy the given object - if (!PROXY_DISABLED && Type.of(value) instanceof ClassType) { - return Proxy.create(value as Object, new CustomProxyHandler()) as Value - } return value as Value } diff --git a/ui2abc/ui-plugins/src/class-transformer.ts b/ui2abc/ui-plugins/src/class-transformer.ts index 3720311ff..043e33878 100644 --- a/ui2abc/ui-plugins/src/class-transformer.ts +++ b/ui2abc/ui-plugins/src/class-transformer.ts @@ -21,12 +21,6 @@ import { Es2pandaTokenType } from "@koalaui/libarkts"; import { ComponentStatementTransformer } from "./component-transformer"; export class ClassStatementTransformer implements ComponentStatementTransformer { - private structTable: StructTable - - constructor(structTable: StructTable) { - this.structTable = structTable - } - collectImports(imports: Importer): void { // TODO: Temporary for observing object classes. Remove after fixing DefaultProxyHandler in ArkTS imports.add('ObservableHandler', '@koalaui/common') @@ -34,7 +28,6 @@ export class ClassStatementTransformer implements ComponentStatementTransformer check(statement: arkts.Statement): boolean { return arkts.isClassDeclaration(statement) - && this.structTable.isUsedTypeName(statement.definition?.ident?.name!) } applyTransform(property: arkts.ClassDeclaration, result: arkts.Statement[]) { @@ -42,7 +35,7 @@ export class ClassStatementTransformer implements ComponentStatementTransformer } private rewriteCustomClass(clazz: arkts.ClassDeclaration): arkts.ClassDeclaration { - const classBody: arkts.Statement[] = [] + const classBody: arkts.AstNode[] = [] clazz.definition?.body.forEach((element) => classBody.push(new CustomClassBodyRewriter(classBody).visitor(element))) @@ -63,10 +56,10 @@ export class ClassStatementTransformer implements ComponentStatementTransformer } class CustomClassBodyRewriter extends arkts.AbstractVisitor { - private readonly extraStmts: arkts.Statement[] = [] + private readonly extraStmts: arkts.AstNode[] = [] private isConstructorContext = false - constructor(extra: arkts.Statement[]) { + constructor(extra: arkts.AstNode[]) { super() this.extraStmts = extra } @@ -120,7 +113,7 @@ class CustomClassBodyRewriter extends arkts.AbstractVisitor { ) } - private generateGetterSetter(property: arkts.ClassProperty, result: arkts.Statement[]) { + private generateGetterSetter(property: arkts.ClassProperty, result: arkts.AstNode[]) { const origPropName = property.id?.name! const backingPropName = this.getBackingPropName(origPropName) diff --git a/ui2abc/ui-plugins/src/component-transformer.ts b/ui2abc/ui-plugins/src/component-transformer.ts index bd0e4f59d..ebb68d0f6 100644 --- a/ui2abc/ui-plugins/src/component-transformer.ts +++ b/ui2abc/ui-plugins/src/component-transformer.ts @@ -43,6 +43,8 @@ import { DecoratorNames, DecoratorParameters, hasDecorator } from "./property-tr import { factory } from "./ui-factory" +import { ClassStatementTransformer } from "./class-transformer"; +import { StructTable } from "./struct-recorder"; export interface ApplicationInfo { bundleName: string, @@ -66,15 +68,13 @@ function computeOptionsName(clazz: arkts.ClassDeclaration): string { export class ComponentTransformer extends arkts.AbstractVisitor { private arkuiImport?: string - private callRewriter: StructCallRewriter private statementsTransformers: ComponentStatementTransformer[] constructor(private imports: Importer, options?: ComponentTransformerOptions) { super() this.arkuiImport = options?.arkui - this.callRewriter = new StructCallRewriter(structTable) this.statementsTransformers = [ - new ClassStatementTransformer(this.structTable) + new ClassStatementTransformer() ] } diff --git a/ui2abc/ui-plugins/src/struct-recorder.ts b/ui2abc/ui-plugins/src/struct-recorder.ts index def93ce48..e225a971c 100644 --- a/ui2abc/ui-plugins/src/struct-recorder.ts +++ b/ui2abc/ui-plugins/src/struct-recorder.ts @@ -118,7 +118,6 @@ export class StructsResolver { export class StructTable { private structByName = new Map() - private usedTypeName = new Set() constructor(private fileName: string, private resolverToSync: StructsResolver|undefined) { if (!resolverToSync) { @@ -188,14 +187,6 @@ export class StructTable { let result = `{"structs": [ ${lines.join(",")} ]}` fs.writeFileSync(metaDatabase(this.fileName), result) } - - addUsedTypeName(name: string) { - this.usedTypeName.add(name) - } - - isUsedTypeName(typeName: string): boolean { - return this.usedTypeName.has(typeName) - } } export class StructRecorder extends arkts.AbstractVisitor { @@ -204,9 +195,6 @@ export class StructRecorder extends arkts.AbstractVisitor { } visitor(node: arkts.AstNode): arkts.AstNode { - if (arkts.isETSNewClassInstanceExpression(node) && arkts.isETSTypeReference(node.typeRef)) { - this.table.addUsedTypeName(node.typeRef.baseName?.name!) - } if (arkts.isETSModule(node)) { const result = this.visitEachChild(node) this.table.update() -- Gitee