diff --git a/arkoala-arkts/arkui/src/ArkStateProperties.ets b/arkoala-arkts/arkui/src/ArkStateProperties.ets index 2fdb6e595bcba5d29aeb0a3fe6df0d3c1341dfc0..5f6ea73ad47f4463045c0fde01023c051a62e416 100644 --- a/arkoala-arkts/arkui/src/ArkStateProperties.ets +++ b/arkoala-arkts/arkui/src/ArkStateProperties.ets @@ -14,7 +14,7 @@ */ import { int32, observableProxy, propDeepCopy } from "@koalaui/common" -import { mutableState, scheduleCallback, MutableState, GlobalStateManager, globalMutableState, ObservableClassV2 } from "@koalaui/runtime" +import { mutableState, scheduleCallback, MutableState, GlobalStateManager, globalMutableState, ObservableClass, ClassMetadata } from "@koalaui/runtime" import { SubscribedAbstractProperty } from "./ArkState" import { AppStorage, LocalStorage } from "./Storage" @@ -59,7 +59,10 @@ class StatableHolder { } private isStatable(value: Value | undefined): boolean { - return value instanceof ObservableClassV2 + if (value instanceof ObservableClass) { + return value.getClassMetadata()?.isObservedV2(value) ?? false + } + return false } } diff --git a/ets-tests/ets/ets-tests/pages/observedv2/TypeDecorator.ets b/ets-tests/ets/ets-tests/pages/observedv2/TypeDecorator.ets new file mode 100644 index 0000000000000000000000000000000000000000..18b1bd33bbcc34475066ad764809812d89dd3ae0 --- /dev/null +++ b/ets-tests/ets/ets-tests/pages/observedv2/TypeDecorator.ets @@ -0,0 +1,60 @@ +/* + * 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 { TestComponent, uiLog } from '@ohos.arkui' + +@ObservedV2 +class BaseSample { + @Trace + baseField1: int | undefined = 0 + @Trace + baseField2: int | undefined = 0 +} + +@ObservedV2 +class Sample extends BaseSample { + @Trace + data: int | undefined = 0 +} + +@ObservedV2 +class Foo { + +} + +@ObservedV2 +class Info { + @Type(Sample) + @Trace + sample: Sample = new Sample() + + @Type(Foo) + @Trace + foo: Foo = new Foo() +} + +@Entry +@Component +struct TypeDecorator { + info: Info = new Info() + build() { + TestComponent({ id: 100 }).onChange(() => { + this.info.sample.data!++ + }) + TestComponent({}) { + uiLog(`sample.data: ${this.info.sample!.data}`) + } + } +} \ No newline at end of file diff --git a/incremental/common/src/index.ts b/incremental/common/src/index.ts index 174cd00fb4babdfe96efceb0b6c9785e2b3e0b43..0533296ef8e6b1a150114df1a463e6d371fec255 100644 --- a/incremental/common/src/index.ts +++ b/incremental/common/src/index.ts @@ -34,7 +34,7 @@ export { ObservableHandler, observableProxy, observableProxyArray, - TrackableProps, + TrackableProperties, trackableProperties, isFunction, propDeepCopy, diff --git a/incremental/compat/src/arkts/observable.ts b/incremental/compat/src/arkts/observable.ts index 71ae3340c6fcebac83e60c10011da55a4903e637..33d25ead508cb88e52e1c897360f715563165a25 100644 --- a/incremental/compat/src/arkts/observable.ts +++ b/incremental/compat/src/arkts/observable.ts @@ -232,8 +232,8 @@ export function observableProxy(value: Value, parent?: ObservableHandler, const valueType = Type.of(value) if (valueType instanceof ClassType && !(value instanceof BaseEnum)) { - const isObservable = isObservableClass(value as Object) - if (trackableProperties(value as Object) == undefined && !isObservable) { + const isObservable = isObservedV1Class(value as Object) + if (!hasTrackableProperties(value as Object) && !isObservable) { return value as Value } if (valueType.hasEmptyConstructor()) { @@ -1132,43 +1132,124 @@ class ObservableDate extends Date { } } -function isObservableClass(value: Object): boolean { - return value instanceof ObservableClass ? value.isObserved() : false; +function getClassMetadata(value: T): ClassMetadata | undefined { + return value instanceof ObservableClass ? value.getClassMetadata() : undefined } -/** - * Interface for getting the observed properties of a class - */ -export interface TrackableProps { - /** - * Retrieves the set of property names that are being tracked for changes using `@Track` decorator - */ - trackedProperties(): ReadonlySet +function isObservedV1Class(value: Object): boolean { + return getClassMetadata(value)?.isObservedV1(value) ?? false } -export function trackableProperties(value: T): ReadonlySet | undefined { - if (value instanceof TrackableProps) { - return value.trackedProperties() - } - if (value instanceof ObservableClassV2) { - return value.tracedProperties() - } - return undefined +function hasTrackableProperties(value: Object): boolean { + return getClassMetadata(value)?.hasTrackableProperties() ?? false } /** * Interface for getting the observability status of a class */ export interface ObservableClass { - /** - * Indicates whether the class is decorated with `@Observed`. - */ - isObserved(): boolean + getClassMetadata(): ClassMetadata | undefined +} + +/** + * Interface for checking the observed properties of a class + */ +export interface TrackableProperties { + isTrackable(propertyName: string): boolean } /** - * To implement the ObservedV2 decorator + * If value is a class, then returns a list of trackable properties + * @param value */ -export interface ObservableClassV2 { - tracedProperties(): ReadonlySet +export function trackableProperties(value: T): TrackableProperties | undefined { + return getClassMetadata(value) +} + +export class ClassMetadata implements TrackableProperties { + private readonly parent: ClassMetadata | undefined + private readonly markAsObservedV1: boolean + private readonly markAsObservedV2: boolean + private readonly targetClass: Class + private static readonly metadataPropName = "__classMetadata" + + /** + * Class property names marked with the @Track or @Trace decorator + * @private + */ + private readonly trackableProperties: ReadonlySet | undefined + + /** + * Contains fields marked with the @Type decorator. + * The key of the map is the property name and the value is the typename of the corresponding field. + * @private + */ + private readonly typedProperties: ReadonlyMap | undefined + + constructor(parent: ClassMetadata | undefined, + markAsObservedV1: boolean, + markAsObservedV2: boolean, + trackable: string[] | undefined, + typed: [string, string][] | undefined) { + const target = Class.ofCaller() + if (target == undefined) { + throw new Error("ClassMetadata must be created in the class context") + } + this.targetClass = target! + this.parent = parent + this.markAsObservedV1 = markAsObservedV1 + this.markAsObservedV2 = markAsObservedV2 + if (trackable) { + this.trackableProperties = new Set(trackable) + } + if (typed) { + this.typedProperties = new Map(typed) + } + } + + isObservedV1(value: Object): boolean { + return this.markAsObservedV1 && Class.of(value) == this.targetClass + } + + isObservedV2(value: Object): boolean { + return this.markAsObservedV2 && Class.of(value) == this.targetClass + } + + isTrackable(propertyName: string): boolean { + return (this.trackableProperties?.has(propertyName) || this.parent?.isTrackable(propertyName)) ?? false + } + + hasTrackableProperties(): boolean { + if (this.trackableProperties) { + return this.trackableProperties!.size > 0 + } + return this.parent?.hasTrackableProperties() ?? false + } + + getTypenameTypeDecorator(propertyName: string): string | undefined { + if (this.typedProperties) { + return this.typedProperties?.get(propertyName) + } + if (this.parent) { + return this.parent!.getTypenameTypeDecorator(propertyName) + } + return undefined + } + + static findClassMetadata(type: Type): ClassMetadata | undefined { + if (type instanceof ClassType) { + const fieldsNum = type.getFieldsNum() + for (let i = 0; i < fieldsNum; i++) { + const field = type.getField(i) + if (field.isStatic() && field.getName() == ClassMetadata.metadataPropName) { + const meta = field.getStaticValue() + if (meta != undefined && meta instanceof ClassMetadata) { + return meta + } + break + } + } + } + return undefined + } } \ No newline at end of file diff --git a/incremental/compat/src/index.ts b/incremental/compat/src/index.ts index 3528b436a0d75de7710246439441e4404ab8ba66..d068f170afefa359a4ac4d8fd1f976dca122fd50 100644 --- a/incremental/compat/src/index.ts +++ b/incremental/compat/src/index.ts @@ -28,10 +28,10 @@ export { Observed, Observable, ObservableHandler, - TrackableProps, - trackableProperties, ObservableClass, - ObservableClassV2, + TrackableProperties, + trackableProperties, + ClassMetadata, observableProxy, observableProxyArray, propDeepCopy, diff --git a/incremental/compat/src/ohos/index.ts b/incremental/compat/src/ohos/index.ts index be94a8a1a1001c3b6acb11abc9b0c104a7d7e42a..892265e6691bc3bd0423b50d01327d3293bc1952 100644 --- a/incremental/compat/src/ohos/index.ts +++ b/incremental/compat/src/ohos/index.ts @@ -27,10 +27,10 @@ export { Observable, ObservableHandler, observableProxy, - TrackableProps, - trackableProperties, ObservableClass, - ObservableClassV2, + TrackableProperties, + trackableProperties, + ClassMetadata, observableProxyArray, propDeepCopy, lcClassName, diff --git a/incremental/compat/src/typescript/observable.ts b/incremental/compat/src/typescript/observable.ts index 4393ea1898ec7ee8463e934bc7df94c96dc65f64..31d9f74cd32af3928946c723f831f8e90d06c477 100644 --- a/incremental/compat/src/typescript/observable.ts +++ b/incremental/compat/src/typescript/observable.ts @@ -522,22 +522,9 @@ function clearObservable(data: any) { } } -/** - * Interface for getting the observed properties of a class - */ -export interface TrackableProps { - /** - * Retrieves the set of property names that are being tracked for changes using `@Track` decorator - */ - trackedProperties(): ReadonlySet | undefined -} - -export function trackableProperties(value: any): ReadonlySet | undefined { - if (typeof value.trackedProperties === 'function') { - return (value as TrackableProps).trackedProperties() - } - if (typeof value.tracedProperties === 'function') { - return (value as ObservableClassV2).tracedProperties() +function getClassMetadata(value: any): ClassMetadata | undefined { + if (value !== undefined && typeof value.getClassMetadata === 'function') { + return (value as ObservableClass).getClassMetadata() } return undefined } @@ -546,15 +533,96 @@ export function trackableProperties(value: any): ReadonlySet | undefined * Interface for getting the observability status of a class */ export interface ObservableClass { - /** - * Indicates whether the class is decorated with `@Observed`. - */ - isObserved(): boolean + getClassMetadata(): ClassMetadata | undefined +} + +/** + * Interface for checking the observed properties of a class + */ +export interface TrackableProperties { + isTrackable(propertyName: string): boolean } /** - * To implement the ObservedV2 decorator + * If value is a class, then returns a list of trackable properties + * @param value */ -export interface ObservableClassV2 { - tracedProperties(): ReadonlySet | undefined +export function trackableProperties(value: T): TrackableProperties | undefined { + return getClassMetadata(value) } + +export class ClassMetadata implements TrackableProperties { + private readonly parent: ClassMetadata | undefined + private readonly markAsObservedV1: boolean + private readonly markAsObservedV2: boolean + private static readonly metadataPropName = "__classMetadata" + + /** + * Class property names marked with the @Track or @Trace decorator + * @private + */ + private readonly trackableProperties: ReadonlySet | undefined + + /** + * Contains fields marked with the @Type decorator. + * The key of the map is the property name and the value is the typename of the corresponding field. + * @private + */ + private readonly typedProperties: ReadonlyMap | undefined + + constructor(parent: ClassMetadata | undefined, + markAsObservedV1: boolean, + markAsObservedV2: boolean, + trackable: string[] | undefined, + typed: [string, string][] | undefined) { + this.parent = parent + this.markAsObservedV1 = markAsObservedV1 + this.markAsObservedV2 = markAsObservedV2 + if (trackable) { + this.trackableProperties = new Set(trackable) + } + if (typed) { + this.typedProperties = new Map(typed) + } + } + + isObservedV1(value: Object): boolean { + return this.markAsObservedV1 + } + + isObservedV2(value: Object): boolean { + return this.markAsObservedV2 + } + + isTrackable(propertyName: string): boolean { + return (this.trackableProperties?.has(propertyName) || this.parent?.isTrackable(propertyName)) ?? false + } + + hasTrackableProperties(): boolean { + if (this.trackableProperties) { + return this.trackableProperties!.size > 0 + } + return this.parent?.hasTrackableProperties() ?? false + } + + getTypenameTypeDecorator(propertyName: string): string | undefined { + if (this.typedProperties) { + return this.typedProperties?.get(propertyName) + } + if (this.parent) { + return this.parent!.getTypenameTypeDecorator(propertyName) + } + return undefined + } + + private static findClassMetadata(type: any): ClassMetadata | undefined { + let prototype = Object.getPrototypeOf(type) + while (prototype) { + if (prototype.hasOwnProperty(ClassMetadata.metadataPropName) && prototype.__classMetadata !== undefined) { + return prototype.__classMetadata + } + prototype = Object.getPrototypeOf(prototype) + } + return undefined + } +} \ No newline at end of file diff --git a/incremental/runtime/src/index.ts b/incremental/runtime/src/index.ts index 336dc502cdff946bee674d1737ca9511f4cda313..de67a0cc4b1cd2e6b0b0523b1933324395c5927b 100644 --- a/incremental/runtime/src/index.ts +++ b/incremental/runtime/src/index.ts @@ -13,7 +13,7 @@ * limitations under the License. */ -export { observableProxy, ObservableClass, ObservableClassV2, TrackableProps } from "@koalaui/compat" +export { observableProxy, ObservableClass, ClassMetadata, TrackableProperties, trackableProperties } from "@koalaui/compat" export { AnimatedState, diff --git a/incremental/runtime/src/states/State.ts b/incremental/runtime/src/states/State.ts index 70abd36c7d7c1896dde17ac042a0029f494b85f7..e24f6a6f2d1008d08e083df56789114cbf65ebc3 100644 --- a/incremental/runtime/src/states/State.ts +++ b/incremental/runtime/src/states/State.ts @@ -21,6 +21,7 @@ import { markableQueue } from "../common/MarkableQueue" import { RuntimeProfiler } from "../common/RuntimeProfiler" import { IncrementalNode } from "../tree/IncrementalNode" import { ReadonlyTreeNode } from "../tree/ReadonlyTreeNode" +import { TrackableProperties } from "@koalaui/compat"; export const CONTEXT_ROOT_SCOPE = "ohos.koala.context.root.scope" export const CONTEXT_ROOT_NODE = "ohos.koala.context.root.node" @@ -1273,15 +1274,15 @@ class TrackedScope { class TrackedScopes { private trackedScopes = new Map() - private trackedProperties: ReadonlySet | undefined + private trackableProperties: TrackableProperties | undefined /** * Set the class tracked properties * @param trackedProperties */ - setTrackedProperties(trackedProperties: ReadonlySet | undefined): void { + setTrackedProperties(trackedProperties: TrackableProperties | undefined): void { this.trackedScopes.clear() - this.trackedProperties = trackedProperties + this.trackableProperties = trackedProperties } /** @@ -1291,7 +1292,7 @@ class TrackedScopes { * Pass undefined to remove tracking. */ register(propertyName: string, dependency: ScopeToStates | undefined): void { - if (!this.trackedProperties?.has(propertyName)) { + if (!this.trackableProperties?.isTrackable(propertyName)) { return } diff --git a/ui2abc/ui-plugins/src/class-transformer.ts b/ui2abc/ui-plugins/src/class-transformer.ts index 80795fe3a91c4dc6f6d13c42d34e3966cdee7f8d..f556667462416aeda8dbc3c0419db6622bcd9778 100644 --- a/ui2abc/ui-plugins/src/class-transformer.ts +++ b/ui2abc/ui-plugins/src/class-transformer.ts @@ -14,10 +14,19 @@ */ import * as arkts from "@koalaui/libarkts" +import { Es2pandaModifierFlags } from "@koalaui/libarkts" import { classProperties } from "./common/arkts-utils" -import { createETSTypeReference, getComponentPackage, getRuntimePackage, Importer, mangle } from "./utils" -import { DecoratorNames, hasDecorator, isDecoratorAnnotation } from "./utils"; +import { + createETSTypeReference, + DecoratorNames, + getComponentPackage, + getDecoratorName, + getRuntimePackage, + Importer, + isDecoratorAnnotation, + mangle +} from "./utils" import { backingFieldNameOf, fieldOf } from "./property-transformers"; type ObservedDecoratorType = DecoratorNames.OBSERVED | DecoratorNames.OBSERVED_V2 @@ -46,23 +55,10 @@ export class ClassTransformer extends arkts.AbstractVisitor { return node } - addImplObservedDecorator(decoratorType: ObservedDecoratorType, result: arkts.TSClassImplements[]) { - this.importer.add('observableProxy', getRuntimePackage()) - let className: string - if (decoratorType == DecoratorNames.OBSERVED) { - className = "ObservableClass" - } else if (decoratorType == DecoratorNames.OBSERVED_V2) { - className = "ObservableClassV2" - } - this.importer.add(className!, getRuntimePackage()) - result.push(arkts.factory.createTSClassImplements(createETSTypeReference(className!))) - } - - addImplTrackDecorator(result: arkts.TSClassImplements[]) { - this.importer.add('TrackableProps', getRuntimePackage()) - result.push(arkts.factory.createTSClassImplements( - createETSTypeReference("TrackableProps")) - ) + addObservableClassImplements(result: arkts.TSClassImplements[]) { + const className = "ObservableClass" + this.importer.add(className, getRuntimePackage()) + result.push(arkts.factory.createTSClassImplements(createETSTypeReference(className))) } updateClass(clazz: arkts.ClassDeclaration, @@ -73,11 +69,9 @@ export class ClassTransformer extends arkts.AbstractVisitor { ...clazz.definition?.implements ?? [], ] if (properties.length > 0) { - if (classContext.decoratorType) { - this.addImplObservedDecorator(classContext.decoratorType, classImplements) - } - if (classContext.trackedProperties.length) { - this.addImplTrackDecorator(classImplements) + if (classContext.isClassObservable()) { + this.importer.add("observableProxy", getRuntimePackage()) + this.addObservableClassImplements(classImplements) } classDef = arkts.factory.updateClassDefinition( classDef, @@ -103,19 +97,8 @@ export class ClassTransformer extends arkts.AbstractVisitor { body: readonly arkts.ClassElement[], classContext: ClassContext): arkts.ClassElement[] { const result: arkts.ClassElement[] = [] - if (classContext.isObserved()) { - createImplObservedFlagMethod(result) - } else if (classContext.isObservedV2()) { - const tracedProps = properties - .filter(tracePropVerifier) - .map(it => it.id?.name!) - createImplTrackablePropsMethod(classDef, "ObservableClassV2", "tracedProperties", tracedProps, result) - } - const trackedProps = properties - .filter(trackPropVerifier) - .map(it => it.id?.name!) - if (trackedProps.length > 0) { - createImplTrackablePropsMethod(classDef, "TrackableProps", "trackedProperties", trackedProps, result) + if (classContext.isClassObservable()) { + this.injectClassMetadata(classDef, classContext, result) } body.forEach(node => { if (arkts.isClassProperty(node) && classContext.needRewriteProperty(node)) { @@ -129,6 +112,73 @@ export class ClassTransformer extends arkts.AbstractVisitor { return result } + injectClassMetadata(classDef: arkts.ClassDefinition, classContext: ClassContext, result: arkts.ClassElement[]) { + const classMetadataName = "ClassMetadata" + const classMetadataPropName = mangle("classMetadata") + + this.importer.add(classMetadataName, getRuntimePackage()) + const classMetadataType = createETSTypeReference(classMetadataName) + let trackableProps: string[] | undefined + if (classContext.trackedPropertyNames.length > 0) { + trackableProps = classContext.trackedPropertyNames + } else if (classContext.tracedPropertyNames.length > 0) { + trackableProps = classContext.tracedPropertyNames + } + + let typedProps: [string, string][] | undefined + if (classContext.typedProperties.length > 0) { + typedProps = [] + for (const prop of classContext.typedProperties.values()) { + typedProps.push([prop[0], prop[1][0].args[0]]) + } + } + const metadataCtorArgs: arkts.Expression[] = [ + this.findClassMetadata(classMetadataType, classDef.super), + arkts.factory.createBooleanLiteral(classContext.isObservedV1()), + arkts.factory.createBooleanLiteral(classContext.isObservedV2()), + trackableProps + ? arkts.factory.createArrayExpression(trackableProps.map(it => arkts.factory.createStringLiteral(it))) + : arkts.factory.createUndefinedLiteral(), + typedProps + ? arkts.factory.createArrayExpression(typedProps.map(it => + arkts.factory.createArrayExpression([arkts.factory.createStringLiteral(it[0]), + arkts.factory.createStringLiteral(it[1])]))) + : arkts.factory.createUndefinedLiteral() + ] + result.push( + arkts.factory.createClassProperty( + arkts.factory.createIdentifier(classMetadataPropName), + arkts.factory.createETSNewClassInstanceExpression(classMetadataType, metadataCtorArgs), + arkts.factory.createETSUnionType([classMetadataType, arkts.factory.createETSUndefinedType()]), + arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_PRIVATE + | arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_READONLY + | arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_STATIC, + false + ) + ) + createClassMetadataMethod(classDef, classMetadataType, classMetadataPropName, result) + } + + findClassMetadata(classMetadataType: arkts.ETSTypeReference, type: arkts.Expression | undefined): arkts.Expression { + return arkts.isETSTypeReference(type) + ? arkts.factory.createCallExpression( + fieldOf(classMetadataType, "findClassMetadata"), + [ + arkts.factory.createCallExpression( + fieldOf(arkts.factory.createIdentifier("Type"), "from"), + [], + arkts.factory.createTSTypeParameterInstantiation([createETSTypeReference(type.baseName?.name!)]), + false, + false, + ) + ], + undefined, + false, + false + ) + : arkts.factory.createUndefinedLiteral() + } + rewriteProperty(property: arkts.ClassProperty, classContext: ClassContext, className: string, @@ -152,7 +202,13 @@ export class ClassTransformer extends arkts.AbstractVisitor { } rewriteAnnotations(annotations: readonly arkts.AnnotationUsage[]): arkts.AnnotationUsage[] { - const decorators = [DecoratorNames.TRACK, DecoratorNames.TRACE, DecoratorNames.OBSERVED, DecoratorNames.OBSERVED_V2] + const decorators = [ + DecoratorNames.TRACK, + DecoratorNames.TRACE, + DecoratorNames.OBSERVED, + DecoratorNames.OBSERVED_V2, + DecoratorNames.TYPE, + ] return annotations.filter(it => !decorators.some(decorator => isDecoratorAnnotation(it, decorator)) ) @@ -228,40 +284,56 @@ export class ClassTransformer extends arkts.AbstractVisitor { } } -function propertyVerifier(property: arkts.ClassProperty, - disallowedModifiers?: arkts.Es2pandaModifierFlags[]): boolean { - if (disallowedModifiers && disallowedModifiers.some(it => arkts.hasModifierFlag(property, it))) { - return false - } - return arkts.hasModifierFlag(property, arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_PUBLIC) +function propertyVerifier(property: arkts.ClassProperty): boolean { + return ![arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_PRIVATE, + Es2pandaModifierFlags.MODIFIER_FLAGS_PROTECTED] + .some(it => arkts.hasModifierFlag(property, it)); } -function trackPropVerifier(property: arkts.ClassProperty): boolean { - return hasDecorator(property, DecoratorNames.TRACK) - && propertyVerifier(property, [arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_STATIC]) +function getDecoratorArgs(decorator: arkts.AnnotationUsage): string[] { + const args: string[] = [] + for (const prop of decorator.properties) { + if (arkts.isClassProperty(prop) && arkts.isIdentifier(prop.value)) { + args.push(prop.value.name) + } + } + return args } -function tracePropVerifier(property: arkts.ClassProperty): boolean { - return hasDecorator(property, DecoratorNames.TRACE) && propertyVerifier(property) +function getDecoratedProperties(properties: readonly arkts.ClassProperty[]): ReadonlyMap { + const propertyInfo = new Map() + for (const prop of properties) { + if (!propertyVerifier(prop)) { + continue + } + const propName = prop.id!.name! + const decorators = propertyInfo.get(propName) ?? [] + + for (const anno of prop.annotations) { + let decorator = getDecoratorName(anno) + if (decorator === undefined) { + continue + } + if (decorators.length === 0) { + propertyInfo.set(propName, decorators) + } + decorators.push(new PropertyDecorator(decorator, getDecoratorArgs(anno))) + } + } + return propertyInfo } function extractClassContext(clazz: arkts.ClassDeclaration, properties: readonly arkts.ClassProperty[]): ClassContext | undefined { if (clazz.definition != undefined) { - return new ClassContext(getObservedDecoratorType(clazz.definition), - properties - .filter(trackPropVerifier) - .map(it => it.id?.name!), - properties - .filter(tracePropVerifier) - .map(it => it.id?.name!)) + return new ClassContext(getObservedDecoratorType(clazz.definition), getDecoratedProperties(properties)) } return undefined } function createBackingPropertyRValue(property: arkts.ClassProperty, value: arkts.Expression | undefined, classContext: ClassContext): arkts.Expression | undefined { const propValue = observePropIfNeeded(property.id?.name!, value, classContext) - if (isStaticProperty(property) && classContext.tracedProperties.includes(property.id?.name!)) { + if (isStaticProperty(property) && classContext.tracedPropertyNames.includes(property.id?.name!)) { return arkts.factory.createCallExpression( arkts.factory.createIdentifier("globalMutableState"), [propValue!], @@ -275,7 +347,7 @@ function createBackingPropertyRValue(property: arkts.ClassProperty, value: arkts } function createBackingPropertyType(property: arkts.ClassProperty, classContext: ClassContext) { - return isStaticProperty(property) && classContext.tracedProperties.includes(property.id?.name!) + return isStaticProperty(property) && classContext.tracedPropertyNames.includes(property.id?.name!) ? undefined : property.typeAnnotation } @@ -285,7 +357,7 @@ function createPropertyAccess(className: string, property: arkts.ClassProperty, ? createETSTypeReference(className) : arkts.factory.createThisExpression() const expr = fieldOf(receiver, backingFieldNameOf(property)) - if (isStaticProperty(property) && classContext.tracedProperties.includes(property.id?.name!)) { + if (isStaticProperty(property) && classContext.tracedPropertyNames.includes(property.id?.name!)) { return fieldOf(expr, "value") } return expr @@ -362,111 +434,24 @@ function createGetterSetter(property: arkts.ClassProperty, return getter } -function addTrackablePropsFromParent(trackedPropsIdent: string, - interfaceName: string, - methodName: string, - result: arkts.Statement[]) { - const trackableProps = arkts.factory.createIfStatement( - arkts.factory.createBinaryExpression( - arkts.factory.createThisExpression(), - createETSTypeReference(interfaceName), - arkts.Es2pandaTokenType.TOKEN_TYPE_KEYW_INSTANCEOF - ), - arkts.factory.createBlockStatement( - [ - arkts.factory.createExpressionStatement( - arkts.factory.createAssignmentExpression( - arkts.factory.createIdentifier(trackedPropsIdent), - arkts.factory.createCallExpression( - fieldOf(arkts.factory.createIdentifier(trackedPropsIdent), "concat"), - [ - arkts.factory.createCallExpression( - fieldOf(arkts.factory.createIdentifier("Array"), "from"), - [ - arkts.factory.createCallExpression( - fieldOf(arkts.factory.createSuperExpression(), methodName), - [], - undefined, - false, - false, - undefined, - ) - ], - undefined, - false, - false, - undefined, - ) - ], - undefined, - false, - false, - undefined, - ), - arkts.Es2pandaTokenType.TOKEN_TYPE_PUNCTUATOR_SUBSTITUTION - ) - ) - ] - ) - ) - result.push(trackableProps) -} - -function createImplTrackablePropsMethod(classDef: arkts.ClassDefinition, - interfaceName: string, - methodName: string, - trackableProps: readonly string[], - result: arkts.ClassElement[]) { - const trackedPropsIdent = mangle("trackedProps") +function createClassMetadataMethod(classDef: arkts.ClassDefinition, + classMetadataType: arkts.ETSTypeReference, + classMetadataPropName: string, + result: arkts.ClassElement[]) { const body: arkts.Statement[] = [] body.push( - arkts.factory.createVariableDeclaration( - arkts.Es2pandaVariableDeclarationKind.VARIABLE_DECLARATION_KIND_LET, - [ - arkts.factory.createVariableDeclarator( - arkts.Es2pandaVariableDeclaratorFlag.VARIABLE_DECLARATOR_FLAG_LET, - arkts.factory.createIdentifier(trackedPropsIdent, createETSTypeReference("Array", ["string"])), - arkts.factory.createArrayExpression( - trackableProps.map(it => arkts.factory.createStringLiteral(it)) - ) - ) - ] + arkts.factory.createReturnStatement( + fieldOf(createETSTypeReference(classDef.ident?.name!), classMetadataPropName) ) ) - if (classDef.super) { - addTrackablePropsFromParent(trackedPropsIdent, interfaceName, methodName, body) - } - body.push(arkts.factory.createReturnStatement( - arkts.factory.createETSNewClassInstanceExpression( - createETSTypeReference("Set", ["string"]), - [arkts.factory.createIdentifier(trackedPropsIdent)] - ) - )) - createMethodDefinition(methodName, + createMethodDefinition("getClassMetadata", arkts.factory.createBlockStatement(body), - createETSTypeReference("ReadonlySet", ["string"]), + arkts.factory.createETSUnionType([classMetadataType, + arkts.factory.createETSUndefinedType()]), result ) } -function createImplObservedFlagMethod(result: arkts.ClassElement[]) { - result.push( - arkts.factory.createClassProperty( - arkts.factory.createIdentifier(mangle("isCalleeCurrentClass")), - createCheckThisCurrentClass(), - arkts.factory.createETSPrimitiveType(arkts.Es2pandaPrimitiveType.PRIMITIVE_TYPE_BOOLEAN), - arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_PRIVATE | arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_READONLY, - false - ) - ) - createMethodDefinition("isObserved", - arkts.factory.createBlockStatement([arkts.factory.createReturnStatement( - fieldOf(arkts.factory.createThisExpression(), mangle("isCalleeCurrentClass")) - )]), - arkts.factory.createETSPrimitiveType(arkts.Es2pandaPrimitiveType.PRIMITIVE_TYPE_BOOLEAN), - result) -} - function createMethodDefinition(methodName: string, body: arkts.AstNode, returnTypeNode: arkts.TypeNode, @@ -497,9 +482,9 @@ function createMethodDefinition(methodName: string, function observePropIfNeeded(propertyName: string, propertyValue: arkts.Expression | undefined, classContext: ClassContext) { - const isClassFullyObserved = classContext.isObserved() && classContext.trackedProperties.length == 0 - const isTrackedProp = classContext.tracedProperties.includes(propertyName) - const isTracedProp = classContext.trackedProperties.includes(propertyName) + const isClassFullyObserved = classContext.isObservedV1() && classContext.trackedPropertyNames.length == 0 + const isTrackedProp = classContext.tracedPropertyNames.includes(propertyName) + const isTracedProp = classContext.trackedPropertyNames.includes(propertyName) if (propertyValue && (isClassFullyObserved || isTrackedProp || isTracedProp)) { return arkts.factory.createCallExpression( arkts.factory.createIdentifier("observableProxy"), @@ -524,28 +509,6 @@ function getObservedDecoratorType(definition: arkts.ClassDefinition): ObservedDe return undefined } -function createCheckThisCurrentClass(): arkts.Expression { - return arkts.factory.createBinaryExpression( - arkts.factory.createCallExpression( - fieldOf(arkts.factory.createIdentifier("Class"), "of"), - [arkts.factory.createThisExpression()], - undefined, - false, - false, - undefined, - ), - arkts.factory.createCallExpression( - fieldOf(arkts.factory.createIdentifier("Class"), "current"), - [], - undefined, - false, - false, - undefined, - ), - arkts.Es2pandaTokenType.TOKEN_TYPE_PUNCTUATOR_EQUAL - ) -} - function isStaticProperty(property: arkts.ClassProperty): boolean { return arkts.hasModifierFlag(property, arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_STATIC) } @@ -574,32 +537,50 @@ function recreateDisposedState(property: arkts.ClassProperty, class ClassContext { readonly decoratorType: ObservedDecoratorType | undefined - readonly trackedProperties: readonly string[] = [] - readonly tracedProperties: readonly string[] = [] + readonly properties: ReadonlyMap constructor(observedDecoratorType: ObservedDecoratorType | undefined, - trackedProperties: readonly string[], - tracedProperties: readonly string[]) { + properties: ReadonlyMap) { this.decoratorType = observedDecoratorType - this.trackedProperties = trackedProperties - this.tracedProperties = tracedProperties + this.properties = properties this.verify() } + get trackedPropertyNames() { + return Array.from(this.properties.entries()) + .filter(it => it[1].some(it => it.name == DecoratorNames.TRACK)) + .map(it => it[0]) + } + + get tracedPropertyNames() { + return Array.from(this.properties.entries()) + .filter(it => it[1].some(it => it.name == DecoratorNames.TRACE)) + .map(it => it[0]) + } + + get typedPropertyNames() { + return this.typedProperties.map(it => it[0]) + } + + get typedProperties() { + return Array.from(this.properties.entries()) + .filter(it => it[1].some(it => it.name == DecoratorNames.TYPE)) + } + private verify() { if (this.decoratorType == DecoratorNames.OBSERVED_V2) { - if (this.trackedProperties.length) { + if (this.trackedPropertyNames.length) { throw new Error("@Track decorator is not applicable with @ObservedV2 decorator") } } else { - if (this.tracedProperties.length) { + if (this.tracedPropertyNames.length || this.typedPropertyNames.length) { throw new Error("@Trace decorator is only used with @ObservedV2 decorator") } } } - isObserved(): boolean { + isObservedV1(): boolean { return this.decoratorType == DecoratorNames.OBSERVED } @@ -608,13 +589,23 @@ class ClassContext { } isClassObservable() { - return this.decoratorType || this.trackedProperties.length + return this.decoratorType || this.trackedPropertyNames.length } needRewriteProperty(property: arkts.ClassProperty): boolean { - if (this.isObserved() || this.trackedProperties.length) { + if (this.isObservedV1() || this.trackedPropertyNames.length) { return !isStaticProperty(property) } - return this.isObservedV2() && this.tracedProperties.includes(property.id?.name!) + return this.isObservedV2() && this.tracedPropertyNames.includes(property.id?.name!) + } +} + +class PropertyDecorator { + readonly name: DecoratorNames + readonly args: string[] = [] + + constructor(name: DecoratorNames, args: string[] = []) { + this.name = name + this.args = args } } \ No newline at end of file diff --git a/ui2abc/ui-plugins/src/utils.ts b/ui2abc/ui-plugins/src/utils.ts index 6ac56d09aa61412065dcba8f0d7813a8b1030dcd..072802416acd5445635024152b0753a30d2856e9 100644 --- a/ui2abc/ui-plugins/src/utils.ts +++ b/ui2abc/ui-plugins/src/utils.ts @@ -264,8 +264,11 @@ export enum DecoratorNames { LOCAL_BUILDER = "LocalBuilder", TRACK = "Track", TRACE = "Trace", + TYPE = "Type", } +const DECORATOR_NAMES_SET = new Set(Object.values(DecoratorNames)); + export enum DecoratorParameters { USE_SHARED_STORAGE = "useSharedStorage", ALLOW_OVERRIDE = "allowOverride", @@ -277,8 +280,22 @@ export function hasEntryAnnotation(node: arkts.ClassDefinition): boolean { ) } +export function getAnnotationName(anno: arkts.AnnotationUsage): string | undefined { + if (!anno.expr) { + return undefined + } + return arkts.isIdentifier(anno.expr) ? anno.expr.name : undefined +} + +export function getDecoratorName(anno: arkts.AnnotationUsage): DecoratorNames | undefined { + const name = getAnnotationName(anno); + return name && DECORATOR_NAMES_SET.has(name as DecoratorNames) + ? name as DecoratorNames + : undefined +} + export function isDecoratorAnnotation(anno: arkts.AnnotationUsage, decoratorName: DecoratorNames): boolean { - return !!anno.expr && arkts.isIdentifier(anno.expr) && anno.expr.name === decoratorName; + return getAnnotationName(anno) === decoratorName; } export function hasDecorator(property: arkts.ClassProperty | arkts.ClassDefinition | arkts.ClassDeclaration | arkts.MethodDefinition | arkts.FunctionDeclaration | arkts.ETSParameterExpression, decoratorName: DecoratorNames): boolean {