From 859bf0cf01d86ef2d2add3a549b9fa45d70ecebe Mon Sep 17 00:00:00 2001 From: Jiakai Shi Date: Fri, 27 Jun 2025 10:26:33 +0800 Subject: [PATCH] ui-plugin collector Signed-off-by: Jiakai Shi Change-Id: I1ccfa17e673abc085a70dc23d60de82eded0c26d --- .../memo-collectors/function-collector.ts | 28 +- .../collectors/memo-collectors/utils.ts | 117 ++++--- .../ui-collectors/call-record-collector.ts | 106 ++++++ .../collectors/ui-collectors/factory.ts | 163 +++++++++ .../ui-collectors/global-class-collector.ts | 58 ++++ .../ui-collectors/normal-class-collector.ts | 143 ++++++++ .../normal-interface-collector.ts | 73 ++++ .../ui-collectors/records/annotations/base.ts | 132 +++++++ .../records/annotations/call-declaration.ts | 77 +++++ .../records/annotations/custom-component.ts | 81 +++++ .../records/annotations/function.ts | 62 ++++ .../records/annotations/index.ts | 24 ++ .../annotations/normal-class-method.ts | 51 +++ .../annotations/normal-class-property.ts | 64 ++++ .../records/annotations/normal-class.ts | 56 +++ .../annotations/normal-interface-property.ts | 54 +++ .../records/annotations/struct-method.ts | 55 +++ .../records/annotations/struct-property.ts | 103 ++++++ .../collectors/ui-collectors/records/base.ts | 75 ++++ .../collectors/ui-collectors/records/cache.ts | 167 +++++++++ .../ui-collectors/records/call-declaration.ts | 140 ++++++++ .../ui-collectors/records/function-call.ts | 324 ++++++++++++++++++ .../ui-collectors/records/function.ts | 87 +++++ .../collectors/ui-collectors/records/index.ts | 31 ++ .../records/normal-class-method.ts | 91 +++++ .../records/normal-class-property.ts | 88 +++++ .../ui-collectors/records/normal-class.ts | 98 ++++++ .../records/normal-interface-property.ts | 89 +++++ .../ui-collectors/records/normal-interface.ts | 53 +++ .../ui-collectors/records/record-builder.ts | 45 +++ .../records/struct-interface-property.ts | 90 +++++ .../ui-collectors/records/struct-interface.ts | 66 ++++ .../ui-collectors/records/struct-method.ts | 95 +++++ .../ui-collectors/records/struct-property.ts | 83 +++++ .../ui-collectors/records/struct.ts | 93 +++++ .../collectors/ui-collectors/shared-types.ts | 27 ++ .../ui-collectors/struct-collector.ts | 157 +++++++++ .../struct-interface-collector.ts | 73 ++++ .../collectors/ui-collectors/ui-visitor.ts | 57 +++ .../collectors/ui-collectors/utils.ts | 180 ++++++++++ .../ui-collectors/validators/base.ts | 78 +++++ .../ui-collectors/validators/cache.ts | 33 ++ .../ui-collectors/validators/index.ts | 21 ++ .../normal-class-method-validator.ts | 31 ++ .../normal-class-property-validator.ts | 31 ++ .../validators/rules/check-builder-param.ts | 63 ++++ .../rules/check-component-link-init.ts | 51 +++ .../rules/check-component-v2-mix-use.ts | 135 ++++++++ .../rules/check-componentV2-state-usage.ts | 199 +++++++++++ .../rules/check-computed-decorator.ts | 246 +++++++++++++ .../ui-collectors/validators/rules/index.ts | 20 ++ .../ui-collectors/validators/safe-types.ts | 30 ++ .../validators/struct-call-validator.ts | 37 ++ .../validators/struct-method-validator.ts | 33 ++ .../validators/struct-property-validator.ts | 34 ++ .../ui-collectors/validators/utils.ts | 38 ++ .../validators/validator-builder.ts | 27 ++ arkui-plugins/common/arkts-utils.ts | 20 +- arkui-plugins/common/declaration-collector.ts | 2 +- arkui-plugins/common/log-collector.ts | 74 +++- arkui-plugins/common/predefines.ts | 89 +++-- arkui-plugins/common/safe-types.ts | 4 +- .../memo-plugins/function-transformer.ts | 5 +- arkui-plugins/memo-plugins/index.ts | 8 +- .../memo-plugins/memo-cache-factory.ts | 18 +- .../builder-lambda-translators/factory.ts | 39 ++- .../builder-lambda-translators/utils.ts | 16 +- .../ui-plugins/component-transformer.ts | 37 +- .../ui-plugins/entry-translators/factory.ts | 4 +- arkui-plugins/ui-plugins/index.ts | 7 +- .../property-translators/builderParam.ts | 23 +- .../property-translators/consume.ts | 12 +- .../ui-plugins/property-translators/link.ts | 12 +- .../property-translators/localstoragelink.ts | 14 +- .../property-translators/localstorageprop.ts | 12 +- .../property-translators/objectlink.ts | 10 +- .../property-translators/observedTrack.ts | 14 +- .../ui-plugins/property-translators/prop.ts | 12 +- .../property-translators/provide.ts | 12 +- .../property-translators/regularProperty.ts | 29 +- .../ui-plugins/property-translators/state.ts | 12 +- .../property-translators/staticProperty.ts | 3 +- .../property-translators/storageProp.ts | 12 +- .../property-translators/storagelink.ts | 12 +- .../ui-plugins/property-translators/utils.ts | 39 ++- .../ui-plugins/struct-translators/utils.ts | 8 +- arkui-plugins/ui-plugins/ui-factory.ts | 3 +- arkui-plugins/ui-plugins/utils.ts | 15 +- .../ui-syntax-plugins/utils/index.ts | 2 +- koala-wrapper/native/src/common.cc | 69 +++- koala-wrapper/src/Es2pandaNativeModule.ts | 12 + koala-wrapper/src/arkts-api/class-by-peer.ts | 21 +- koala-wrapper/src/arkts-api/index.ts | 1 + koala-wrapper/src/arkts-api/node-by-type.ts | 35 ++ .../node-utilities/ArrowFunctionExpression.ts | 6 +- .../node-utilities/CallExpression.ts | 6 +- .../arkts-api/node-utilities/ClassProperty.ts | 6 +- .../node-utilities/ETSFunctionType.ts | 6 +- .../node-utilities/ETSParameterExpression.ts | 6 +- .../arkts-api/node-utilities/ETSUnionType.ts | 6 +- .../arkts-api/node-utilities/Identifier.ts | 6 +- .../node-utilities/MethodDefinition.ts | 6 +- .../src/arkts-api/node-utilities/Property.ts | 6 +- .../node-utilities/ReturnStatement.ts | 6 +- .../node-utilities/ScriptFunction.ts | 6 +- .../node-utilities/TSTypeAliasDeclaration.ts | 6 +- .../node-utilities/VariableDeclarator.ts | 6 +- koala-wrapper/src/arkts-api/peers/AstNode.ts | 23 ++ .../src/arkts-api/static/globalUtils.ts | 2 +- koala-wrapper/src/arkts-api/types.ts | 4 +- .../src/arkts-api/utilities/nodeCache.ts | 64 +++- .../src/arkts-api/utilities/public.ts | 10 +- koala-wrapper/src/reexport-for-generated.ts | 2 +- 113 files changed, 5296 insertions(+), 367 deletions(-) create mode 100644 arkui-plugins/collectors/ui-collectors/call-record-collector.ts create mode 100644 arkui-plugins/collectors/ui-collectors/factory.ts create mode 100644 arkui-plugins/collectors/ui-collectors/global-class-collector.ts create mode 100644 arkui-plugins/collectors/ui-collectors/normal-class-collector.ts create mode 100644 arkui-plugins/collectors/ui-collectors/normal-interface-collector.ts create mode 100644 arkui-plugins/collectors/ui-collectors/records/annotations/base.ts create mode 100644 arkui-plugins/collectors/ui-collectors/records/annotations/call-declaration.ts create mode 100644 arkui-plugins/collectors/ui-collectors/records/annotations/custom-component.ts create mode 100644 arkui-plugins/collectors/ui-collectors/records/annotations/function.ts create mode 100644 arkui-plugins/collectors/ui-collectors/records/annotations/index.ts create mode 100644 arkui-plugins/collectors/ui-collectors/records/annotations/normal-class-method.ts create mode 100644 arkui-plugins/collectors/ui-collectors/records/annotations/normal-class-property.ts create mode 100644 arkui-plugins/collectors/ui-collectors/records/annotations/normal-class.ts create mode 100644 arkui-plugins/collectors/ui-collectors/records/annotations/normal-interface-property.ts create mode 100644 arkui-plugins/collectors/ui-collectors/records/annotations/struct-method.ts create mode 100644 arkui-plugins/collectors/ui-collectors/records/annotations/struct-property.ts create mode 100644 arkui-plugins/collectors/ui-collectors/records/base.ts create mode 100644 arkui-plugins/collectors/ui-collectors/records/cache.ts create mode 100644 arkui-plugins/collectors/ui-collectors/records/call-declaration.ts create mode 100644 arkui-plugins/collectors/ui-collectors/records/function-call.ts create mode 100644 arkui-plugins/collectors/ui-collectors/records/function.ts create mode 100644 arkui-plugins/collectors/ui-collectors/records/index.ts create mode 100644 arkui-plugins/collectors/ui-collectors/records/normal-class-method.ts create mode 100644 arkui-plugins/collectors/ui-collectors/records/normal-class-property.ts create mode 100644 arkui-plugins/collectors/ui-collectors/records/normal-class.ts create mode 100644 arkui-plugins/collectors/ui-collectors/records/normal-interface-property.ts create mode 100644 arkui-plugins/collectors/ui-collectors/records/normal-interface.ts create mode 100644 arkui-plugins/collectors/ui-collectors/records/record-builder.ts create mode 100644 arkui-plugins/collectors/ui-collectors/records/struct-interface-property.ts create mode 100644 arkui-plugins/collectors/ui-collectors/records/struct-interface.ts create mode 100644 arkui-plugins/collectors/ui-collectors/records/struct-method.ts create mode 100644 arkui-plugins/collectors/ui-collectors/records/struct-property.ts create mode 100644 arkui-plugins/collectors/ui-collectors/records/struct.ts create mode 100644 arkui-plugins/collectors/ui-collectors/shared-types.ts create mode 100644 arkui-plugins/collectors/ui-collectors/struct-collector.ts create mode 100644 arkui-plugins/collectors/ui-collectors/struct-interface-collector.ts create mode 100644 arkui-plugins/collectors/ui-collectors/ui-visitor.ts create mode 100644 arkui-plugins/collectors/ui-collectors/utils.ts create mode 100644 arkui-plugins/collectors/ui-collectors/validators/base.ts create mode 100644 arkui-plugins/collectors/ui-collectors/validators/cache.ts create mode 100644 arkui-plugins/collectors/ui-collectors/validators/index.ts create mode 100644 arkui-plugins/collectors/ui-collectors/validators/normal-class-method-validator.ts create mode 100644 arkui-plugins/collectors/ui-collectors/validators/normal-class-property-validator.ts create mode 100644 arkui-plugins/collectors/ui-collectors/validators/rules/check-builder-param.ts create mode 100644 arkui-plugins/collectors/ui-collectors/validators/rules/check-component-link-init.ts create mode 100644 arkui-plugins/collectors/ui-collectors/validators/rules/check-component-v2-mix-use.ts create mode 100644 arkui-plugins/collectors/ui-collectors/validators/rules/check-componentV2-state-usage.ts create mode 100644 arkui-plugins/collectors/ui-collectors/validators/rules/check-computed-decorator.ts create mode 100644 arkui-plugins/collectors/ui-collectors/validators/rules/index.ts create mode 100644 arkui-plugins/collectors/ui-collectors/validators/safe-types.ts create mode 100644 arkui-plugins/collectors/ui-collectors/validators/struct-call-validator.ts create mode 100644 arkui-plugins/collectors/ui-collectors/validators/struct-method-validator.ts create mode 100644 arkui-plugins/collectors/ui-collectors/validators/struct-property-validator.ts create mode 100644 arkui-plugins/collectors/ui-collectors/validators/utils.ts create mode 100644 arkui-plugins/collectors/ui-collectors/validators/validator-builder.ts create mode 100644 koala-wrapper/src/arkts-api/node-by-type.ts diff --git a/arkui-plugins/collectors/memo-collectors/function-collector.ts b/arkui-plugins/collectors/memo-collectors/function-collector.ts index df1f8fd4f..8719667be 100644 --- a/arkui-plugins/collectors/memo-collectors/function-collector.ts +++ b/arkui-plugins/collectors/memo-collectors/function-collector.ts @@ -15,6 +15,7 @@ import * as arkts from '@koalaui/libarkts'; import { AbstractVisitor } from '../../common/abstract-visitor'; +import { NodeCacheNames } from '../../common/predefines'; import { checkIsMemoFromMemoableInfo, collectMemoableInfoInFunctionReturnType, @@ -49,14 +50,14 @@ export class MemoFunctionCollector extends AbstractVisitor { private collectMemoAstNode(node: arkts.AstNode, info: MemoableInfo): void { if (checkIsMemoFromMemoableInfo(info, false)) { - arkts.NodeCache.getInstance().collect(node); + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).collect(node); } } private collectCallWithDeclaredPeerInParamMap(node: arkts.CallExpression, peer: arkts.AstNode['peer']): void { const memoableInfo = this.paramMemoableInfoMap!.get(peer)!; if (checkIsMemoFromMemoableInfo(memoableInfo, true)) { - arkts.NodeCache.getInstance().collect(node); + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).collect(node); } } @@ -65,10 +66,11 @@ export class MemoFunctionCollector extends AbstractVisitor { declarator: arkts.VariableDeclarator ): void { const shouldCollect = - arkts.NodeCache.getInstance().has(declarator) || - (!!declarator.initializer && arkts.NodeCache.getInstance().has(declarator.initializer)); + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).has(declarator) || + (!!declarator.initializer && + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).has(declarator.initializer)); if (shouldCollect) { - arkts.NodeCache.getInstance().collect(node); + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).collect(node); } } @@ -114,7 +116,7 @@ export class MemoFunctionCollector extends AbstractVisitor { } private visitCallExpression(node: arkts.CallExpression): arkts.AstNode { - if (arkts.NodeCache.getInstance().has(node)) { + if (arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).has(node)) { this.shouldCollectReturn = false; this.visitEachChild(node); this.shouldCollectReturn = true; @@ -128,8 +130,8 @@ export class MemoFunctionCollector extends AbstractVisitor { this.shouldCollectReturn = true; return node; } - if (arkts.NodeCache.getInstance().has(decl)) { - arkts.NodeCache.getInstance().collect(node); + if (arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).has(decl)) { + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).collect(node); } if (this.paramMemoableInfoMap?.has(decl.peer)) { this.collectCallWithDeclaredPeerInParamMap(node, decl.peer); @@ -150,9 +152,9 @@ export class MemoFunctionCollector extends AbstractVisitor { return node; } if (this.paramMemoableInfoMap?.has(decl.peer)) { - arkts.NodeCache.getInstance().collect(node); + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).collect(node); } else if (arkts.isEtsParameterExpression(decl) && this.paramMemoableInfoMap?.has(decl.identifier.peer)) { - arkts.NodeCache.getInstance().collect(node); + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).collect(node); } return node; } @@ -161,7 +163,7 @@ export class MemoFunctionCollector extends AbstractVisitor { if (!!this.returnMemoableInfo && !!node.argument && arkts.isArrowFunctionExpression(node.argument)) { this.collectMemoAstNode(node.argument, this.returnMemoableInfo); } - arkts.NodeCache.getInstance().collect(node); + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).collect(node); this.visitEachChild(node); return node; } @@ -208,8 +210,8 @@ export class MemoFunctionCollector extends AbstractVisitor { } if ( arkts.isArrowFunctionExpression(node) && - !arkts.NodeCache.getInstance().has(node) && - !arkts.NodeCache.getInstance().has(node.scriptFunction) + !arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).has(node) && + !arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).has(node.scriptFunction) ) { this.shouldCollectReturn = false; } diff --git a/arkui-plugins/collectors/memo-collectors/utils.ts b/arkui-plugins/collectors/memo-collectors/utils.ts index 1fe12a46d..8c04fdbd6 100644 --- a/arkui-plugins/collectors/memo-collectors/utils.ts +++ b/arkui-plugins/collectors/memo-collectors/utils.ts @@ -16,7 +16,7 @@ import * as arkts from '@koalaui/libarkts'; import { annotation, forEachArgWithParam, isDecoratorAnnotation } from '../../common/arkts-utils'; import { ImportCollector } from '../../common/import-collector'; -import { DecoratorNames, GenSymPrefix, MEMO_IMPORT_SOURCE_NAME } from '../../common/predefines'; +import { DecoratorNames, BuiltInNames, MEMO_IMPORT_SOURCE_NAME, NodeCacheNames } from '../../common/predefines'; import { MemoFunctionCollector } from './function-collector'; export enum MemoNames { @@ -77,11 +77,11 @@ export function addMemoAnnotation(node: T, memoName: Memo collectMemoAnnotationImport(memoName); if (arkts.isEtsParameterExpression(node)) { node.annotations = newAnnotations; - arkts.NodeCache.getInstance().collect(node); + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).collect(node); return node; } const newNode = node.setAnnotations(newAnnotations) as T; - arkts.NodeCache.getInstance().collect(newNode); + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).collect(newNode); return newNode; } @@ -120,7 +120,7 @@ export function collectMemoAnnotationSource(memoName: MemoNames = MemoNames.MEMO export function collectMemoableInfoInUnionType(node: arkts.AstNode, info?: MemoableInfo): MemoableInfo { let currInfo = info ?? {}; - if (arkts.NodeCache.getInstance().has(node)) { + if (arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).has(node)) { return { ...currInfo, hasMemo: true, hasProperType: true }; } if (!arkts.isETSUnionType(node)) { @@ -140,7 +140,7 @@ export function collectMemoableInfoInUnionType(node: arkts.AstNode, info?: Memoa export function collectMemoableInfoInTypeReference(node: arkts.AstNode, info?: MemoableInfo): MemoableInfo { let currInfo = info ?? {}; - if (arkts.NodeCache.getInstance().has(node)) { + if (arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).has(node)) { return { ...currInfo, hasMemo: true, hasProperType: true }; } if (!arkts.isETSTypeReference(node) || !node.part || !arkts.isETSTypeReferencePart(node.part)) { @@ -159,7 +159,7 @@ export function collectMemoableInfoInTypeReference(node: arkts.AstNode, info?: M export function collectMemoableInfoInFunctionType(node: arkts.AstNode, info?: MemoableInfo): MemoableInfo { let currInfo = info ?? {}; - if (arkts.NodeCache.getInstance().has(node)) { + if (arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).has(node)) { return { ...currInfo, hasMemo: true, hasProperType: true }; } if (!arkts.isETSFunctionType(node)) { @@ -172,7 +172,7 @@ export function collectMemoableInfoInFunctionType(node: arkts.AstNode, info?: Me export function collectMemoableInfoInTypeAlias(node: arkts.AstNode, info?: MemoableInfo): MemoableInfo { let currInfo = info ?? {}; - if (arkts.NodeCache.getInstance().has(node)) { + if (arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).has(node)) { return { ...currInfo, hasMemo: true, hasProperType: true }; } if (!arkts.isTSTypeAliasDeclaration(node)) { @@ -193,7 +193,7 @@ export function collectMemoableInfoInTypeAlias(node: arkts.AstNode, info?: Memoa export function collectMemoableInfoInParameter(node: arkts.AstNode, info?: MemoableInfo): MemoableInfo { let currInfo = info ?? {}; - if (arkts.NodeCache.getInstance().has(node)) { + if (arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).has(node)) { return { ...currInfo, hasMemo: true, hasProperType: true }; } if (!arkts.isEtsParameterExpression(node)) { @@ -220,7 +220,7 @@ export function collectMemoableInfoInParameter(node: arkts.AstNode, info?: Memoa export function collectMemoableInfoInVariableDeclarator(node: arkts.AstNode, info?: MemoableInfo): MemoableInfo { let currInfo = info ?? {}; - if (arkts.NodeCache.getInstance().has(node)) { + if (arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).has(node)) { return { ...currInfo, hasMemo: true, hasProperType: true }; } if (!arkts.isVariableDeclarator(node)) { @@ -264,7 +264,7 @@ export function collectMemoableInfoInVariableDeclarator(node: arkts.AstNode, inf export function collectMemoableInfoInProperty(node: arkts.AstNode, info?: MemoableInfo): MemoableInfo { let currInfo = info ?? {}; - if (arkts.NodeCache.getInstance().has(node)) { + if (arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).has(node)) { const property = node as arkts.Property; const hasProperType = !!property.value && arkts.isArrowFunctionExpression(property.value); return { ...currInfo, hasMemo: true, hasProperType }; @@ -302,7 +302,7 @@ export function collectMemoableInfoInProperty(node: arkts.AstNode, info?: Memoab export function collectMemoableInfoInClassProperty(node: arkts.AstNode, info?: MemoableInfo): MemoableInfo { let currInfo = info ?? {}; - if (arkts.NodeCache.getInstance().has(node)) { + if (arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).has(node)) { return { ...currInfo, hasMemo: true, hasProperType: true }; } if (!arkts.isClassProperty(node)) { @@ -326,7 +326,7 @@ export function collectMemoableInfoInClassProperty(node: arkts.AstNode, info?: M export function collectMemoableInfoInArrowFunction(node: arkts.AstNode, info?: MemoableInfo): MemoableInfo { let currInfo = info ?? {}; - if (arkts.NodeCache.getInstance().has(node)) { + if (arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).has(node)) { return { ...currInfo, hasMemo: true, hasProperType: true }; } if (!arkts.isArrowFunctionExpression(node)) { @@ -358,7 +358,7 @@ export function collectMemoableInfoInArrowFunction(node: arkts.AstNode, info?: M export function collectMemoableInfoInScriptFunction(node: arkts.AstNode, info?: MemoableInfo): MemoableInfo { let currInfo = info ?? {}; - if (arkts.NodeCache.getInstance().has(node)) { + if (arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).has(node)) { return { ...currInfo, hasMemo: true, hasProperType: true }; } if (!arkts.isScriptFunction(node)) { @@ -399,13 +399,13 @@ export function collectMemoableInfoInType(node: arkts.AstNode, info?: MemoableIn export function collectMemoableInfoInFunctionReturnType(node: arkts.ScriptFunction): MemoableInfo { if (!!node.returnTypeAnnotation) { let memoableInfo: MemoableInfo; - if (arkts.NodeCache.getInstance().has(node.returnTypeAnnotation)) { + if (arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).has(node.returnTypeAnnotation)) { memoableInfo = { hasMemo: true, hasProperType: true }; } else { memoableInfo = collectMemoableInfoInType(node.returnTypeAnnotation); } if ((memoableInfo.hasMemo || memoableInfo.hasBuilder) && memoableInfo.hasProperType) { - arkts.NodeCache.getInstance().collect(node.returnTypeAnnotation); + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).collect(node.returnTypeAnnotation); } return memoableInfo; } @@ -416,7 +416,7 @@ export function collectGensymDeclarator(declarator: arkts.VariableDeclarator, in if (!info.hasMemo && !info.hasBuilder) { return; } - arkts.NodeCache.getInstance().collect(declarator); + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).collect(declarator); const initializer = declarator.initializer; if (!initializer || !arkts.isConditionalExpression(initializer)) { return; @@ -475,14 +475,18 @@ function collectMemoableInfoInFunctionParam( const peers: arkts.AstNode['peer'][] = []; let memoableInfo: MemoableInfo; const _param = param as arkts.ETSParameterExpression; - if (arkts.NodeCache.getInstance().has(_param)) { - const metadata = arkts.NodeCache.getInstance().get(_param)!.metadata ?? {}; + if (arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).has(_param)) { + const metadata = arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).get(_param)!.metadata ?? {}; const { hasMemoSkip } = metadata; memoableInfo = { hasMemo: true, hasMemoSkip, hasProperType: true }; } else { memoableInfo = collectMemoableInfoInParameter(_param); } - if (_param.identifier.name.startsWith(GenSymPrefix.INTRINSIC) && !!node.body && arkts.isBlockStatement(node.body)) { + if ( + _param.identifier.name.startsWith(BuiltInNames.GENSYM_INTRINSIC_PREFIX) && + !!node.body && + arkts.isBlockStatement(node.body) + ) { const declaration = node.body.statements.at(gensymCount); if (!!declaration && arkts.isVariableDeclaration(declaration) && declaration.declarators.length > 0) { const declarator = declaration.declarators[0]; @@ -494,7 +498,9 @@ function collectMemoableInfoInFunctionParam( } } if (checkIsMemoFromMemoableInfo(memoableInfo)) { - arkts.NodeCache.getInstance().collect(_param, { hasMemoSkip: memoableInfo.hasMemoSkip }); + arkts.NodeCacheFactory.getInstance() + .getCache(NodeCacheNames.MEMO) + .collect(_param, { hasMemoSkip: memoableInfo.hasMemoSkip }); } if (!memoableInfo.hasMemoSkip && shouldCollectParameter) { peers.push(_param.identifier.peer); @@ -516,7 +522,7 @@ export function findCanAddMemoFromTypeAnnotation( } const memoableInfo = collectMemoableInfoInType(typeAnnotation); if (!!memoableInfo.hasMemo && !!memoableInfo.hasProperType) { - arkts.NodeCache.getInstance().collect(typeAnnotation); + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).collect(typeAnnotation); } return !!memoableInfo.hasBuilder && !memoableInfo.hasMemo && !!memoableInfo.hasProperType; } @@ -530,7 +536,7 @@ export function findCanAddMemoFromTypeAnnotation( export function findCanAddMemoFromProperty(property: arkts.AstNode): property is arkts.Property { const memoableInfo = collectMemoableInfoInProperty(property); if (!!memoableInfo.hasMemo && !!memoableInfo.hasProperType) { - arkts.NodeCache.getInstance().collect(property); + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).collect(property); } return !!memoableInfo.hasBuilder && !memoableInfo.hasMemo && !!memoableInfo.hasProperType; } @@ -544,7 +550,7 @@ export function findCanAddMemoFromProperty(property: arkts.AstNode): property is export function findCanAddMemoFromClassProperty(property: arkts.AstNode): property is arkts.ClassProperty { const memoableInfo = collectMemoableInfoInClassProperty(property); if (!!memoableInfo.hasMemo && !!memoableInfo.hasProperType) { - arkts.NodeCache.getInstance().collect(property); + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).collect(property); } return ( (!!memoableInfo.hasBuilder || !!memoableInfo.hasBuilderParam) && @@ -565,7 +571,9 @@ export function findCanAddMemoFromParameter(param: arkts.AstNode | undefined): p } const memoableInfo = collectMemoableInfoInParameter(param); if (!!memoableInfo.hasMemo && !!memoableInfo.hasProperType) { - arkts.NodeCache.getInstance().collect(param, { hasMemoSkip: memoableInfo.hasMemoSkip }); + arkts.NodeCacheFactory.getInstance() + .getCache(NodeCacheNames.MEMO) + .collect(param, { hasMemoSkip: memoableInfo.hasMemoSkip }); } return !!memoableInfo.hasBuilder && !memoableInfo.hasMemo && !!memoableInfo.hasProperType; } @@ -589,11 +597,15 @@ export function findCanAddMemoFromArrowFunction(node: arkts.AstNode): node is ar ); const isMemoReturnType = checkIsMemoFromMemoableInfo(returnMemoableInfo); if (isMemoReturnType) { - arkts.NodeCache.getInstance().collect(node.scriptFunction.returnTypeAnnotation!); + arkts.NodeCacheFactory.getInstance() + .getCache(NodeCacheNames.MEMO) + .collect(node.scriptFunction.returnTypeAnnotation!); } const isMemo = checkIsMemoFromMemoableInfo(memoableInfo); - if (isMemo && !arkts.NodeCache.getInstance().has(node)) { - arkts.NodeCache.getInstance().collect(node, { hasMemoEntry, hasMemoIntrinsic }); + if (isMemo && !arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).has(node)) { + arkts.NodeCacheFactory.getInstance() + .getCache(NodeCacheNames.MEMO) + .collect(node, { hasMemoEntry, hasMemoIntrinsic }); if (!!node.scriptFunction.body && arkts.isBlockStatement(node.scriptFunction.body)) { const disableCollectReturn = hasMemoEntry || hasMemoIntrinsic; collectMemoScriptFunctionBody( @@ -617,7 +629,7 @@ export function findCanAddMemoFromArrowFunction(node: arkts.AstNode): node is ar export function findCanAddMemoFromTypeAlias(node: arkts.AstNode): node is arkts.TSTypeAliasDeclaration { const memoableInfo = collectMemoableInfoInTypeAlias(node); if (!!memoableInfo.hasMemo && !!memoableInfo.hasProperType) { - arkts.NodeCache.getInstance().collect(node); + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).collect(node); } return !!memoableInfo.hasBuilder && !memoableInfo.hasMemo && !!memoableInfo.hasProperType; } @@ -642,15 +654,19 @@ export function findCanAddMemoFromMethod(node: arkts.AstNode): node is arkts.Met const isMemo = checkIsMemoFromMemoableInfo(memoableInfo); const isMemoReturnType = checkIsMemoFromMemoableInfo(returnMemoableInfo); if (isMemoReturnType) { - arkts.NodeCache.getInstance().collect(node.scriptFunction.returnTypeAnnotation!); + arkts.NodeCacheFactory.getInstance() + .getCache(NodeCacheNames.MEMO) + .collect(node.scriptFunction.returnTypeAnnotation!); } - if (isMemo && !arkts.NodeCache.getInstance().has(node)) { + if (isMemo && !arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).has(node)) { const metadata = collectMetadataInMethod(node); - arkts.NodeCache.getInstance().collect(node, { - ...metadata, - hasMemoEntry, - hasMemoIntrinsic, - }); + arkts.NodeCacheFactory.getInstance() + .getCache(NodeCacheNames.MEMO) + .collect(node, { + ...metadata, + hasMemoEntry, + hasMemoIntrinsic, + }); if (!!node.scriptFunction.body && arkts.isBlockStatement(node.scriptFunction.body)) { const disableCollectReturn = hasMemoEntry || hasMemoIntrinsic; collectMemoScriptFunctionBody( @@ -672,7 +688,7 @@ export function findCanAddMemoFromMethod(node: arkts.AstNode): node is arkts.Met * @param node `arkts.CallExpression` node. */ export function collectMemoFromCallExpression(node: arkts.CallExpression): void { - if (arkts.NodeCache.getInstance().has(node)) { + if (arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).has(node)) { return; } const expr = findIdentifierFromCallee(node.expression); @@ -681,8 +697,8 @@ export function collectMemoFromCallExpression(node: arkts.CallExpression): void return; } let isCollected: boolean = false; - if (arkts.NodeCache.getInstance().has(decl)) { - arkts.NodeCache.getInstance().collect(node); + if (arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).has(decl)) { + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).collect(node); isCollected = true; } if (arkts.isMethodDefinition(decl)) { @@ -691,18 +707,18 @@ export function collectMemoFromCallExpression(node: arkts.CallExpression): void isCollected = collectCallWithDeclaredClassProperty(node, decl); } if (isCollected && arkts.isTSAsExpression(node.expression) && node.expression.typeAnnotation) { - arkts.NodeCache.getInstance().collect(node.expression.typeAnnotation); + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).collect(node.expression.typeAnnotation); } } export function collectCallWithDeclaredClassProperty(node: arkts.CallExpression, decl: arkts.ClassProperty): boolean { - if (arkts.NodeCache.getInstance().has(decl)) { - arkts.NodeCache.getInstance().collect(node); + if (arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).has(decl)) { + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).collect(node); return true; } const memoableInfo = collectMemoableInfoInClassProperty(decl); if (checkIsMemoFromMemoableInfo(memoableInfo, false) || memoableInfo.hasBuilder || memoableInfo.hasBuilderParam) { - arkts.NodeCache.getInstance().collect(node); + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).collect(node); return true; } return false; @@ -716,15 +732,20 @@ export function collectCallWithDeclaredMethod(node: arkts.CallExpression, decl: const isTrailingCall = node.isTrailingCall; const options = { hasRestParameter, isTrailingCall }; forEachArgWithParam(args, params, collectCallArgsWithMethodParams, options); - if (arkts.NodeCache.getInstance().has(decl)) { - const { hasMemoEntry, hasMemoIntrinsic } = arkts.NodeCache.getInstance().get(decl)!.metadata ?? {}; - arkts.NodeCache.getInstance().collect(node, { hasReceiver, hasMemoEntry, hasMemoIntrinsic }); + if (arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).has(decl)) { + const { hasMemoEntry, hasMemoIntrinsic } = + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).get(decl)!.metadata ?? {}; + arkts.NodeCacheFactory.getInstance() + .getCache(NodeCacheNames.MEMO) + .collect(node, { hasReceiver, hasMemoEntry, hasMemoIntrinsic }); return true; } else { const memoableInfo = collectMemoableInfoInScriptFunction(decl.scriptFunction); if (checkIsMemoFromMemoableInfo(memoableInfo, true)) { const { hasMemoEntry, hasMemoIntrinsic } = memoableInfo; - arkts.NodeCache.getInstance().collect(node, { hasReceiver, hasMemoEntry, hasMemoIntrinsic }); + arkts.NodeCacheFactory.getInstance() + .getCache(NodeCacheNames.MEMO) + .collect(node, { hasReceiver, hasMemoEntry, hasMemoIntrinsic }); return true; } } @@ -736,13 +757,13 @@ export function collectCallArgsWithMethodParams(arg: arkts.Expression | undefine return; } let info: MemoableInfo; - if (arkts.NodeCache.getInstance().has(param)) { + if (arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).has(param)) { info = { hasMemo: true, hasProperType: true }; } else { info = collectMemoableInfoInParameter(param); } if (checkIsMemoFromMemoableInfo(info) && arkts.isArrowFunctionExpression(arg)) { - arkts.NodeCache.getInstance().collect(arg); + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).collect(arg); const returnMemoableInfo = collectMemoableInfoInFunctionReturnType(arg.scriptFunction); const [paramMemoableInfoMap, gensymCount] = collectMemoableInfoMapInFunctionParams(arg.scriptFunction); if (!!arg.scriptFunction.body && arkts.isBlockStatement(arg.scriptFunction.body)) { diff --git a/arkui-plugins/collectors/ui-collectors/call-record-collector.ts b/arkui-plugins/collectors/ui-collectors/call-record-collector.ts new file mode 100644 index 000000000..021cc24cb --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/call-record-collector.ts @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { CallInfo, CallRecord } from './records'; +import { UICollectMetadata } from './shared-types'; +import { findRootCallee, findRootCallObject } from './utils'; +import { StructCallValidator, ValidatorBuilder } from './validators'; + +export interface CallRecordInfo { + call: arkts.CallExpression; + callInfo: CallInfo; +} + +export class CallRecordCollector { + private static instance: CallRecordCollector; + private _metadata: UICollectMetadata; + private _rootCallInfo?: CallRecordInfo; + private _prevCallInfo?: CallRecordInfo; + private _chainingCallNames?: string[]; + private _shouldCollectPreviousCall: boolean = false; + + private constructor(metadata: UICollectMetadata) { + this._metadata = metadata; + } + + static getInstance(metadata: UICollectMetadata): CallRecordCollector { + if (!this.instance) { + this.instance = new CallRecordCollector(metadata); + } + return this.instance; + } + + private get prevCallInfo(): CallRecordInfo | undefined { + return this._prevCallInfo; + } + + private set prevCallInfo(prevCallInfo: CallRecordInfo | undefined) { + if (this._shouldCollectPreviousCall && prevCallInfo?.callInfo.ptr !== this._prevCallInfo?.callInfo.ptr) { + this.finishLastCollectInPreviousCall(); + this._shouldCollectPreviousCall = false; + } + this._prevCallInfo = prevCallInfo; + if (this._prevCallInfo !== undefined) { + this._shouldCollectPreviousCall = true; + } + } + + private finishLastCollectInPreviousCall(): void { + if (!this._prevCallInfo) { + return; + } + // TODO: cache the call with full chaining + const { call, callInfo } = this._prevCallInfo; + // console.log('[CALL] node: ', call.dumpSrc()); + // console.log('[CALL] node info: ', callInfo); + ValidatorBuilder.build(StructCallValidator).checkIsViolated(call, callInfo); + } + + reset() { + this.prevCallInfo = undefined; + this._rootCallInfo = undefined; + this._chainingCallNames = undefined; + this._shouldCollectPreviousCall = false; + } + + collect(call: arkts.CallExpression): void { + const callRecord = new CallRecord(this._metadata); + + const rootCallObject = findRootCallObject(call.expression); + const rootCallee = findRootCallee(call.expression); + callRecord.withRootCallObject(rootCallObject).withRootCallee(rootCallee); + const isWithInChain = !!this._rootCallInfo && call.expression.findNodeInInnerChild(this._rootCallInfo.call); + if (isWithInChain) { + if (!!rootCallee?.name) { + this._chainingCallNames?.push(rootCallee.name); + } + callRecord.withRootCallInfo(this._rootCallInfo!.callInfo); + } else { + this._chainingCallNames = !!rootCallee?.name ? [rootCallee.name] : undefined; + } + if (!!this._chainingCallNames) { + callRecord.withChainingCallNames(this._chainingCallNames); + } + + callRecord.collect(call); + const callInfo = callRecord.toRecord()!; + if (!isWithInChain) { + this._rootCallInfo = { call, callInfo }; + this.finishLastCollectInPreviousCall(); + } + this.prevCallInfo = { call, callInfo }; + } +} diff --git a/arkui-plugins/collectors/ui-collectors/factory.ts b/arkui-plugins/collectors/ui-collectors/factory.ts new file mode 100644 index 000000000..87a652c6f --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/factory.ts @@ -0,0 +1,163 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { + checkIsCustomComponentDeclaredClassFromInfo, + checkIsCustomComponentFromInfo, + checkIsObservedClassFromInfo, + checkIsETSGlobalClassFromInfo, + checkIsCommonMethodInterfaceFromInfo, +} from './utils'; +import { + CustomComponentInterfaceRecord, + CustomComponentRecord, + NormalClassRecord, + NormalInterfaceRecord, +} from './records'; +import { StructCollector } from './struct-collector'; +import { GlobalClassCollector } from './global-class-collector'; +import { NormalClassCollector } from './normal-class-collector'; +import { StructInterfaceCollector } from './struct-interface-collector'; +import { NormalInterfaceCollector } from './normal-interface-collector'; +import { CallRecordCollector } from './call-record-collector'; +import { UICollectMetadata } from './shared-types'; +import { ARKUI_COMPONENT_COMMON_SOURCE_NAME, NodeCacheNames } from '../../common/predefines'; +import { LogCollector } from '../../common/log-collector'; + +export function findAndCollectUINodeInPreOrder(node: arkts.AstNode, metadata?: UICollectMetadata): void { + const type = arkts.nodeType(node); + if (preOrderCollectByType.has(type)) { + preOrderCollectByType.get(type)!(node, metadata); + } +} + +export function findAndCollectUINodeInPostOrder(node: arkts.AstNode, metadata?: UICollectMetadata): void { + const type = arkts.nodeType(node); + if (postOrderCollectByType.has(type)) { + postOrderCollectByType.get(type)!(node, metadata); + } +} + +export class CollectFactory { + static findAndCollectClass(node: arkts.ClassDeclaration, metadata: UICollectMetadata): arkts.AstNode { + const structRecord = new CustomComponentRecord(metadata); + structRecord.collect(node); + + let classInfo = structRecord.toRecord(); + if (!!classInfo && checkIsCustomComponentFromInfo(classInfo)) { + // TODO: collect CustomComponent Class + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.UI).collect(node, structRecord.toJSON()); + const structCollector = new StructCollector({ ...metadata, structRecord }); + structCollector.visitor(node); + structCollector.reset(); + // console.log("[STRUCT] node: ", node.dumpSrc()); + return node; + } + if (!!classInfo && checkIsCustomComponentDeclaredClassFromInfo(classInfo)) { + // TODO: collect CustomComponent Declaration Class + const structCollector = new StructCollector({ ...metadata, structRecord }); + structCollector.disableCollectProperty().visitor(node); + structCollector.reset(); + // console.log("[CUSTOM COMPONENT DECL] node: ", node.dumpSrc()); + return node; + } + + const classRecord = new NormalClassRecord(metadata); + classRecord.collect(node); + + classInfo = classRecord.toRecord(); + // console.log("[CLASS] node: ", node.dumpSrc()); + // console.log("[CLASS] classInfo: ", classInfo?.annotationInfo); + if (!!classInfo && checkIsETSGlobalClassFromInfo(classInfo)) { + const globalClassCollector = new GlobalClassCollector(metadata); + globalClassCollector.visitor(node); + globalClassCollector.reset(); + // console.log("[ETSGLOBAL CLASS] node: ", node.dumpSrc()); + return node; + } + if (!!classInfo) { + if (checkIsObservedClassFromInfo(classInfo)) { + // TODO: collect @Observed Normal Class + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.UI).collect(node, classRecord.toJSON()); + } + const normalClassCollector = new NormalClassCollector({ ...metadata, classRecord }); + normalClassCollector.visitor(node); + normalClassCollector.reset(); + // console.log("[NORMAL CLASS] node: ", node.dumpSrc()); + return node; + } + return node; + } + + static findAndCollectInterface(node: arkts.TSInterfaceDeclaration, metadata: UICollectMetadata): arkts.AstNode { + const interfaceRecord = new CustomComponentInterfaceRecord(metadata); + interfaceRecord.collect(node); + + let interfaceInfo = interfaceRecord.toRecord(); + if (!!interfaceInfo && checkIsCustomComponentFromInfo(interfaceInfo)) { + // TODO: collect __Options_ CustomComponent Interface + const interfaceCollector = new StructInterfaceCollector({ ...metadata, interfaceRecord }); + interfaceCollector.visitor(node); + interfaceCollector.reset(); + // console.log("[STRUCT INTERFACE] node: ", node.dumpSrc()); + return node; + } + + const normalInterfaceRecord = new NormalInterfaceRecord(metadata); + normalInterfaceRecord.collect(node); + + interfaceInfo = normalInterfaceRecord.toRecord(); + if ( + metadata.externalSourceName === ARKUI_COMPONENT_COMMON_SOURCE_NAME && + checkIsCommonMethodInterfaceFromInfo(interfaceInfo) + ) { + // TODO: collect CommonMethod Declared Interface + arkts.NodeCacheFactory.getInstance() + .getCache(NodeCacheNames.UI) + .collect(node, normalInterfaceRecord.toJSON()); + } + + const interfaceCollector = new NormalInterfaceCollector({ + ...metadata, + interfaceRecord: normalInterfaceRecord, + }); + interfaceCollector.visitor(node); + interfaceCollector.reset(); + // console.log("[INTERFACE METHOD] node: ", node.dumpSrc()); + + return node; + } + + static findAndCollectCall(node: arkts.CallExpression, metadata: UICollectMetadata): arkts.AstNode { + CallRecordCollector.getInstance(metadata).collect(node); + return node; + } + + static visitETSModule(node: arkts.EtsScript, metadata: UICollectMetadata): arkts.AstNode { + LogCollector.getInstance().emitLogInfo(); + return node; + } +} + +const preOrderCollectByType = new Map arkts.AstNode>([ + [arkts.Es2pandaAstNodeType.AST_NODE_TYPE_CLASS_DECLARATION, CollectFactory.findAndCollectClass], + [arkts.Es2pandaAstNodeType.AST_NODE_TYPE_TS_INTERFACE_DECLARATION, CollectFactory.findAndCollectInterface], +]); + +const postOrderCollectByType = new Map arkts.AstNode>([ + [arkts.Es2pandaAstNodeType.AST_NODE_TYPE_CALL_EXPRESSION, CollectFactory.findAndCollectCall], + [arkts.Es2pandaAstNodeType.AST_NODE_TYPE_ETS_MODULE, CollectFactory.visitETSModule], +]); diff --git a/arkui-plugins/collectors/ui-collectors/global-class-collector.ts b/arkui-plugins/collectors/ui-collectors/global-class-collector.ts new file mode 100644 index 000000000..4b04d3c94 --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/global-class-collector.ts @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { AbstractVisitor, VisitorOptions } from '../../common/abstract-visitor'; +import { NodeCacheNames } from '../../common/predefines'; +import { FunctionRecord } from './records/function'; + +export interface GlobalClassCollectorOptions extends VisitorOptions { + shouldIgnoreDecl?: boolean; +} + +export class GlobalClassCollector extends AbstractVisitor { + public shouldIgnoreDecl: boolean; + + constructor(options: GlobalClassCollectorOptions) { + super(options); + this.shouldIgnoreDecl = options.shouldIgnoreDecl ?? false; + } + + private collectMethod(node: arkts.MethodDefinition): void { + const methodRecord = new FunctionRecord({ + shouldIgnoreDecl: this.shouldIgnoreDecl, + }); + methodRecord.collect(node); + + const methodInfo = methodRecord.toRecord(); + if (!methodInfo || methodInfo.isGlobalInit || methodInfo.isGlobalMain) { + return; + } + if (!!methodInfo.annotationInfo && Object.keys(methodInfo.annotationInfo).length > 0) { + // TODO: collect method info to cache + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.UI).collect(node, methodRecord.toJSON()); + // console.log("[ETSGLOBAL CLASS METHOD] node: ", node.dumpSrc()); + } + } + + visitor(node: arkts.ClassDeclaration): arkts.ClassDeclaration { + node.definition?.body.forEach((st) => { + if (arkts.isMethodDefinition(st)) { + this.collectMethod(st); + } + }); + return node; + } +} diff --git a/arkui-plugins/collectors/ui-collectors/normal-class-collector.ts b/arkui-plugins/collectors/ui-collectors/normal-class-collector.ts new file mode 100644 index 000000000..934b71199 --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/normal-class-collector.ts @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { AbstractVisitor, VisitorOptions } from '../../common/abstract-visitor'; +import { + NormalClassMethodRecord, + NormalClassPropertyInfo, + NormalClassPropertyRecord, + NormalClassRecord, +} from './records'; +import { BuiltInNames, NodeCacheNames } from '../../common/predefines'; +import { formatBuiltInImplementedPropertyName } from './utils'; +import { NormalClassMethodValidator, NormalClassPropertyValidator, ValidatorBuilder } from './validators'; + +export interface NormalClassCollectorOptions extends VisitorOptions { + classRecord: NormalClassRecord; + shouldIgnoreDecl?: boolean; +} + +interface MethodRecordCollection { + node: arkts.MethodDefinition; + record: NormalClassMethodRecord; +} + +export class NormalClassCollector extends AbstractVisitor { + private _classRecord: NormalClassRecord; + public shouldIgnoreDecl: boolean; + + private _getters: MethodRecordCollection[]; + private _setters: MethodRecordCollection[]; + private _implementedPropertyNames: string[]; + + constructor(options: NormalClassCollectorOptions) { + super(options); + this._classRecord = options.classRecord; + this.shouldIgnoreDecl = options.shouldIgnoreDecl ?? false; + this._getters = []; + this._setters = []; + this._implementedPropertyNames = []; + } + + private canCollectPropertyFromInfo(info: NormalClassPropertyInfo): boolean { + if (info.classInfo?.annotationInfo?.hasObserved) { + return true; + } + if (!!info.annotationInfo && Object.keys(info.annotationInfo).length > 0) { + return true; + } + return false; + } + + private collectProperty(node: arkts.ClassProperty): void { + const propertyRecord = new NormalClassPropertyRecord({ + classRecord: this._classRecord, + shouldIgnoreDecl: this.shouldIgnoreDecl, + }); + propertyRecord.collect(node); + + const propertyInfo = propertyRecord.toRecord(); + if (!propertyInfo) { + return; + } + if (this.canCollectPropertyFromInfo(propertyInfo)) { + if (propertyInfo.name?.startsWith(BuiltInNames.IMPLEMENT_PROPETY_PREFIX)) { + this._implementedPropertyNames.push(formatBuiltInImplementedPropertyName(propertyInfo.name)); + } + // TODO: collect property info to cache + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.UI).collect(node, propertyRecord.toJSON()); + } + // console.log('[NORMAL CLASS PROPERTY] node: ', node.dumpSrc()); + ValidatorBuilder.build(NormalClassPropertyValidator).checkIsViolated(node, propertyInfo); + } + + private rememberMethod(node: arkts.MethodDefinition, record: NormalClassMethodRecord): void { + if (node.kind === arkts.Es2pandaMethodDefinitionKind.METHOD_DEFINITION_KIND_GET) { + this._getters.push({ node, record }); + } + if (node.kind === arkts.Es2pandaMethodDefinitionKind.METHOD_DEFINITION_KIND_SET) { + this._setters.push({ node, record }); + } + } + + private collectMethod(node: arkts.MethodDefinition): void { + const methodRecord = new NormalClassMethodRecord({ + classRecord: this._classRecord, + shouldIgnoreDecl: this.shouldIgnoreDecl, + }); + methodRecord.collect(node); + + const methodInfo = methodRecord.toRecord(); + if (!methodInfo) { + return; + } + + if (this._implementedPropertyNames.includes(node.name.name)) { + this.rememberMethod(node, methodRecord); + } + // console.log('[NORMAL CLASS METHOD] node: ', node.dumpSrc()); + // console.log('[NORMAL CLASS METHOD] methodInfo: ', methodInfo); + ValidatorBuilder.build(NormalClassMethodValidator).checkIsViolated(node, methodInfo); + } + + private cacheMethod(collection: MethodRecordCollection) { + arkts.NodeCacheFactory.getInstance() + .getCache(NodeCacheNames.UI) + .collect(collection.node, collection.record.toJSON()); + } + + reset(): void { + super.reset(); + this._getters = []; + this._setters = []; + this._implementedPropertyNames = []; + } + + visitor(node: arkts.ClassDeclaration): arkts.ClassDeclaration { + node.definition?.body.forEach((st) => { + if (arkts.isClassProperty(st)) { + this.collectProperty(st); + } else if (arkts.isMethodDefinition(st)) { + this.collectMethod(st); + } + }); + if (this._implementedPropertyNames.length > 0) { + this._getters.forEach((collection) => this.cacheMethod(collection)); + this._setters.forEach((collection) => this.cacheMethod(collection)); + } + return node; + } +} diff --git a/arkui-plugins/collectors/ui-collectors/normal-interface-collector.ts b/arkui-plugins/collectors/ui-collectors/normal-interface-collector.ts new file mode 100644 index 000000000..ee0f00e3c --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/normal-interface-collector.ts @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { AbstractVisitor, VisitorOptions } from '../../common/abstract-visitor'; +import { + NormalInterfacePropertyInfo, + NormalInterfacePropertyRecord, + NormalInterfaceRecord, +} from './records'; +import { NodeCacheNames } from '../../common/predefines'; + +export interface NormalInterfaceCollectorOptions extends VisitorOptions { + interfaceRecord: NormalInterfaceRecord; + shouldIgnoreDecl?: boolean; +} + +export class NormalInterfaceCollector extends AbstractVisitor { + private _interfaceRecord: NormalInterfaceRecord; + public shouldIgnoreDecl: boolean; + + constructor(options: NormalInterfaceCollectorOptions) { + super(options); + this._interfaceRecord = options.interfaceRecord; + this.shouldIgnoreDecl = options.shouldIgnoreDecl ?? false; + } + + private canCollectMethodFromInfo(info: NormalInterfacePropertyInfo): boolean { + if (!!info.annotationInfo && Object.keys(info.annotationInfo).length > 0) { + return true; + } + return false; + } + + private collectMethod(node: arkts.MethodDefinition): void { + const methodRecord = new NormalInterfacePropertyRecord({ + interfaceRecord: this._interfaceRecord, + shouldIgnoreDecl: this.shouldIgnoreDecl, + }); + methodRecord.collect(node); + + const methodInfo = methodRecord.toRecord(); + if (!methodInfo) { + return; + } + if (this.canCollectMethodFromInfo(methodInfo)) { + // TODO: collect method info to cache + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.UI).collect(node, methodRecord.toJSON()); + // console.log("[INTERFACE METHOD] node: ", node.dumpSrc()); + } + } + + visitor(node: arkts.TSInterfaceDeclaration): arkts.TSInterfaceDeclaration { + node.body?.body.forEach((st) => { + if (arkts.isMethodDefinition(st)) { + this.collectMethod(st); + } + }); + return node; + } +} diff --git a/arkui-plugins/collectors/ui-collectors/records/annotations/base.ts b/arkui-plugins/collectors/ui-collectors/records/annotations/base.ts new file mode 100644 index 000000000..8f8303a2c --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/records/annotations/base.ts @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { getAnnotationName } from '../../utils'; +import { BaseRecord, RecordOptions } from '../base'; + +export type Annotations = { + [K in string]?: arkts.AnnotationUsage; +}; + +export type AnnotationInfo = { + [K in string as `has${K}`]?: boolean; +}; + +export type AnnotationRecord = { + annotations?: U; + annotationInfo?: V; + ignoredAnnotations?: Annotations; + ignoredAnnotationInfo?: AnnotationInfo; +}; + +function firstToLower(str: string): string { + return str.charAt(0).toLowerCase() + str.slice(1); +} + +export abstract class BaseAnnotationRecord< + U extends Annotations = Annotations, + V extends AnnotationInfo = AnnotationInfo +> extends BaseRecord> { + protected abstract annotationNames: string[]; + protected _annotations: U = {} as U; + protected _annotationInfo: V = {} as V; + + protected _ignoredAnnotations: Annotations = {}; + protected _ignoredAnnotationInfo: AnnotationInfo = {}; + + constructor(options: RecordOptions) { + super(options); + } + + public get annotations(): U | undefined { + if (Object.keys(this._annotations).length === 0) { + return undefined; + } + return this._annotations; + } + + public get annotationInfo(): V | undefined { + if (Object.keys(this._annotationInfo).length === 0) { + return undefined; + } + return this._annotationInfo; + } + + public get ignoredAnnotations(): Annotations | undefined { + if (Object.keys(this._ignoredAnnotations).length === 0) { + return undefined; + } + return this._ignoredAnnotations; + } + + public get ignoredAnnotationInfo(): AnnotationInfo | undefined { + if (Object.keys(this._ignoredAnnotationInfo).length === 0) { + return undefined; + } + return this._ignoredAnnotationInfo; + } + + private updateAnnotationInfo(name: string | undefined): void { + const newInfo = this.updateAnnotationInfoByName(this._annotationInfo, name); + this._annotationInfo = newInfo; + } + + private updateAnnotations(anno: arkts.AnnotationUsage, name: string | undefined): void { + if (!!name && !!this._annotationInfo[`has${name}`] && !this._annotations[name]) { + this._annotations = { ...this._annotations, [name]: anno }; + } + } + + private updateIgnoreAnnotationInfo(name: string | undefined): void { + if (!!name && !this.annotationNames.includes(name)) { + this._ignoredAnnotationInfo[`has${name}`] = true; + } + } + + private updateIgnoreAnnotations(anno: arkts.AnnotationUsage, name: string | undefined): void { + if (!!name && !!this._ignoredAnnotationInfo[`has${name}`] && !this._ignoredAnnotations[name]) { + this._ignoredAnnotations = { ...this._ignoredAnnotations, [name]: anno }; + } + } + + collectFromNode(node: arkts.AnnotationUsage): void { + const name = getAnnotationName(node, this.shouldIgnoreDecl); + this.updateAnnotationInfo(name); + this.updateAnnotations(node, name); + this.updateIgnoreAnnotationInfo(name); + this.updateIgnoreAnnotations(node, name); + } + + refreshOnce(): void { + const currInfo: AnnotationRecord = { + ...this.info, + ...(this.annotations && { annotations: this.annotations }), + ...(this.annotationInfo && { annotationInfo: this.annotationInfo }), + ...(this.ignoredAnnotations && { ignoredAnnotations: this.ignoredAnnotations }), + ...(this.ignoredAnnotationInfo && { ignoredAnnotationInfo: this.ignoredAnnotationInfo }), + }; + this.info = currInfo; + } + + toJSON(): AnnotationRecord { + this.refresh(); + return { + ...(this.info?.annotationInfo && { annotationInfo: this.info.annotationInfo }), + }; + } + + abstract updateAnnotationInfoByName(info: V, name: string | undefined): V; +} diff --git a/arkui-plugins/collectors/ui-collectors/records/annotations/call-declaration.ts b/arkui-plugins/collectors/ui-collectors/records/annotations/call-declaration.ts new file mode 100644 index 000000000..7c7dd162f --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/records/annotations/call-declaration.ts @@ -0,0 +1,77 @@ +/* + * 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 { FunctionAnnotationInfo, FunctionAnnotations } from './function'; +import { StructPropertyAnnotationInfo, StructPropertyAnnotations } from './struct-property'; +import { StructMethodAnnotationInfo, StructMethodAnnotations } from './struct-method'; +import { NormalClassPropertyAnnotationInfo, NormalClassPropertyAnnotations } from './normal-class-property'; +import { NormalClassMethodAnnotationInfo, NormalClassMethodAnnotations } from './normal-class-method'; +import { BaseAnnotationRecord } from './base'; +import { RecordOptions } from '../base'; +import { BuilderLambdaNames, DecoratorNames } from '../../../../common/predefines'; + +export type CallDeclAnnotationInfo = FunctionAnnotationInfo & + StructPropertyAnnotationInfo & + StructMethodAnnotationInfo & + NormalClassPropertyAnnotationInfo & + NormalClassMethodAnnotationInfo; + +export type CallDeclAnnotations = FunctionAnnotations & + StructPropertyAnnotations & + StructMethodAnnotations & + NormalClassPropertyAnnotations & + NormalClassMethodAnnotations; + +export class CallDeclAnnotationRecord extends BaseAnnotationRecord { + protected annotationNames: string[]; + + constructor(options: RecordOptions) { + super(options); + this.annotationNames = [ + DecoratorNames.BUILDER, + DecoratorNames.ANIMATABLE_EXTEND, + BuilderLambdaNames.ANNOTATION_NAME, + DecoratorNames.STATE, + DecoratorNames.STORAGE_LINK, + DecoratorNames.STORAGE_PROP, + DecoratorNames.LINK, + DecoratorNames.PROP, + DecoratorNames.PROVIDE, + DecoratorNames.CONSUME, + DecoratorNames.OBJECT_LINK, + DecoratorNames.WATCH, + DecoratorNames.BUILDER_PARAM, + DecoratorNames.LOCAL_STORAGE_PROP, + DecoratorNames.LOCAL_STORAGE_LINK, + DecoratorNames.PROP_REF, + DecoratorNames.STORAGE_PROP_REF, + DecoratorNames.LOCAL, + DecoratorNames.PARAM, + DecoratorNames.EVENT, + DecoratorNames.REQUIRE, + DecoratorNames.COMPUTED, + DecoratorNames.JSONSTRINGIFYIGNORE, + DecoratorNames.JSONRENAME, + DecoratorNames.TRACK, + ]; + } + + updateAnnotationInfoByName(info: CallDeclAnnotationInfo, name: string | undefined): CallDeclAnnotationInfo { + if (!!name && this.annotationNames.includes(name)) { + info[`has${name}`] = true; + } + return info; + } +} diff --git a/arkui-plugins/collectors/ui-collectors/records/annotations/custom-component.ts b/arkui-plugins/collectors/ui-collectors/records/annotations/custom-component.ts new file mode 100644 index 000000000..d727da321 --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/records/annotations/custom-component.ts @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { AnnotationInfo, Annotations, BaseAnnotationRecord } from './base'; +import { StructDecoratorNames } from '../../../../common/predefines'; +import { RecordOptions } from '../base'; + +export interface StructAnnotationInfo extends AnnotationInfo { + hasComponent?: boolean; + hasComponentV2?: boolean; + hasEntry?: boolean; + hasReusable?: boolean; + hasReusableV2?: boolean; + hasCustomLayout?: boolean; + hasCustomDialog?: boolean; +} + +export interface CustomComponentAnnotations extends Annotations { + [StructDecoratorNames.COMPONENT]?: arkts.AnnotationUsage; + [StructDecoratorNames.COMPONENT_V2]?: arkts.AnnotationUsage; + [StructDecoratorNames.ENTRY]?: arkts.AnnotationUsage; + [StructDecoratorNames.RESUABLE]?: arkts.AnnotationUsage; + [StructDecoratorNames.RESUABLE_V2]?: arkts.AnnotationUsage; + [StructDecoratorNames.CUSTOM_LAYOUT]?: arkts.AnnotationUsage; + [StructDecoratorNames.CUSTOMDIALOG]?: arkts.AnnotationUsage; +} + +export class CustomComponentAnnotationRecord extends BaseAnnotationRecord< + CustomComponentAnnotations, + StructAnnotationInfo +> { + protected annotationNames: string[]; + + constructor(options: RecordOptions) { + super(options); + this.shouldIgnoreDecl = true; // TODO: remove this line + this.annotationNames = Object.values(StructDecoratorNames); + } + + updateAnnotationInfoByName(info: StructAnnotationInfo, name: string | undefined): StructAnnotationInfo { + switch (name) { + case StructDecoratorNames.COMPONENT: + info.hasComponent = true; + break; + case StructDecoratorNames.COMPONENT_V2: + info.hasComponentV2 = true; + break; + case StructDecoratorNames.ENTRY: + info.hasEntry = true; + break; + case StructDecoratorNames.RESUABLE: + info.hasReusable = true; + break; + case StructDecoratorNames.RESUABLE_V2: + info.hasReusableV2 = true; + break; + case StructDecoratorNames.CUSTOM_LAYOUT: + info.hasCustomLayout = true; + break; + case StructDecoratorNames.CUSTOMDIALOG: + info.hasCustomDialog = true; + break; + default: + return info; + } + return info; + } +} diff --git a/arkui-plugins/collectors/ui-collectors/records/annotations/function.ts b/arkui-plugins/collectors/ui-collectors/records/annotations/function.ts new file mode 100644 index 000000000..f0c96ad81 --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/records/annotations/function.ts @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { AnnotationInfo, Annotations, BaseAnnotationRecord } from './base'; +import { BuilderLambdaNames, DecoratorNames } from '../../../../common/predefines'; +import { RecordOptions } from '../base'; + +export interface FunctionAnnotationInfo extends AnnotationInfo { + hasBuilder?: boolean; + hasAnimatableExtend?: boolean; + hasComponentBuilder?: boolean; +} + +export interface FunctionAnnotations extends Annotations { + [DecoratorNames.BUILDER]?: arkts.AnnotationUsage; + [DecoratorNames.ANIMATABLE_EXTEND]?: arkts.AnnotationUsage; + [BuilderLambdaNames.ANNOTATION_NAME]?: arkts.AnnotationUsage; +} + +export class FunctionAnnotationRecord extends BaseAnnotationRecord { + protected annotationNames: string[]; + + constructor(options: RecordOptions) { + super(options); + this.shouldIgnoreDecl = true; // TODO: remove this line + this.annotationNames = [ + DecoratorNames.BUILDER, + DecoratorNames.ANIMATABLE_EXTEND, + BuilderLambdaNames.ANNOTATION_NAME, + ]; + } + + updateAnnotationInfoByName(info: FunctionAnnotationInfo, name: string | undefined): FunctionAnnotationInfo { + switch (name) { + case DecoratorNames.BUILDER: + info.hasBuilder = true; + break; + case DecoratorNames.ANIMATABLE_EXTEND: + info.hasAnimatableExtend = true; + break; + case BuilderLambdaNames.ANNOTATION_NAME: + info.hasComponentBuilder = true; + break; + default: + return info; + } + return info; + } +} diff --git a/arkui-plugins/collectors/ui-collectors/records/annotations/index.ts b/arkui-plugins/collectors/ui-collectors/records/annotations/index.ts new file mode 100644 index 000000000..5600cd48e --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/records/annotations/index.ts @@ -0,0 +1,24 @@ +/* + * 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 * from './custom-component'; +export * from './normal-class'; +export * from './normal-class-property'; +export * from './normal-class-method'; +export * from './function'; +export * from './struct-property'; +export * from './struct-method'; +export * from './normal-interface-property'; +export * from './call-declaration'; \ No newline at end of file diff --git a/arkui-plugins/collectors/ui-collectors/records/annotations/normal-class-method.ts b/arkui-plugins/collectors/ui-collectors/records/annotations/normal-class-method.ts new file mode 100644 index 000000000..b26e9e307 --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/records/annotations/normal-class-method.ts @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { AnnotationInfo, Annotations, BaseAnnotationRecord } from './base'; +import { DecoratorNames } from '../../../../common/predefines'; +import { RecordOptions } from '../base'; + +export interface NormalClassMethodAnnotationInfo extends AnnotationInfo { + hasComputed?: boolean; +} + +export interface NormalClassMethodAnnotations extends Annotations { + [DecoratorNames.COMPUTED]?: arkts.AnnotationUsage; +} + +export class NormalClassMethodAnnotationRecord extends BaseAnnotationRecord< + NormalClassMethodAnnotations, + NormalClassMethodAnnotationInfo +> { + protected annotationNames: string[]; + + constructor(options: RecordOptions) { + super(options); + this.shouldIgnoreDecl = true; // TODO: remove this line + this.annotationNames = [DecoratorNames.COMPUTED]; + } + + updateAnnotationInfoByName(info: NormalClassMethodAnnotationInfo, name: string | undefined): NormalClassMethodAnnotationInfo { + switch (name) { + case DecoratorNames.COMPUTED: + info.hasComputed = true; + break; + default: + return info; + } + return info; + } +} diff --git a/arkui-plugins/collectors/ui-collectors/records/annotations/normal-class-property.ts b/arkui-plugins/collectors/ui-collectors/records/annotations/normal-class-property.ts new file mode 100644 index 000000000..67a00e3e9 --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/records/annotations/normal-class-property.ts @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { AnnotationInfo, Annotations, BaseAnnotationRecord } from './base'; +import { DecoratorNames } from '../../../../common/predefines'; +import { RecordOptions } from '../base'; + +export interface NormalClassPropertyAnnotationInfo extends AnnotationInfo { + hasJsonStringifyIgnore?: boolean; + hasJsonRename?: boolean; + hasTrack?: boolean; +} + +export interface NormalClassPropertyAnnotations extends Annotations { + [DecoratorNames.JSONSTRINGIFYIGNORE]?: arkts.AnnotationUsage; + [DecoratorNames.JSONRENAME]?: arkts.AnnotationUsage; + [DecoratorNames.TRACK]?: arkts.AnnotationUsage; +} + +export class NormalClassPropertyAnnotationRecord extends BaseAnnotationRecord< + NormalClassPropertyAnnotations, + NormalClassPropertyAnnotationInfo +> { + protected annotationNames: string[]; + + constructor(options: RecordOptions) { + super(options); + this.shouldIgnoreDecl = true; // TODO: remove this line + this.annotationNames = [DecoratorNames.JSONSTRINGIFYIGNORE, DecoratorNames.JSONRENAME, DecoratorNames.TRACK]; + } + + updateAnnotationInfoByName( + info: NormalClassPropertyAnnotationInfo, + name: string | undefined + ): NormalClassPropertyAnnotationInfo { + switch (name) { + case DecoratorNames.JSONSTRINGIFYIGNORE: + info.hasJsonStringifyIgnore = true; + break; + case DecoratorNames.JSONRENAME: + info.hasJsonRename = true; + break; + case DecoratorNames.TRACK: + info.hasTrack = true; + break; + default: + return info; + } + return info; + } +} diff --git a/arkui-plugins/collectors/ui-collectors/records/annotations/normal-class.ts b/arkui-plugins/collectors/ui-collectors/records/annotations/normal-class.ts new file mode 100644 index 000000000..9f2e1587f --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/records/annotations/normal-class.ts @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { AnnotationInfo, Annotations, BaseAnnotationRecord } from './base'; +import { DecoratorNames } from '../../../../common/predefines'; +import { RecordOptions } from '../base'; + +export interface NormalClassAnnotationInfo extends AnnotationInfo { + hasObserved?: boolean; + hasObservedV2?: boolean; +} + +export interface NormalClassAnnotations extends Annotations { + [DecoratorNames.OBSERVED]?: arkts.AnnotationUsage; + [DecoratorNames.OBSERVED_V2]?: arkts.AnnotationUsage; +} + +export class NormalClassAnnotationRecord extends BaseAnnotationRecord< + NormalClassAnnotations, + NormalClassAnnotationInfo +> { + protected annotationNames: string[]; + + constructor(options: RecordOptions) { + super(options); + this.shouldIgnoreDecl = true; // TODO: remove this line + this.annotationNames = [DecoratorNames.OBSERVED, DecoratorNames.OBSERVED_V2]; + } + + updateAnnotationInfoByName(info: NormalClassAnnotationInfo, name: string | undefined): NormalClassAnnotationInfo { + switch (name) { + case DecoratorNames.OBSERVED: + info.hasObserved = true; + break; + case DecoratorNames.OBSERVED_V2: + info.hasObservedV2 = true; + break; + default: + return info; + } + return info; + } +} diff --git a/arkui-plugins/collectors/ui-collectors/records/annotations/normal-interface-property.ts b/arkui-plugins/collectors/ui-collectors/records/annotations/normal-interface-property.ts new file mode 100644 index 000000000..26848161d --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/records/annotations/normal-interface-property.ts @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { AnnotationInfo, Annotations, BaseAnnotationRecord } from './base'; +import { DecoratorNames } from '../../../../common/predefines'; +import { RecordOptions } from '../base'; + +export interface NormalInterfacePropertyAnnotationInfo extends AnnotationInfo { + hasBuilder?: boolean; +} + +export interface NormalInterfacePropertyAnnotations extends Annotations { + [DecoratorNames.BUILDER]?: arkts.AnnotationUsage; +} + +export class NormalInterfacePropertyAnnotationRecord extends BaseAnnotationRecord< + NormalInterfacePropertyAnnotations, + NormalInterfacePropertyAnnotationInfo +> { + protected annotationNames: string[]; + + constructor(options: RecordOptions) { + super(options); + this.shouldIgnoreDecl = true; // TODO: remove this line + this.annotationNames = [DecoratorNames.BUILDER]; + } + + updateAnnotationInfoByName( + info: NormalInterfacePropertyAnnotationInfo, + name: string | undefined + ): NormalInterfacePropertyAnnotationInfo { + switch (name) { + case DecoratorNames.BUILDER: + info.hasBuilder = true; + break; + default: + return info; + } + return info; + } +} diff --git a/arkui-plugins/collectors/ui-collectors/records/annotations/struct-method.ts b/arkui-plugins/collectors/ui-collectors/records/annotations/struct-method.ts new file mode 100644 index 000000000..1a4b01330 --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/records/annotations/struct-method.ts @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { AnnotationInfo, Annotations, BaseAnnotationRecord } from './base'; +import { DecoratorNames } from '../../../../common/predefines'; +import { RecordOptions } from '../base'; + +export interface StructMethodAnnotationInfo extends AnnotationInfo { + hasBuilder?: boolean; + hasComputed?: boolean; +} + +export interface StructMethodAnnotations extends Annotations { + [DecoratorNames.BUILDER]?: arkts.AnnotationUsage; + [DecoratorNames.COMPUTED]?: arkts.AnnotationUsage; +} + +export class StructMethodAnnotationRecord extends BaseAnnotationRecord< + StructMethodAnnotations, + StructMethodAnnotationInfo +> { + protected annotationNames: string[]; + + constructor(options: RecordOptions) { + super(options); + this.annotationNames = [DecoratorNames.BUILDER, DecoratorNames.COMPUTED]; + } + + updateAnnotationInfoByName(info: StructMethodAnnotationInfo, name: string | undefined): StructMethodAnnotationInfo { + switch (name) { + case DecoratorNames.BUILDER: + info.hasBuilder = true; + break; + case DecoratorNames.COMPUTED: + info.hasComputed = true; + break; + default: + return info; + } + return info; + } +} diff --git a/arkui-plugins/collectors/ui-collectors/records/annotations/struct-property.ts b/arkui-plugins/collectors/ui-collectors/records/annotations/struct-property.ts new file mode 100644 index 000000000..1a58bcfd6 --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/records/annotations/struct-property.ts @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { AnnotationInfo, Annotations, BaseAnnotationRecord } from './base'; +import { DecoratorNames } from '../../../../common/predefines'; +import { RecordOptions } from '../base'; + +export interface StructPropertyAnnotationInfo extends AnnotationInfo { + hasState?: boolean; + hasStorageLink?: boolean; + hasStorageProp?: boolean; + hasLink?: boolean; + hasProp?: boolean; + hasProvide?: boolean; + hasConsume?: boolean; + hasObjectLink?: boolean; + hasWatch?: boolean; + hasBuilderParam?: boolean; + hasLocalStorageProp?: boolean; + hasLocalStorageLink?: boolean; + hasPropRef?: boolean; + hasStoragePropRef?: boolean; + hasLocal?: boolean; + hasParam?: boolean; + hasEvent?: boolean; + hasRequire?: boolean; +} + +export interface StructPropertyAnnotations extends Annotations { + [DecoratorNames.STATE]?: arkts.AnnotationUsage; + [DecoratorNames.STORAGE_LINK]?: arkts.AnnotationUsage; + [DecoratorNames.STORAGE_PROP]?: arkts.AnnotationUsage; + [DecoratorNames.LINK]?: arkts.AnnotationUsage; + [DecoratorNames.PROP]?: arkts.AnnotationUsage; + [DecoratorNames.PROVIDE]?: arkts.AnnotationUsage; + [DecoratorNames.CONSUME]?: arkts.AnnotationUsage; + [DecoratorNames.OBJECT_LINK]?: arkts.AnnotationUsage; + [DecoratorNames.WATCH]?: arkts.AnnotationUsage; + [DecoratorNames.BUILDER_PARAM]?: arkts.AnnotationUsage; + [DecoratorNames.LOCAL_STORAGE_PROP]?: arkts.AnnotationUsage; + [DecoratorNames.LOCAL_STORAGE_LINK]?: arkts.AnnotationUsage; + [DecoratorNames.PROP_REF]?: arkts.AnnotationUsage; + [DecoratorNames.STORAGE_PROP_REF]?: arkts.AnnotationUsage; + [DecoratorNames.LOCAL]?: arkts.AnnotationUsage; + [DecoratorNames.PARAM]?: arkts.AnnotationUsage; + [DecoratorNames.EVENT]?: arkts.AnnotationUsage; + [DecoratorNames.REQUIRE]?: arkts.AnnotationUsage; +} + +export class StructPropertyAnnotationRecord extends BaseAnnotationRecord< + StructPropertyAnnotations, + StructPropertyAnnotationInfo +> { + protected annotationNames: string[]; + + constructor(options: RecordOptions) { + super(options); + this.shouldIgnoreDecl = true; // TODO: remove this line + this.annotationNames = [ + DecoratorNames.STATE, + DecoratorNames.STORAGE_LINK, + DecoratorNames.STORAGE_PROP, + DecoratorNames.LINK, + DecoratorNames.PROP, + DecoratorNames.PROVIDE, + DecoratorNames.CONSUME, + DecoratorNames.OBJECT_LINK, + DecoratorNames.WATCH, + DecoratorNames.BUILDER_PARAM, + DecoratorNames.LOCAL_STORAGE_PROP, + DecoratorNames.LOCAL_STORAGE_LINK, + DecoratorNames.PROP_REF, + DecoratorNames.STORAGE_PROP_REF, + DecoratorNames.LOCAL, + DecoratorNames.PARAM, + DecoratorNames.EVENT, + DecoratorNames.REQUIRE, + ]; + } + + updateAnnotationInfoByName( + info: StructPropertyAnnotationInfo, + name: string | undefined + ): StructPropertyAnnotationInfo { + if (!!name && this.annotationNames.includes(name)) { + info[`has${name}`] = true; + } + return info; + } +} diff --git a/arkui-plugins/collectors/ui-collectors/records/base.ts b/arkui-plugins/collectors/ui-collectors/records/base.ts new file mode 100644 index 000000000..fff4152d4 --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/records/base.ts @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; + +export interface RecordOptions { + shouldIgnoreDecl: boolean; +} + +export abstract class BaseRecord> { + protected info: T | undefined; + protected shouldIgnoreDecl: boolean; + + private _isChanged: boolean = false; + private _isCollected: boolean = false; + + constructor(options: RecordOptions) { + this.shouldIgnoreDecl = options.shouldIgnoreDecl; + } + + protected get isChanged(): boolean { + return this._isChanged; + } + + protected set isChanged(isChanged: boolean) { + this._isChanged = isChanged; + } + + get isCollected(): boolean { + return this._isCollected; + } + + getOptions(): RecordOptions { + return { + shouldIgnoreDecl: this.shouldIgnoreDecl + } + } + + toRecord(): T | undefined { + this.refresh(); + return this.info; + } + + collect(node: Node): void { + this.collectFromNode(node); + this.isChanged = true; + this._isCollected = true; + } + + refresh(): void { + if (!this.isChanged) { + return; + } + this.refreshOnce(); + this.isChanged = false; + } + + protected abstract collectFromNode(node: Node): void; + + protected abstract refreshOnce(): void; + + abstract toJSON(): T; +} \ No newline at end of file diff --git a/arkui-plugins/collectors/ui-collectors/records/cache.ts b/arkui-plugins/collectors/ui-collectors/records/cache.ts new file mode 100644 index 000000000..2ea11e0b3 --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/records/cache.ts @@ -0,0 +1,167 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { BaseRecord } from './base'; +import { AstNodePointer } from 'common/safe-types'; + +/** + * Singleton LRU Cache implementation using Map's insertion order + * for efficient least-recently-used eviction. + */ +export class RecordCache>> { + private static instance: RecordCache>>; + private cache: Map; + private maxSize: number; + + private constructor(maxSize: number = 100) { + if (maxSize <= 0) { + throw new Error('Cache size must be positive'); + } + this.cache = new Map(); + this.maxSize = maxSize; + } + + /** + * Get the singleton instance of the cache + * @param maxSize Maximum number of items to store (default: 100) + * @returns The cache instance + */ + public static getInstance>>( + maxSize: number = 100 + ): RecordCache { + if (!this.instance) { + this.instance = new RecordCache(maxSize); + } else if (maxSize !== RecordCache.instance.maxSize) { + this.instance.resize(maxSize); + } + return this.instance as RecordCache; + } + + /** + * Get a value from the cache + * @param key Cache key + * @returns The cached value or undefined if not found + */ + public get(key: AstNodePointer): T | undefined { + const value = this.cache.get(key); + if (value !== undefined) { + // Refresh key by deleting and re-adding it + this.cache.delete(key); + this.cache.set(key, value); + } + return value; + } + + /** + * Set a value in the cache + * @param key Cache key + * @param value Value to cache + */ + public set(key: AstNodePointer, value: T): void { + if (this.cache.has(key)) { + // Refresh key by deleting it first + this.cache.delete(key); + } else if (this.cache.size >= this.maxSize) { + // Evict the first item (least recently used) + const firstKey = this.cache.keys().next().value; + if (firstKey !== undefined) { + this.cache.delete(firstKey); + } + } + this.cache.set(key, value); + } + + /** + * Check if a key exists in the cache + * @param key Cache key to check + * @returns True if the key exists + */ + public has(key: AstNodePointer): boolean { + return this.cache.has(key); + } + + /** + * Delete a key from the cache + * @param key Cache key to delete + * @returns True if the key was deleted + */ + public delete(key: AstNodePointer): boolean { + return this.cache.delete(key); + } + + /** + * Clear all items from the cache + */ + public clear(): void { + this.cache.clear(); + } + + /** + * Get the current number of items in the cache + * @returns Number of cached items + */ + public size(): number { + return this.cache.size; + } + + /** + * Get all cache keys (in order of most recently used) + * @returns Array of keys + */ + public keys(): string[] { + return Array.from(this.cache.keys()).reverse(); + } + + /** + * Get all cache values (in order of most recently used) + * @returns Array of values + */ + public values(): T[] { + return Array.from(this.cache.values()).reverse(); + } + + /** + * Get all cache entries (in order of most recently used) + * @returns Array of [key, value] pairs + */ + public entries(): [string, T][] { + return Array.from(this.cache.entries()).reverse(); + } + + /** + * Resize the cache (evicts LRU items if new size is smaller) + * @param newSize New maximum cache size + */ + public resize(newSize: number): void { + if (newSize <= 0) { + throw new Error('Cache size must be positive'); + } + + this.maxSize = newSize; + while (this.cache.size > this.maxSize) { + const firstKey = this.cache.keys().next().value!; + this.cache.delete(firstKey); + } + } + + /** + * Execute a function for each cache entry (from most to least recently used) + * @param callback Function to execute + */ + public forEach(callback: (value: T, key: AstNodePointer) => void): void { + this.entries().forEach(([key, value]) => callback(value, key)); + } +} diff --git a/arkui-plugins/collectors/ui-collectors/records/call-declaration.ts b/arkui-plugins/collectors/ui-collectors/records/call-declaration.ts new file mode 100644 index 000000000..568e655c2 --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/records/call-declaration.ts @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { CallDeclAnnotationInfo, CallDeclAnnotationRecord, CallDeclAnnotations } from './annotations'; +import { AnnotationRecord } from './annotations/base'; +import { BaseRecord, RecordOptions } from './base'; +import { BuiltInNames } from '../../../common/predefines'; + +export type CallDeclInfo = AnnotationRecord & { + /** + * declaration node's name. + */ + declName?: string; + + /** + * declaration node's modifier flags. + */ + modifiers?: arkts.Es2pandaModifierFlags; + + /** + * the module name where the declaration node is from. + */ + moduleName?: string; + + /** + * whether the call has function with receiver. + */ + hasReceiver?: boolean; + + /** + * whether the declaration node is a class property. + */ + isDeclFromClassProperty?: boolean; + + /** + * whether the declaration node is a class method. + */ + isDeclFromMethod?: boolean; + + /** + * whether declaration node is a global function. + */ + isDeclFromFunction?: boolean; +}; + +export class CallDeclRecord extends BaseRecord { + private _annotationRecord: CallDeclAnnotationRecord; + + protected declName?: string; + protected modifiers?: arkts.Es2pandaModifierFlags; + protected moduleName?: string; + protected hasReceiver?: boolean; + protected isDeclFromClassProperty?: boolean; + protected isDeclFromMethod?: boolean; + protected isDeclFromFunction?: boolean; + + constructor(options: RecordOptions) { + super(options); + this._annotationRecord = new CallDeclAnnotationRecord(options); + } + + private collectAnnotations(annotations: readonly arkts.AnnotationUsage[]): void { + for (const anno of annotations) { + this._annotationRecord.collect(anno); + } + } + + private collectFromClassProperty(node: arkts.ClassProperty): void { + if (!node.key || !arkts.isIdentifier(node.key)) { + return; + } + this.collectAnnotations(node.annotations); + this.declName = node.key.name; + this.modifiers = node.modifiers; + this.isDeclFromClassProperty = true; + } + + private collectFromMethod(node: arkts.MethodDefinition): void { + this.collectAnnotations(node.scriptFunction.annotations); + this.declName = node.name.name; + this.modifiers = node.modifiers; + this.hasReceiver = node.scriptFunction.hasReceiver; + if ( + !!node.parent && + arkts.isMethodDefinition(node.parent) && + node.parent.name.name === BuiltInNames.ETS_GLOBAL_CLASS + ) { + this.isDeclFromFunction = true; + } else { + this.isDeclFromMethod = true; + } + } + + protected collectFromNode(node: arkts.AstNode): void { + this.moduleName = arkts.getProgramFromAstNode(node)?.moduleName; + if (arkts.isClassProperty(node)) { + this.collectFromClassProperty(node); + } else if (arkts.isMethodDefinition(node)) { + this.collectFromMethod(node); + } + } + + protected refreshOnce(): void { + let currInfo = this.info ?? {}; + const annotationRecord = this._annotationRecord.toRecord(); + currInfo = { + ...currInfo, + ...(this.declName && { declName: this.declName }), + ...(this.modifiers && { modifiers: this.modifiers }), + ...(annotationRecord && { ...annotationRecord }), + }; + this.info = currInfo; + } + + toJSON(): CallDeclInfo { + this.refresh(); + return { + ...(this.info?.declName && { declName: this.info.declName }), + ...(this.info?.modifiers && { modifiers: this.info.modifiers }), + ...(this.info?.moduleName && { moduleName: this.info.moduleName }), + ...(this.info?.isDeclFromClassProperty && { isDeclFromClassProperty: this.info.isDeclFromClassProperty }), + ...(this.info?.isDeclFromMethod && { isDeclFromMethod: this.info.isDeclFromMethod }), + ...(this.info?.isDeclFromFunction && { isDeclFromFunction: this.info.isDeclFromFunction }), + ...(this.info?.annotationInfo && { annotationInfo: this.info.annotationInfo }), + }; + } +} diff --git a/arkui-plugins/collectors/ui-collectors/records/function-call.ts b/arkui-plugins/collectors/ui-collectors/records/function-call.ts new file mode 100644 index 000000000..c9737b1a8 --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/records/function-call.ts @@ -0,0 +1,324 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { BaseRecord, RecordOptions } from './base'; +import { CallDeclInfo, CallDeclRecord } from './call-declaration'; +import { CustomComponentInfo, CustomComponentRecord } from './struct'; +import { RecordBuilder } from './record-builder'; +import { + checkIsCallNameFromBindable, + checkIsCallNameFromResource, + findRootCallee, + findRootCallObject, + getDeclFromArkUI, + getStructFromCall, +} from '../utils'; +import { AstNodePointer } from '../../../common/safe-types'; +import { ARKUI_IMPORT_PREFIX_NAMES, Dollars } from '../../../common/predefines'; +import { matchPrefix } from '../../../common/arkts-utils'; +import { DeclarationCollector } from '../../../common/declaration-collector'; + +export type CallInfo = CallDeclInfo & { + /** + * this call node pointer + */ + ptr?: AstNodePointer; + + /** + * this call name (i.e. callee must be an Identifier), e.g calls like `a[0]()` has no call name + */ + callName?: string; + + /** + * a list of call names start from the root call to this call (both inclusive) + */ + chainingCallNames?: string[]; + + /** + * whether this call is from current class's method or property + */ + isThis?: boolean; + + /** + * whether this call has trailing lambda argument + */ + isTrailingCall?: boolean; + + /** + * whether this call is `$$()` bindable call + */ + isBindableCall?: Dollars.DOLLAR_DOLLAR; + + /** + * whether bthis call is `$r()` or `$rawfile()` resource call + */ + isResourceCall?: Dollars.DOLLAR_RESOURCE | Dollars.DOLLAR_RAWFILE; + + /** + * call information from this call's root (e.g. `A.b().c()` has root call `b()`), call is root call if not exist + */ + rootCallInfo?: CallInfo; + + /** + * struct information from this call's object, call is not a struct call if not exist + */ + structDeclInfo?: CustomComponentInfo; + + /** + * struct information which contains this call, call is not in a struct if not exist + */ + fromStructInfo?: CustomComponentInfo; +}; + +export class CallRecord extends BaseRecord { + private _declRecord: CallDeclRecord; + private _structDeclRecord?: CustomComponentRecord; + private _fromStructRecord?: CustomComponentRecord; + + protected callName?: string; + protected ptr?: AstNodePointer; + protected isThis?: boolean; + protected isTrailingCall?: boolean; + protected isBindableCall?: Dollars.DOLLAR_DOLLAR; + protected isResourceCall?: Dollars.DOLLAR_RESOURCE | Dollars.DOLLAR_RAWFILE; + protected declInfo?: CallDeclInfo; + protected structDeclInfo?: CustomComponentInfo; + protected fromStructInfo?: CustomComponentInfo; + + private _rootCallObject?: arkts.Identifier | undefined; + private _rootCallee?: arkts.Identifier | undefined; + private _rootCallInfo?: CallInfo; + private _chainingCallNames?: string[]; + + constructor(options: RecordOptions) { + super(options); + this._declRecord = new CallDeclRecord(options); + } + + protected get rootCallInfo(): CallInfo | undefined { + return this._rootCallInfo; + } + + protected set rootCallInfo(rootCallInfo: CallInfo | undefined) { + if (this._rootCallInfo?.ptr !== rootCallInfo?.ptr) { + this._rootCallInfo = rootCallInfo; + this.isChanged = true; + } + } + + protected get chainingCallNames(): string[] | undefined { + return this._chainingCallNames; + } + + protected set chainingCallNames(chainingCallNames: string[] | undefined) { + this._chainingCallNames = chainingCallNames; + this.isChanged = true; + } + + private checkIsThisFromCallee(callee: arkts.Identifier | undefined): boolean { + if (!callee) { + return false; + } + if (!callee.parent || !arkts.isMemberExpression(callee.parent)) { + return false; + } + return callee.parent.object && arkts.isThisExpression(callee.parent.object); + } + + private collectFromDecl(decl: arkts.AstNode | undefined): void { + if (!decl) { + return; + } + this._declRecord.collect(decl); + } + + private collectStructDeclInfo(structNode: arkts.ClassDefinition | undefined): void { + if (!structNode || !structNode.parent || !arkts.isClassDeclaration(structNode.parent)) { + return; + } + const _record = RecordBuilder.build(CustomComponentRecord, structNode.parent, this.getOptions()); + this._structDeclRecord = _record; + if (!this._structDeclRecord.isCollected) { + this._structDeclRecord.collect(structNode.parent); + } + } + + private collectFromStructInfo(structNode: arkts.ClassDefinition | undefined): void { + if (!structNode || !structNode.parent || !arkts.isClassDeclaration(structNode.parent)) { + return; + } + const _record = RecordBuilder.build(CustomComponentRecord, structNode.parent, this.getOptions()); + this._fromStructRecord = _record; + if (!this._fromStructRecord.isCollected) { + this._fromStructRecord.collect(structNode.parent); + } + } + + private findStructDeclInfo(): void { + if (!!this.rootCallInfo?.structDeclInfo) { + this.structDeclInfo = this.rootCallInfo.structDeclInfo; + } else if (!this.structDeclInfo) { + const structNode = getStructFromCall(this._rootCallObject, this._rootCallee); + this.collectStructDeclInfo(structNode); + } + } + + private findFromStructInfo(): void { + if (!!this.rootCallInfo?.fromStructInfo) { + this.fromStructInfo = this.rootCallInfo.fromStructInfo; + } else if (!this.fromStructInfo) { + const structNode = this._rootCallee?.findOuterParent( + arkts.Es2pandaAstNodeType.AST_NODE_TYPE_CLASS_DEFINITION + ); + this.collectFromStructInfo(structNode); + } + } + + private findResourceCall(decl: arkts.AstNode, declInfo: CallDeclInfo): void { + const name = declInfo.declName; + const moduleName = declInfo.moduleName; + if (!this.shouldIgnoreDecl && (!moduleName || !matchPrefix(ARKUI_IMPORT_PREFIX_NAMES, moduleName))) { + return; + } + if (!name || !checkIsCallNameFromResource(name)) { + return; + } + this.isResourceCall = name; + DeclarationCollector.getInstance().collect(decl); + } + + private findBinableCall(decl: arkts.AstNode, declInfo: CallDeclInfo): void { + const name = declInfo.declName; + const moduleName = declInfo.moduleName; + if (!this.shouldIgnoreDecl && (!moduleName || !matchPrefix(ARKUI_IMPORT_PREFIX_NAMES, moduleName))) { + return; + } + if (!name || !checkIsCallNameFromBindable(name)) { + return; + } + this.isBindableCall = name; + DeclarationCollector.getInstance().collect(decl); + } + + private findIsSpecialCall(decl?: arkts.AstNode): void { + const declInfo = this._declRecord.toRecord(); + if (!decl || !declInfo || !declInfo.declName) { + return; + } + this.findBinableCall(decl, declInfo); + this.findResourceCall(decl, declInfo); + } + + withRootCallee(rootCallee: arkts.Identifier | undefined): this { + this._rootCallee = rootCallee; + this.findStructDeclInfo(); + this.findFromStructInfo(); + return this; + } + + withRootCallObject(rootCallObject: arkts.Identifier | undefined): this { + this._rootCallObject = rootCallObject; + this.findStructDeclInfo(); + return this; + } + + withRootCallInfo(rootCallInfo: CallInfo): this { + this.rootCallInfo = rootCallInfo; + this.findStructDeclInfo(); + this.findFromStructInfo(); + return this; + } + + withChainingCallNames(chainingCallNames: string[]): this { + this.chainingCallNames = chainingCallNames; + return this; + } + + collectFromNode(node: arkts.CallExpression): void { + this.ptr = node.peer; + + const callee = node.expression; + this._rootCallObject = this._rootCallObject ?? findRootCallObject(callee); + this._rootCallee = this._rootCallee ?? findRootCallee(callee); + this.callName = this._rootCallee?.name; + this.isThis = this.checkIsThisFromCallee(this._rootCallee); + this.isTrailingCall = node.isTrailingCall; + this.findStructDeclInfo(); + this.findFromStructInfo(); + + const decl = arkts.getPeerDecl(callee.peer); + this.collectFromDecl(decl); + this.findIsSpecialCall(decl); + } + + refreshOnce(): void { + let currInfo = this.info ?? {}; + const declRecord = this._declRecord.toRecord(); + const structDeclInfo = this.structDeclInfo ?? this._structDeclRecord?.toRecord(); + const fromStructInfo = this.fromStructInfo ?? this._fromStructRecord?.toRecord(); + currInfo = { + ...currInfo, + ...(this.ptr && { ptr: this.ptr }), + ...(this.callName && { callName: this.callName }), + ...(this.chainingCallNames && { chainingCallNames: this.chainingCallNames }), + ...(this.isThis && { isThis: this.isThis }), + ...(this.isTrailingCall && { isTrailingCall: this.isTrailingCall }), + ...(declRecord && { ...declRecord }), + ...(this.rootCallInfo && { rootCallInfo: this.rootCallInfo }), + ...(structDeclInfo && { structDeclInfo }), + ...(fromStructInfo && { fromStructInfo }), + }; + this.info = currInfo; + } + + toRootCallJSON(): CallInfo | undefined { + if (!this.info || !this.info.rootCallInfo) { + return undefined; + } + const rootInfo = this.info.rootCallInfo; + return { + ...(rootInfo.callName && { callName: rootInfo.callName }), + ...(rootInfo.chainingCallNames && { chainingCallNames: rootInfo.chainingCallNames }), + ...(rootInfo.isThis && { isThis: rootInfo.isThis }), + ...(rootInfo.isTrailingCall && { isTrailingCall: rootInfo.isTrailingCall }), + ...(rootInfo.declName && { declName: rootInfo.declName }), + ...(rootInfo.modifiers && { modifiers: rootInfo.modifiers }), + ...(rootInfo.moduleName && { moduleName: rootInfo.moduleName }), + ...(rootInfo.isDeclFromClassProperty && { isDeclFromClassProperty: rootInfo.isDeclFromClassProperty }), + ...(rootInfo.isDeclFromMethod && { isDeclFromMethod: rootInfo.isDeclFromMethod }), + ...(rootInfo.isDeclFromFunction && { isDeclFromFunction: rootInfo.isDeclFromFunction }), + ...(rootInfo.annotationInfo && { annotationInfo: rootInfo.annotationInfo }), + }; + } + + toJSON(): CallInfo { + this.refresh(); + const declInfo = this._declRecord.toJSON(); + const rootCallInfo = this.toRootCallJSON(); + const structDeclInfo = this._structDeclRecord?.toJSON(); + const fromStructInfo = this._fromStructRecord?.toJSON(); + return { + ...(this.info?.callName && { callName: this.info.callName }), + ...(this.info?.chainingCallNames && { chainingCallNames: this.info.chainingCallNames }), + ...(this.info?.isThis && { isThis: this.info.isThis }), + ...(this.info?.isTrailingCall && { isTrailingCall: this.info.isTrailingCall }), + ...(declInfo && { ...declInfo }), + ...(rootCallInfo && { rootCallInfo }), + ...(structDeclInfo && { structDeclInfo }), + ...(fromStructInfo && { fromStructInfo }), + }; + } +} diff --git a/arkui-plugins/collectors/ui-collectors/records/function.ts b/arkui-plugins/collectors/ui-collectors/records/function.ts new file mode 100644 index 000000000..2e00b30f3 --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/records/function.ts @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { FunctionAnnotationInfo, FunctionAnnotationRecord, FunctionAnnotations } from './annotations'; +import { AnnotationRecord } from './annotations/base'; +import { BaseRecord, RecordOptions } from './base'; +import { BuiltInNames } from '../../../common/predefines'; + +export type FunctionInfo = AnnotationRecord< + FunctionAnnotations, + FunctionAnnotationInfo +> & { + name?: string; + modifiers?: arkts.Es2pandaModifierFlags; + kind?: arkts.Es2pandaMethodDefinitionKind; + isDecl?: boolean; + isGlobalInit?: boolean; + isGlobalMain?: boolean; +}; + +export class FunctionRecord extends BaseRecord { + private _annotationRecord: FunctionAnnotationRecord; + + protected name?: string; + protected modifiers?: arkts.Es2pandaModifierFlags; + protected kind?: arkts.Es2pandaMethodDefinitionKind; + protected isDecl?: boolean; + protected isGlobalInit?: boolean; + protected isGlobalMain?: boolean; + + constructor(options: RecordOptions) { + super(options); + this._annotationRecord = new FunctionAnnotationRecord(options); + } + + collectFromNode(node: arkts.MethodDefinition): void { + this.name = node.name.name; + this.modifiers = node.modifiers; + this.kind = node.kind; + this.isDecl = arkts.hasModifierFlag(node, arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_DECLARE); + this.isGlobalInit = this.name === BuiltInNames.GLOBAL_INIT_METHOD; + this.isGlobalMain = this.name === BuiltInNames.GLOBAL_MAIN_METHOD; + for (const anno of node.scriptFunction.annotations) { + this._annotationRecord.collect(anno); + } + } + + refreshOnce(): void { + let currInfo = this.info ?? {}; + const annotationRecord = this._annotationRecord.toRecord(); + currInfo = { + ...currInfo, + ...(this.name && { name: this.name }), + ...(this.modifiers && { modifiers: this.modifiers }), + ...(this.kind && { kind: this.kind }), + ...(this.isDecl && { isDecl: this.isDecl }), + ...(this.isGlobalInit && { isGlobalInit: this.isGlobalInit }), + ...(this.isGlobalMain && { isGlobalMain: this.isGlobalMain }), + ...(annotationRecord && { ...annotationRecord }), + }; + this.info = currInfo; + } + + toJSON(): FunctionInfo { + this.refresh(); + return { + ...(this.info?.name && { name: this.info.name }), + ...(this.info?.modifiers && { modifiers: this.info.modifiers }), + ...(this.info?.kind && { kind: this.info.kind }), + ...(this.info?.isDecl && { isDecl: this.info.isDecl }), + ...(this.info?.annotationInfo && { annotationInfo: this.info.annotationInfo }), + }; + } +} diff --git a/arkui-plugins/collectors/ui-collectors/records/index.ts b/arkui-plugins/collectors/ui-collectors/records/index.ts new file mode 100644 index 000000000..c48cb8bd3 --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/records/index.ts @@ -0,0 +1,31 @@ +/* + * 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 * from './annotations'; +export * from './normal-class'; +export * from './normal-class-property'; +export * from './normal-class-method'; +export * from './normal-interface'; +export * from './normal-interface-property'; +export * from './struct'; +export * from './struct-property'; +export * from './struct-method'; +export * from './struct-interface'; +export * from './struct-interface-property'; +export * from './call-declaration'; +export * from './function'; +export * from './function-call'; +export * from './record-builder'; +export * from './cache'; \ No newline at end of file diff --git a/arkui-plugins/collectors/ui-collectors/records/normal-class-method.ts b/arkui-plugins/collectors/ui-collectors/records/normal-class-method.ts new file mode 100644 index 000000000..f1f6de124 --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/records/normal-class-method.ts @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { BaseRecord, RecordOptions } from './base'; +import { NormalClassInfo, NormalClassRecord } from './normal-class'; +import { AnnotationRecord } from './annotations/base'; +import { + NormalClassMethodAnnotationInfo, + NormalClassMethodAnnotationRecord, + NormalClassMethodAnnotations, +} from './annotations'; + +export type NormalClassMethodInfo = AnnotationRecord & { + classInfo?: NormalClassInfo; + name?: string; + modifiers?: arkts.Es2pandaModifierFlags; + kind?: arkts.Es2pandaMethodDefinitionKind; + isDecl?: boolean; +}; + +export interface NormalClassMethodRecordOptions extends RecordOptions { + classRecord?: NormalClassRecord; +} + +export class NormalClassMethodRecord extends BaseRecord { + private _annotationRecord: NormalClassMethodAnnotationRecord; + private _classRecord?: NormalClassRecord; + + protected name?: string; + protected modifiers?: arkts.Es2pandaModifierFlags; + protected kind?: arkts.Es2pandaMethodDefinitionKind; + protected isDecl?: boolean; + + constructor(options: NormalClassMethodRecordOptions) { + super(options); + this._classRecord = options.classRecord; + this._annotationRecord = new NormalClassMethodAnnotationRecord(options); + } + + collectFromNode(node: arkts.MethodDefinition): void { + this.name = node.name.name; + this.modifiers = node.modifiers; + this.kind = node.kind; + this.isDecl = arkts.hasModifierFlag(node, arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_DECLARE); + for (const anno of node.scriptFunction.annotations) { + this._annotationRecord.collect(anno); + } + } + + refreshOnce(): void { + let currInfo = this.info ?? {}; + const annotationRecord = this._annotationRecord.toRecord(); + const classRecord = this._classRecord?.toRecord(); + currInfo = { + ...currInfo, + ...(this.name && { name: this.name }), + ...(this.modifiers && { modifiers: this.modifiers }), + ...(this.kind && { kind: this.kind }), + ...(this.isDecl && { isDecl: this.isDecl }), + ...(annotationRecord && { ...annotationRecord }), + ...(classRecord && { classInfo: classRecord }), + }; + this.info = currInfo; + } + + toJSON(): NormalClassMethodInfo { + this.refresh(); + const classInfo = this._classRecord?.toJSON(); + return { + ...(this.info?.name && { name: this.info.name }), + ...(this.info?.modifiers && { modifiers: this.info.modifiers }), + ...(this.info?.kind && { kind: this.info.kind }), + ...(this.info?.isDecl && { isDecl: this.info.isDecl }), + ...(this.info?.annotationInfo && { annotationInfo: this.info.annotationInfo }), + ...(classInfo && { classInfo }), + }; + } +} diff --git a/arkui-plugins/collectors/ui-collectors/records/normal-class-property.ts b/arkui-plugins/collectors/ui-collectors/records/normal-class-property.ts new file mode 100644 index 000000000..923c7cbd8 --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/records/normal-class-property.ts @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { + NormalClassPropertyAnnotationInfo, + NormalClassPropertyAnnotationRecord, + NormalClassPropertyAnnotations, +} from './annotations'; +import { AnnotationRecord } from './annotations/base'; +import { BaseRecord, RecordOptions } from './base'; +import { NormalClassInfo, NormalClassRecord } from './normal-class'; + +export type NormalClassPropertyInfo = AnnotationRecord< + NormalClassPropertyAnnotations, + NormalClassPropertyAnnotationInfo +> & { + classInfo?: NormalClassInfo; + name?: string; + modifiers?: arkts.Es2pandaModifierFlags; +}; + +export interface NormalClassPropertyRecordOptions extends RecordOptions { + classRecord: NormalClassRecord; +} + +export class NormalClassPropertyRecord extends BaseRecord { + private _annotationRecord: NormalClassPropertyAnnotationRecord; + private _classRecord: NormalClassRecord; + + protected name?: string; + protected modifiers?: arkts.Es2pandaModifierFlags; + + constructor(options: NormalClassPropertyRecordOptions) { + super(options); + this._classRecord = options.classRecord; + this._annotationRecord = new NormalClassPropertyAnnotationRecord(options); + } + + collectFromNode(node: arkts.ClassProperty): void { + const key: arkts.Expression | undefined = node.key; + if (!key || !arkts.isIdentifier(key)) { + return; + } + this.name = key.name; + this.modifiers = node.modifiers; + for (const anno of node.annotations) { + this._annotationRecord.collect(anno); + } + } + + refreshOnce(): void { + let currInfo = this.info ?? {}; + const annotationRecord = this._annotationRecord.toRecord(); + const classRecord = this._classRecord.toRecord(); + currInfo = { + ...currInfo, + ...(this.name && { name: this.name }), + ...(this.modifiers && { modifiers: this.modifiers }), + ...(annotationRecord && { ...annotationRecord }), + ...(classRecord && { classInfo: classRecord }), + }; + this.info = currInfo; + } + + toJSON(): NormalClassPropertyInfo { + this.refresh(); + const classInfo = this._classRecord.toJSON(); + return { + ...(this.info?.name && { name: this.info.name }), + ...(this.info?.modifiers && { modifiers: this.info.modifiers }), + ...(this.info?.annotationInfo && { annotationInfo: this.info.annotationInfo }), + ...(classInfo && { classInfo }), + }; + } +} diff --git a/arkui-plugins/collectors/ui-collectors/records/normal-class.ts b/arkui-plugins/collectors/ui-collectors/records/normal-class.ts new file mode 100644 index 000000000..9ef8298c1 --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/records/normal-class.ts @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { NormalClassAnnotationInfo, NormalClassAnnotationRecord, NormalClassAnnotations } from './annotations'; +import { AnnotationRecord } from './annotations/base'; +import { BaseRecord, RecordOptions } from './base'; +import { RecordCache } from './cache'; +import { BuiltInNames } from '../../../common/predefines'; +import { AstNodePointer } from '../../../common/safe-types'; + +export type NormalClassInfo = AnnotationRecord & { + /** + * class defintion node's pointer. + */ + definitionPtr?: AstNodePointer; + + /** + * class name. + */ + name?: string; + + /** + * whether this class is declared. + */ + isDecl?: boolean; + + /** + * whether this class is ETSGLOBAL class. + */ + isETSGlobal?: boolean; +}; + +export class NormalClassRecord extends BaseRecord { + private _annotationRecord: NormalClassAnnotationRecord; + + protected definitionPtr?: AstNodePointer; + protected name?: string; + protected isDecl?: boolean; + protected isETSGlobal?: boolean; + + constructor(options: RecordOptions) { + super(options); + this._annotationRecord = new NormalClassAnnotationRecord(options); + } + + collectFromNode(node: arkts.ClassDeclaration): void { + // console.log("[NormalClassRecord] node: ", node.dumpSrc()); + const definition: arkts.ClassDefinition | undefined = node.definition; + if (!definition || !definition?.ident?.name) { + return; + } + this.name = definition.ident.name; + this.definitionPtr = definition.peer; + this.isETSGlobal = this.name === BuiltInNames.ETS_GLOBAL_CLASS; + this.isDecl = arkts.hasModifierFlag(node, arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_DECLARE); + for (const anno of definition.annotations) { + this._annotationRecord.collect(anno); + } + RecordCache.getInstance().set(node.peer, this); + } + + refreshOnce(): void { + let currInfo = this.info ?? {}; + const annotationRecord = this._annotationRecord.toRecord(); + currInfo = { + ...currInfo, + ...(this.name && { name: this.name }), + ...(this.isDecl && { isDecl: this.isDecl }), + ...(this.isETSGlobal && { isETSGlobal: this.isETSGlobal }), + ...(this.definitionPtr && { definitionPtr: this.definitionPtr }), + ...(annotationRecord && { ...annotationRecord }), + }; + this.info = currInfo; + } + + toJSON(): NormalClassInfo { + this.refresh(); + return { + ...(this.info?.name && { name: this.info.name }), + ...(this.info?.isDecl && { isDecl: this.info.isDecl }), + ...(this.isETSGlobal && { isETSGlobal: this.isETSGlobal }), + ...(this.info?.annotationInfo && { annotationInfo: this.info.annotationInfo }), + }; + } +} diff --git a/arkui-plugins/collectors/ui-collectors/records/normal-interface-property.ts b/arkui-plugins/collectors/ui-collectors/records/normal-interface-property.ts new file mode 100644 index 000000000..43bd7c03a --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/records/normal-interface-property.ts @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { + NormalInterfacePropertyAnnotationInfo, + NormalInterfacePropertyAnnotationRecord, + NormalInterfacePropertyAnnotations, +} from './annotations'; +import { AnnotationRecord } from './annotations/base'; +import { BaseRecord, RecordOptions } from './base'; +import { NormalInterfaceInfo, NormalInterfaceRecord } from './normal-interface'; + +export type NormalInterfacePropertyInfo = AnnotationRecord< + NormalInterfacePropertyAnnotations, + NormalInterfacePropertyAnnotationInfo +> & { + interfaceInfo?: NormalInterfaceInfo; + name?: string; + modifiers?: arkts.Es2pandaModifierFlags; + kind?: arkts.Es2pandaMethodDefinitionKind; +}; + +export interface NormalInterfacePropertyRecordOptions extends RecordOptions { + interfaceRecord: NormalInterfaceRecord; +} + +export class NormalInterfacePropertyRecord extends BaseRecord { + private _annotationRecord: NormalInterfacePropertyAnnotationRecord; + private _interfaceRecord: NormalInterfaceRecord; + + protected name?: string; + protected modifiers?: arkts.Es2pandaModifierFlags; + protected kind?: arkts.Es2pandaMethodDefinitionKind; + + constructor(options: NormalInterfacePropertyRecordOptions) { + super(options); + this._interfaceRecord = options.interfaceRecord; + this._annotationRecord = new NormalInterfacePropertyAnnotationRecord(options); + } + + collectFromNode(node: arkts.MethodDefinition): void { + this.name = node.name.name; + this.modifiers = node.modifiers; + this.kind = node.kind; + for (const anno of node.scriptFunction.annotations) { + this._annotationRecord.collect(anno); + } + } + + refreshOnce(): void { + let currInfo = this.info ?? {}; + const annotationRecord = this._annotationRecord.toRecord(); + const interfaceRecord = this._interfaceRecord.toRecord(); + currInfo = { + ...currInfo, + ...(this.name && { name: this.name }), + ...(this.modifiers && { modifiers: this.modifiers }), + ...(this.kind && { kind: this.kind }), + ...(annotationRecord && { ...annotationRecord }), + ...(interfaceRecord && { interfaceInfo: interfaceRecord }), + }; + this.info = currInfo; + } + + toJSON(): NormalInterfacePropertyInfo { + this.refresh(); + const interfaceInfo = this._interfaceRecord.toJSON(); + return { + ...(this.info?.name && { name: this.info.name }), + ...(this.info?.modifiers && { modifiers: this.info.modifiers }), + ...(this.info?.kind && { kind: this.info.kind }), + ...(this.info?.annotationInfo && { annotationInfo: this.info.annotationInfo }), + ...(interfaceInfo && { interfaceInfo }), + }; + } +} diff --git a/arkui-plugins/collectors/ui-collectors/records/normal-interface.ts b/arkui-plugins/collectors/ui-collectors/records/normal-interface.ts new file mode 100644 index 000000000..729be1798 --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/records/normal-interface.ts @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { BaseRecord, RecordOptions } from './base'; + +export type NormalInterfaceInfo = { + name?: string; +}; + +export class NormalInterfaceRecord extends BaseRecord { + protected name?: string; + + constructor(options: RecordOptions) { + super(options); + } + + collectFromNode(node: arkts.TSInterfaceDeclaration): void { + const interfaceBody: arkts.TSInterfaceBody | undefined = node.body; + if (!interfaceBody || !node.id?.name) { + return; + } + this.name = node.id.name; + } + + refreshOnce(): void { + let currInfo = this.info ?? {}; + currInfo = { + ...currInfo, + ...(this.name && { name: this.name }), + } + this.info = currInfo; + } + + toJSON(): NormalInterfaceInfo { + this.refresh(); + return { + ...(this.info?.name && { name: this.info.name }), + } + } +} diff --git a/arkui-plugins/collectors/ui-collectors/records/record-builder.ts b/arkui-plugins/collectors/ui-collectors/records/record-builder.ts new file mode 100644 index 000000000..8af86e11f --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/records/record-builder.ts @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { BaseRecord, RecordOptions } from './base'; +import { RecordCache } from './cache'; +import { AstNodePointer } from 'common/safe-types'; + +function getOrPut< + T extends BaseRecord>, + U extends RecordOptions = RecordOptions +>(key: AstNodePointer, options: U, create: (options: U) => T): T { + if (RecordCache.getInstance().has(key)) { + return RecordCache.getInstance().get(key)!; + } + const newRecord = create(options); + RecordCache.getInstance().set(key, newRecord); + return newRecord; +} + +export class RecordBuilder { + static build>, T extends RecordOptions = RecordOptions>( + Record: { new (options: T): V }, + node: U, + options: T + ): V { + return getOrPut(node.peer, options, (options: T) => new Record(options)); + } + + static reset(): void { + RecordCache.getInstance().clear(); + } +} diff --git a/arkui-plugins/collectors/ui-collectors/records/struct-interface-property.ts b/arkui-plugins/collectors/ui-collectors/records/struct-interface-property.ts new file mode 100644 index 000000000..ca3012971 --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/records/struct-interface-property.ts @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { StructPropertyAnnotationInfo, StructPropertyAnnotationRecord, StructPropertyAnnotations } from './annotations'; +import { AnnotationRecord } from './annotations/base'; +import { BaseRecord, RecordOptions } from './base'; +import { CustomComponentInterfaceInfo, CustomComponentInterfaceRecord } from './struct-interface'; +import { RecordCache } from './cache'; + +export type CustomComponentInterfacePropertyInfo = AnnotationRecord< + StructPropertyAnnotations, + StructPropertyAnnotationInfo +> & { + interfaceInfo?: CustomComponentInterfaceInfo; + name?: string; + modifiers?: arkts.Es2pandaModifierFlags; + kind?: arkts.Es2pandaMethodDefinitionKind; +}; + +export interface CustomComponentInterfacePropertyRecordOptions extends RecordOptions { + interfaceRecord?: CustomComponentInterfaceRecord; +} + +export class CustomComponentInterfacePropertyRecord extends BaseRecord< + arkts.MethodDefinition, + CustomComponentInterfacePropertyInfo +> { + private _annotationRecord: StructPropertyAnnotationRecord; + private _interfaceRecord?: CustomComponentInterfaceRecord; + + protected name?: string; + protected modifiers?: arkts.Es2pandaModifierFlags; + protected kind?: arkts.Es2pandaMethodDefinitionKind; + + constructor(options: CustomComponentInterfacePropertyRecordOptions) { + super(options); + this._interfaceRecord = options.interfaceRecord; + this._annotationRecord = new StructPropertyAnnotationRecord(options); + } + + collectFromNode(node: arkts.MethodDefinition): void { + this.name = node.name.name; + this.modifiers = node.modifiers; + this.kind = node.kind; + for (const anno of node.scriptFunction.annotations) { + this._annotationRecord.collect(anno); + } + RecordCache.getInstance().set(node.peer, this); + } + + refreshOnce(): void { + let currInfo = this.info ?? {}; + const annotationRecord = this._annotationRecord.toRecord(); + const interfaceRecord = this._interfaceRecord?.toRecord(); + currInfo = { + ...currInfo, + ...(this.name && { name: this.name }), + ...(this.modifiers && { modifiers: this.modifiers }), + ...(this.kind && { kind: this.kind }), + ...(annotationRecord && { ...annotationRecord }), + ...(interfaceRecord && { interfaceInfo: interfaceRecord }), + }; + this.info = currInfo; + } + + toJSON(): CustomComponentInterfacePropertyInfo { + this.refresh(); + const interfaceInfo = this._interfaceRecord?.toJSON(); + return { + ...(this.info?.name && { name: this.info.name }), + ...(this.info?.modifiers && { modifiers: this.info.modifiers }), + ...(this.info?.kind && { kind: this.info.kind }), + ...(this.info?.annotationInfo && { annotationInfo: this.info.annotationInfo }), + ...(interfaceInfo && { interfaceInfo }), + }; + } +} diff --git a/arkui-plugins/collectors/ui-collectors/records/struct-interface.ts b/arkui-plugins/collectors/ui-collectors/records/struct-interface.ts new file mode 100644 index 000000000..83bdd9ac5 --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/records/struct-interface.ts @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { CustomComponentAnnotationRecord, CustomComponentAnnotations, StructAnnotationInfo } from './annotations'; +import { AnnotationRecord } from './annotations/base'; +import { BaseRecord, RecordOptions } from './base'; +import { RecordCache } from './cache'; + +export type CustomComponentInterfaceInfo = AnnotationRecord & { + name?: string; +}; + +export class CustomComponentInterfaceRecord extends BaseRecord { + private _annotationRecord: CustomComponentAnnotationRecord; + + protected name?: string; + + constructor(options: RecordOptions) { + super(options); + this._annotationRecord = new CustomComponentAnnotationRecord(options); + } + + collectFromNode(node: arkts.TSInterfaceDeclaration): void { + const interfaceBody: arkts.TSInterfaceBody | undefined = node.body; + if (!interfaceBody || !node.id?.name) { + return; + } + this.name = node.id.name; + for (const anno of node.annotations) { + this._annotationRecord.collect(anno); + } + RecordCache.getInstance().set(node.peer, this); + } + + refreshOnce(): void { + let currInfo = this.info ?? {}; + const annotationRecord = this._annotationRecord.toRecord(); + currInfo = { + ...currInfo, + ...(this.name && { name: this.name }), + ...(annotationRecord && { ...annotationRecord }) + } + this.info = currInfo; + } + + toJSON(): CustomComponentInterfaceInfo { + this.refresh(); + return { + ...(this.info?.name && { name: this.info.name }), + ...(this.info?.annotationInfo && { annotationInfo: this.info.annotationInfo }), + } + } +} diff --git a/arkui-plugins/collectors/ui-collectors/records/struct-method.ts b/arkui-plugins/collectors/ui-collectors/records/struct-method.ts new file mode 100644 index 000000000..28804dc5b --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/records/struct-method.ts @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { StructMethodAnnotationInfo, StructMethodAnnotationRecord, StructMethodAnnotations } from './annotations'; +import { AnnotationRecord } from './annotations/base'; +import { BaseRecord, RecordOptions } from './base'; +import { CustomComponentRecord, CustomComponentInfo } from './struct'; +import { CustomComponentNames } from '../../../common/predefines'; +import { RecordCache } from './cache'; + +export type StructMethodInfo = AnnotationRecord & { + structInfo?: CustomComponentInfo; + name?: string; + modifiers?: arkts.Es2pandaModifierFlags; + kind?: arkts.Es2pandaMethodDefinitionKind; + isDecl?: boolean; + isCtor?: boolean; +}; + +export interface StructMethodRecordOptions extends RecordOptions { + structRecord?: CustomComponentRecord; +} + +export class StructMethodRecord extends BaseRecord { + private _annotationRecord: StructMethodAnnotationRecord; + private _structRecord?: CustomComponentRecord; + + protected name?: string; + protected modifiers?: arkts.Es2pandaModifierFlags; + protected kind?: arkts.Es2pandaMethodDefinitionKind; + protected isDecl?: boolean; + protected isCtor?: boolean; + + constructor(options: StructMethodRecordOptions) { + super(options); + this._structRecord = options.structRecord; + this._annotationRecord = new StructMethodAnnotationRecord(options); + } + + collectFromNode(node: arkts.MethodDefinition): void { + this.name = node.name.name; + this.modifiers = node.modifiers; + this.kind = node.kind; + this.isDecl = arkts.hasModifierFlag(node, arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_DECLARE); + this.isCtor = this.name === CustomComponentNames.COMPONENT_CONSTRUCTOR_ORI; + for (const anno of node.scriptFunction.annotations) { + this._annotationRecord.collect(anno); + } + RecordCache.getInstance().set(node.peer, this); + } + + refreshOnce(): void { + let currInfo = this.info ?? {}; + const annotationRecord = this._annotationRecord.toRecord(); + const structRecord = this._structRecord?.toRecord(); + currInfo = { + ...currInfo, + ...(this.name && { name: this.name }), + ...(this.modifiers && { modifiers: this.modifiers }), + ...(this.kind && { kind: this.kind }), + ...(this.isDecl && { isDecl: this.isDecl }), + ...(this.isCtor && { isCtor: this.isCtor }), + ...(annotationRecord && { ...annotationRecord }), + ...(structRecord && { structInfo: structRecord }), + }; + this.info = currInfo; + } + + toJSON(): StructMethodInfo { + this.refresh(); + const structInfo = this._structRecord?.toJSON(); + return { + ...(this.info?.name && { name: this.info.name }), + ...(this.info?.modifiers && { modifiers: this.info.modifiers }), + ...(this.info?.kind && { kind: this.info.kind }), + ...(this.info?.isDecl && { isDecl: this.info.isDecl }), + ...(this.info?.isCtor && { isCtor: this.info.isCtor }), + ...(this.info?.annotationInfo && { annotationInfo: this.info.annotationInfo }), + ...(structInfo && { structInfo }), + }; + } +} diff --git a/arkui-plugins/collectors/ui-collectors/records/struct-property.ts b/arkui-plugins/collectors/ui-collectors/records/struct-property.ts new file mode 100644 index 000000000..ff1d698ad --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/records/struct-property.ts @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { StructPropertyAnnotationInfo, StructPropertyAnnotationRecord, StructPropertyAnnotations } from './annotations'; +import { AnnotationRecord } from './annotations/base'; +import { BaseRecord, RecordOptions } from './base'; +import { CustomComponentRecord, CustomComponentInfo } from './struct'; +import { RecordCache } from './cache'; + +export type StructPropertyInfo = AnnotationRecord & { + structInfo?: CustomComponentInfo; + name?: string; + modifiers?: arkts.Es2pandaModifierFlags; +}; + +export interface StructPropertyRecordOptions extends RecordOptions { + structRecord: CustomComponentRecord; +} + +export class StructPropertyRecord extends BaseRecord { + private _annotationRecord: StructPropertyAnnotationRecord; + private _structRecord: CustomComponentRecord; + + protected name?: string; + protected modifiers?: arkts.Es2pandaModifierFlags; + + constructor(options: StructPropertyRecordOptions) { + super(options); + this._structRecord = options.structRecord; + this._annotationRecord = new StructPropertyAnnotationRecord(options); + } + + collectFromNode(node: arkts.ClassProperty): void { + const key: arkts.Expression | undefined = node.key; + if (!key || !arkts.isIdentifier(key)) { + return; + } + this.name = key.name; + this.modifiers = node.modifiers; + for (const anno of node.annotations) { + this._annotationRecord.collect(anno); + } + RecordCache.getInstance().set(node.peer, this); + } + + refreshOnce(): void { + let currInfo = this.info ?? {}; + const annotationRecord = this._annotationRecord.toRecord(); + const structRecord = this._structRecord.toRecord(); + currInfo = { + ...currInfo, + ...(this.name && { name: this.name }), + ...(this.modifiers && { modifiers: this.modifiers }), + ...(annotationRecord && { ...annotationRecord }), + ...(structRecord && { structInfo: structRecord }), + }; + this.info = currInfo; + } + + toJSON(): StructPropertyInfo { + this.refresh(); + const structInfo = this._structRecord.toJSON(); + return { + ...(this.info?.name && { name: this.info.name }), + ...(this.info?.modifiers && { modifiers: this.info.modifiers }), + ...(this.info?.annotationInfo && { annotationInfo: this.info.annotationInfo }), + ...(structInfo && { structInfo }), + }; + } +} diff --git a/arkui-plugins/collectors/ui-collectors/records/struct.ts b/arkui-plugins/collectors/ui-collectors/records/struct.ts new file mode 100644 index 000000000..c23b0f786 --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/records/struct.ts @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { CustomComponentAnnotationRecord, CustomComponentAnnotations, StructAnnotationInfo } from './annotations'; +import { AnnotationRecord } from './annotations/base'; +import { BaseRecord, RecordOptions } from './base'; +import { checkIsCustomComponentDeclaredClass, checkIsStructFromNode } from '../utils'; +import { AstNodePointer } from '../../../common/safe-types'; +import { RecordCache } from './cache'; + +export type CustomComponentInfo = AnnotationRecord & { + /** + * class defintion node's pointer. + */ + definitionPtr?: AstNodePointer; + + /** + * struct name, or declared `CustomComponent` etcs. name in header files. + */ + name?: string; + + /** + * whether this struct or `CustomComponent` class is declared. + */ + isDecl?: boolean; +}; + +export class CustomComponentRecord extends BaseRecord { + private _annotationRecord: CustomComponentAnnotationRecord; + + protected definitionPtr?: AstNodePointer; + protected name?: string; + protected isDecl?: boolean; + + constructor(options: RecordOptions) { + super(options); + this._annotationRecord = new CustomComponentAnnotationRecord(options); + } + + collectFromNode(node: arkts.ClassDeclaration): void { + const definition: arkts.ClassDefinition | undefined = node.definition; + if (!definition || !definition?.ident?.name) { + return; + } + this.name = definition.ident.name; + this.isDecl = arkts.hasModifierFlag(node, arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_DECLARE); + if (checkIsCustomComponentDeclaredClass(definition, this.isDecl)) { + return; + } + if (checkIsStructFromNode(node)) { + for (const anno of definition.annotations) { + this._annotationRecord.collect(anno); + } + this.definitionPtr = definition.peer; + } + RecordCache.getInstance().set(node.peer, this); + } + + refreshOnce(): void { + let currInfo = this.info ?? {}; + const annotationRecord = this._annotationRecord.toRecord(); + currInfo = { + ...currInfo, + ...(this.name && { name: this.name }), + ...(this.isDecl && { isDecl: this.isDecl }), + ...(this.definitionPtr && { definitionPtr: this.definitionPtr }), + ...(annotationRecord && { ...annotationRecord }) + } + this.info = currInfo; + } + + toJSON(): CustomComponentInfo { + this.refresh(); + return { + ...(this.info?.name && { name: this.info.name }), + ...(this.info?.isDecl && { isDecl: this.info.isDecl }), + ...(this.info?.annotationInfo && { annotationInfo: this.info.annotationInfo }), + } + } +} diff --git a/arkui-plugins/collectors/ui-collectors/shared-types.ts b/arkui-plugins/collectors/ui-collectors/shared-types.ts new file mode 100644 index 000000000..c0625bb4b --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/shared-types.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. + */ + +import { VisitorOptions } from "../../common/abstract-visitor"; +import { AstNodePointer } from "../../common/safe-types"; +import { CustomComponentInfo, StructMethodInfo, StructPropertyInfo } from "./records"; + +export interface UICollectMetadata extends VisitorOptions { + shouldIgnoreDecl: boolean; +} + +export interface StructCollectorInfo extends CustomComponentInfo { + propertyInfoMap: Record; + methodInfoMap: Record; +} \ No newline at end of file diff --git a/arkui-plugins/collectors/ui-collectors/struct-collector.ts b/arkui-plugins/collectors/ui-collectors/struct-collector.ts new file mode 100644 index 000000000..8e22f31ca --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/struct-collector.ts @@ -0,0 +1,157 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { + CustomComponentRecord, + StructMethodInfo, + StructMethodRecord, + StructPropertyInfo, + StructPropertyRecord, +} from './records'; +import { AbstractVisitor, VisitorOptions } from '../../common/abstract-visitor'; +import { BuilderLambdaNames, CustomComponentNames, NodeCacheNames } from '../../common/predefines'; +import { AstNodePointer } from '../../common/safe-types'; +import { StructMethodValidator, StructPropertyValidator, ValidatorBuilder } from './validators'; + +export interface StructCollectorOptions extends VisitorOptions { + structRecord: CustomComponentRecord; + shouldIgnoreDecl?: boolean; +} + +export class StructCollector extends AbstractVisitor { + private _structRecord: CustomComponentRecord; + private _disableCollectProperty: boolean = false; + private _shouldCollectProperty: boolean = true; + private _properties: Record = {}; + private _methods: Record = {}; + + public shouldIgnoreDecl: boolean; + + constructor(options: StructCollectorOptions) { + super(options); + this._structRecord = options.structRecord; + this.shouldIgnoreDecl = options.shouldIgnoreDecl ?? false; + } + + get propertyInfoMap(): Record { + return this._properties; + } + + get methodInfoMap(): Record { + return this._methods; + } + + private get shouldCollectProperty(): boolean { + if (this._disableCollectProperty) { + return false; + } + return this._shouldCollectProperty; + } + + private set shouldCollectProperty(newValue: boolean) { + if (this._disableCollectProperty) { + return; + } + this._shouldCollectProperty = newValue; + } + + private canCollectMethodFromInfo(info: StructMethodInfo): boolean { + if (!!info.isDecl && info.isCtor) { + return true; + } + if (info.isDecl && info.name === BuilderLambdaNames.ORIGIN_METHOD_NAME) { + return true; + } + if (info.name === CustomComponentNames.COMPONENT_BUILD_ORI) { + return true; + } + if (!!info.annotationInfo && Object.keys(info.annotationInfo).length > 0) { + return true; + } + return false; + } + + private collectProperty(node: arkts.ClassProperty): void { + const propertyRecord = new StructPropertyRecord({ + structRecord: this._structRecord, + shouldIgnoreDecl: this.shouldIgnoreDecl, + }); + propertyRecord.collect(node); + + const propertyInfo = propertyRecord.toRecord(); + if (!propertyInfo) { + return; + } + // TODO: call ui-checker + // TODO: collect property info to cache + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.UI).collect(node, propertyRecord.toJSON()); + this._properties[node.peer] = propertyInfo; + + // console.log('[STRUCT PROPERTY] node: ', node.dumpSrc()); + // console.log('[STRUCT PROPERTY] propertyInfo: ', propertyInfo); + ValidatorBuilder.build(StructPropertyValidator).checkIsViolated(node, propertyInfo); + } + + private collectMethod(node: arkts.MethodDefinition): void { + const methodRecord = new StructMethodRecord({ + structRecord: this._structRecord, + shouldIgnoreDecl: this.shouldIgnoreDecl, + }); + methodRecord.collect(node); + + const methodInfo = methodRecord.toRecord(); + if (!methodInfo) { + return; + } + // TODO: call ui-checker + if (this.canCollectMethodFromInfo(methodInfo)) { + // TODO: collect method info to cache + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.UI).collect(node, methodRecord.toJSON()); + this._methods[node.peer] = methodInfo; + } + // console.log('[STRUCT METHOD] node: ', node.dumpSrc()); + // console.log('[STRUCT METHOD] methodInfo: ', methodInfo); + ValidatorBuilder.build(StructMethodValidator).checkIsViolated(node, methodInfo); + } + + disableCollectProperty(): this { + this._disableCollectProperty = true; + return this; + } + + enableCollectProperty(): this { + this._disableCollectProperty = false; + return this; + } + + reset(): void { + this._shouldCollectProperty = true; + this._disableCollectProperty = false; + this._properties = {}; + this._methods = {}; + } + + visitor(node: arkts.ClassDeclaration): arkts.ClassDeclaration { + node.definition?.body.forEach((st) => { + if (arkts.isClassProperty(st) && this.shouldCollectProperty) { + this.collectProperty(st); + } else if (arkts.isMethodDefinition(st)) { + this.collectMethod(st); + } + }); + return node; + } +} diff --git a/arkui-plugins/collectors/ui-collectors/struct-interface-collector.ts b/arkui-plugins/collectors/ui-collectors/struct-interface-collector.ts new file mode 100644 index 000000000..f62b214d3 --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/struct-interface-collector.ts @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { AbstractVisitor, VisitorOptions } from '../../common/abstract-visitor'; +import { + CustomComponentInterfacePropertyInfo, + CustomComponentInterfacePropertyRecord, + CustomComponentInterfaceRecord, +} from './records'; +import { NodeCacheNames } from '../../common/predefines'; + +export interface StructInterfaceCollectorOptions extends VisitorOptions { + interfaceRecord: CustomComponentInterfaceRecord; + shouldIgnoreDecl?: boolean; +} + +export class StructInterfaceCollector extends AbstractVisitor { + private _interfaceRecord: CustomComponentInterfaceRecord; + public shouldIgnoreDecl: boolean; + + constructor(options: StructInterfaceCollectorOptions) { + super(options); + this._interfaceRecord = options.interfaceRecord; + this.shouldIgnoreDecl = options.shouldIgnoreDecl ?? false; + } + + private canCollectMethodFromInfo(info: CustomComponentInterfacePropertyInfo): boolean { + if (!!info.annotationInfo && Object.keys(info.annotationInfo).length > 0) { + return true; + } + return false; + } + + private collectMethod(node: arkts.MethodDefinition): void { + const methodRecord = new CustomComponentInterfacePropertyRecord({ + interfaceRecord: this._interfaceRecord, + shouldIgnoreDecl: this.shouldIgnoreDecl, + }); + methodRecord.collect(node); + + const methodInfo = methodRecord.toRecord(); + if (!methodInfo) { + return; + } + if (this.canCollectMethodFromInfo(methodInfo)) { + // TODO: collect method info to cache + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.UI).collect(node, methodRecord.toJSON()); + // console.log("[STRUCT INTERFACE METHOD] node: ", node.dumpSrc()); + } + } + + visitor(node: arkts.TSInterfaceDeclaration): arkts.TSInterfaceDeclaration { + node.body?.body.forEach((st) => { + if (arkts.isMethodDefinition(st)) { + this.collectMethod(st); + } + }); + return node; + } +} diff --git a/arkui-plugins/collectors/ui-collectors/ui-visitor.ts b/arkui-plugins/collectors/ui-collectors/ui-visitor.ts new file mode 100644 index 000000000..416b5d34b --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/ui-visitor.ts @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { findAndCollectUINodeInPostOrder, findAndCollectUINodeInPreOrder } from './factory'; +import { UICollectMetadata } from './shared-types'; +import { CallRecordCollector } from './call-record-collector'; +import { ValidatorBuilder } from './validators'; +import { AbstractVisitor, VisitorOptions } from '../../common/abstract-visitor'; +import { LogCollector } from '../../common/log-collector'; + +export interface UIVisitorOptions extends VisitorOptions { + shouldIgnoreDecl?: boolean; +} + +export class UIVisitor extends AbstractVisitor { + private shouldIgnoreDecl?: boolean; + + constructor(options?: UIVisitorOptions) { + super(options); + this.shouldIgnoreDecl = true; // options?.shouldIgnoreDecl; + } + + reset(): void { + CallRecordCollector.getInstance(this.getMetadata()).reset(); + LogCollector.getInstance().reset(); + ValidatorBuilder.reset(); + } + + getMetadata(): UICollectMetadata { + return { + isExternal: this.isExternal, + externalSourceName: this.externalSourceName, + program: this.program, + shouldIgnoreDecl: this.shouldIgnoreDecl ?? false, + } + } + + visitor(node: arkts.AstNode): arkts.AstNode { + findAndCollectUINodeInPreOrder(node, this.getMetadata()); + const newNode = this.visitEachChild(node); + findAndCollectUINodeInPostOrder(newNode, this.getMetadata()); + return newNode; + } +} diff --git a/arkui-plugins/collectors/ui-collectors/utils.ts b/arkui-plugins/collectors/ui-collectors/utils.ts new file mode 100644 index 000000000..3013af320 --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/utils.ts @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { matchPrefix } from '../../common/arkts-utils'; +import { DeclarationCollector } from '../../common/declaration-collector'; +import { + CustomComponentNames, + ARKUI_IMPORT_PREFIX_NAMES, + ArkUIDeclInterfaceNames, + BuiltInNames, + BuilderLambdaNames, + Dollars, + StateManagementTypes, +} from '../../common/predefines'; +import { CustomComponentInfo, CustomComponentInterfaceInfo, NormalClassInfo, NormalInterfaceInfo } from './records'; + +export function findRootCallee(callee: arkts.AstNode | undefined): arkts.Identifier | undefined { + if (!callee) { + return undefined; + } + if (arkts.isIdentifier(callee)) { + return callee; + } + if (arkts.isMemberExpression(callee)) { + return findRootCallee(callee.property); + } + if (arkts.isTSAsExpression(callee)) { + return findRootCallee(callee.expr); + } + if (arkts.isTSNonNullExpression(callee)) { + return findRootCallee(callee.expr); + } + return undefined; +} + +export function findRootCallObject(callee: arkts.AstNode | undefined): arkts.Identifier | undefined { + if (!callee) { + return undefined; + } + if (arkts.isIdentifier(callee)) { + return callee; + } + if (arkts.isMemberExpression(callee)) { + return findRootCallee(callee.object); + } + if (arkts.isTSAsExpression(callee)) { + return findRootCallee(callee.expr); + } + if (arkts.isTSNonNullExpression(callee)) { + return findRootCallee(callee.expr); + } + return undefined; +} + +export function getAnnotationName(anno: arkts.AnnotationUsage, ignoreDecl?: boolean): string | undefined { + if (!anno.expr || !arkts.isIdentifier(anno.expr)) { + return undefined; + } + if (!ignoreDecl && !getDeclFromArkUI(anno.expr)) { + return undefined; + } + return anno.expr.name; +} + +export function getDeclFromArkUI( + node: arkts.AstNode, + matchSourcePrefix: (string | RegExp)[] = ARKUI_IMPORT_PREFIX_NAMES +): arkts.AstNode | undefined { + const decl = arkts.getPeerDecl(node.peer); + if (!decl) { + return undefined; + } + const moduleName: string | undefined = arkts.getProgramFromAstNode(decl)?.moduleName; + if (!moduleName || !matchPrefix(matchSourcePrefix, moduleName)) { + return undefined; + } + DeclarationCollector.getInstance().collect(decl); + return decl; +} + +export function formatBuiltInImplementedPropertyName(name: string): string { + return name.slice(BuiltInNames.IMPLEMENT_PROPETY_PREFIX.length); +} + +export function getStructFromCall( + callObject: arkts.Identifier | undefined, + callee: arkts.Identifier | undefined +): arkts.ClassDefinition | undefined { + if (!callObject || !callee) { + return undefined; + } + if (callee.name !== BuilderLambdaNames.ORIGIN_METHOD_NAME) { + return undefined; + } + const decl = arkts.getPeerDecl(callObject.peer); + if (!decl || !arkts.isClassDefinition(decl) || !arkts.classDefinitionIsFromStructConst(decl) || !decl.ident) { + return undefined; + } + return decl; +} + +export function checkIsCallNameFromResource(name: string): name is (Dollars.DOLLAR_RESOURCE | Dollars.DOLLAR_RAWFILE) { + return name === Dollars.DOLLAR_RESOURCE || name === Dollars.DOLLAR_RAWFILE; +} + +export function checkIsCallNameFromBindable(name: string): name is Dollars.DOLLAR_DOLLAR { + return name === Dollars.DOLLAR_DOLLAR; +} + +export function checkIsNameStartWithBackingField(node: arkts.AstNode | undefined): boolean { + if (!node || !arkts.isIdentifier(node)) { + return false; + } + return node.name.startsWith(StateManagementTypes.BACKING); +} + +export function checkIsStructFromNode(node: arkts.AstNode): boolean { + if (arkts.isStructDeclaration(node)) { + return true; + } + if ( + arkts.isClassDeclaration(node) && + !!node.definition && + arkts.classDefinitionIsFromStructConst(node.definition) + ) { + return true; + } + return false; +} + +export function checkIsCustomComponentDeclaredClass(node: arkts.ClassDefinition, isDecl?: boolean): boolean { + if (!node.ident) { + return false; + } + let info: CustomComponentInfo = { name: node.ident.name, isDecl }; + return checkIsCustomComponentDeclaredClassFromInfo(info); +} + +export function checkIsCustomComponentFromInfo( + info: CustomComponentInfo | CustomComponentInterfaceInfo | undefined +): boolean { + const annotationInfo = info?.annotationInfo ?? {}; + return !!annotationInfo.hasComponent || !!annotationInfo.hasComponentV2 || !!annotationInfo.hasCustomDialog; +} + +export function checkIsCustomComponentDeclaredClassFromInfo(info: CustomComponentInfo | undefined): boolean { + if (!info || !info.isDecl) { + return false; + } + return ( + info?.name === CustomComponentNames.COMPONENT_CLASS_NAME || + info?.name === CustomComponentNames.COMPONENT_V2_CLASS_NAME + ); +} + +export function checkIsObservedClassFromInfo(info: NormalClassInfo | undefined): boolean { + const annotationInfo = info?.annotationInfo ?? {}; + return !!annotationInfo.hasObserved || !!annotationInfo.hasObservedV2; +} + +export function checkIsETSGlobalClassFromInfo(info: NormalClassInfo | undefined): boolean { + return !!info?.isETSGlobal; +} + +export function checkIsCommonMethodInterfaceFromInfo(info: NormalInterfaceInfo | undefined): boolean { + return info?.name === ArkUIDeclInterfaceNames.COMMON_METHOD; +} diff --git a/arkui-plugins/collectors/ui-collectors/validators/base.ts b/arkui-plugins/collectors/ui-collectors/validators/base.ts new file mode 100644 index 000000000..e186a093a --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/validators/base.ts @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { LogCollector, LogInfo, SuggestionOptions } from '../../../common/log-collector'; + +export interface Validator { + checkIsViolated(node: TargetNode, metadata?: ContextMetadata): void; + collectContext(context: ContextMetadata): this; + reset(): void; +} + +export abstract class BaseValidator implements Validator { + protected context?: R; + + reset(): void { + this.context = undefined; + } + + checkIsViolated(node: T, metadata?: R | undefined): void { + if (!!metadata) { + this.collectContext(metadata); + } + this.reportIfViolated(node); + } + + collectContext(context: R): this { + this.context = context; + return this; + } + + protected report(logInfo: LogInfo): void { + // console.log('[VALIDATOR] report: ', formatReport(logInfo)); + LogCollector.getInstance().collectLogInfo(logInfo); + } + + abstract reportIfViolated(node: T): void; +} + +// TODO: remove this +function formatReport(logInfo: LogInfo): string { + const node = logInfo.node.dumpSrc(); + return JSON.stringify( + { + node, + level: logInfo.level, + message: logInfo.message, + args: logInfo.args, + suggestion: formatSuggestion(logInfo.suggestion), + code: logInfo.code, + }, + null, + 2 + ); +} + +// TODO: remove this +function formatSuggestion(suggestion: SuggestionOptions | undefined): Object | undefined { + if (!suggestion) { + return undefined; + } + const startRange = `(${suggestion.range[0].index()}, ${suggestion.range[0].line()})`; + const endRange = `(${suggestion.range[1].index()}, ${suggestion.range[1].line()})`; + const range = `${startRange} - ${endRange}`; + return { code: suggestion.code, range, args: suggestion.args }; +} diff --git a/arkui-plugins/collectors/ui-collectors/validators/cache.ts b/arkui-plugins/collectors/ui-collectors/validators/cache.ts new file mode 100644 index 000000000..c66b85a84 --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/validators/cache.ts @@ -0,0 +1,33 @@ +/* + * 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 { Validator } from "./base"; + +const cache = new Map(); + +export function getOrPut(key: string, create: () => Validator): Validator { + if (cache.has(key)) { + return cache.get(key)!; + } + + const newValidator = create(); + cache.set(key, newValidator); + return newValidator; +} + +export function clearValidatorCache(): void { + Array.from(cache.values()).forEach((validator) => validator.reset()); + cache.clear(); +} \ No newline at end of file diff --git a/arkui-plugins/collectors/ui-collectors/validators/index.ts b/arkui-plugins/collectors/ui-collectors/validators/index.ts new file mode 100644 index 000000000..56c26b9d1 --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/validators/index.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. + */ + +export * from './struct-call-validator'; +export * from './struct-property-validator'; +export * from './struct-method-validator'; +export * from './normal-class-method-validator'; +export * from './normal-class-property-validator'; +export * from './validator-builder'; \ No newline at end of file diff --git a/arkui-plugins/collectors/ui-collectors/validators/normal-class-method-validator.ts b/arkui-plugins/collectors/ui-collectors/validators/normal-class-method-validator.ts new file mode 100644 index 000000000..efa3809d7 --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/validators/normal-class-method-validator.ts @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { BaseValidator } from './base'; +import { NormalClassMethodInfo } from '../records'; +import { checkComputedDecorator } from './rules'; + +export class NormalClassMethodValidator extends BaseValidator { + reportIfViolated(node: arkts.MethodDefinition): void { + const metadata = this.context ?? {}; + if (!metadata.classInfo?.definitionPtr) { + return; + } + + const classNode = arkts.classByPeer(metadata.classInfo.definitionPtr); + checkComputedDecorator.bind(this)(node, classNode); + } +} diff --git a/arkui-plugins/collectors/ui-collectors/validators/normal-class-property-validator.ts b/arkui-plugins/collectors/ui-collectors/validators/normal-class-property-validator.ts new file mode 100644 index 000000000..699f36add --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/validators/normal-class-property-validator.ts @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { BaseValidator } from './base'; +import { NormalClassPropertyInfo } from '../records'; +import { checkComputedDecorator } from './rules'; + +export class NormalClassPropertyValidator extends BaseValidator { + reportIfViolated(node: arkts.ClassProperty): void { + const metadata = this.context ?? {}; + if (!metadata.classInfo?.definitionPtr) { + return; + } + + const classNode = arkts.classByPeer(metadata.classInfo.definitionPtr); + checkComputedDecorator.bind(this)(node, classNode); + } +} diff --git a/arkui-plugins/collectors/ui-collectors/validators/rules/check-builder-param.ts b/arkui-plugins/collectors/ui-collectors/validators/rules/check-builder-param.ts new file mode 100644 index 000000000..71d8a20a3 --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/validators/rules/check-builder-param.ts @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { BaseValidator } from '../base'; +import { isAnnotatedProperty } from '../utils'; +import { CallInfo } from '../../records'; +import { DecoratorNames, LogType } from '../../../../common/predefines'; + +/** + * 校验规则:自定义组件尾随闭包调用的场景,struct 声明中只能有1个BuilderParam + * + * 校验等级:error + */ +export function checkBuilderParam( + this: BaseValidator, + struct: arkts.ClassDefinition +): void { + const metadata = this.context ?? {}; + if (!metadata.isTrailingCall) { + return; + } + + // 从struct 中获取被@BuilderParam 修饰的属性个数 + const properties: Array<{ property: arkts.ClassProperty; argc: number }> = []; + for (const member of struct.body) { + if (isAnnotatedProperty(member, DecoratorNames.BUILDER_PARAM)) { + properties.push({ + property: member, + argc: getBuilderParamTypeArgc(member), + }); + } + } + + // 如果@BuilderParam个数超过1个或者@BuilderParam修饰的函数类型有参数,那么就报错 + if (properties.length > 1 || (properties.length === 1 && properties[0].argc > 0)) { + const structName = struct.ident!.name; + this.report({ + node: struct.parent!, // Class Declaration has correct position information + level: LogType.ERROR, + message: `In the trailing lambda case, '${structName}' must have one and only one property decorated with @BuilderParam, and its @BuilderParam expects no parameter.`, + }); + } +} + +function getBuilderParamTypeArgc(property: arkts.ClassProperty): number { + if (!property.typeAnnotation || !arkts.isETSFunctionType(property.typeAnnotation)) { + return -1; + } + return property.typeAnnotation.params.length; +} diff --git a/arkui-plugins/collectors/ui-collectors/validators/rules/check-component-link-init.ts b/arkui-plugins/collectors/ui-collectors/validators/rules/check-component-link-init.ts new file mode 100644 index 000000000..b22387aa2 --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/validators/rules/check-component-link-init.ts @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { BaseValidator } from '../base'; +import { isAnnotatedProperty } from '../utils'; +import { CallInfo } from '../../records'; +import { DecoratorNames, LogType } from '../../../../common/predefines'; +import { createSuggestion, getPositionRangeFromNode } from '../../../../common/log-collector'; + +/** + * 校验规则:当V2组件使用V1组件时,V1组件中不允许存在被`@Link`装饰的属性 + * + * 校验等级:error + */ +export function checkComponentLinkInit( + this: BaseValidator, + struct: arkts.ClassDefinition +): void { + const metadata = this.context ?? {}; + const fromComponentV2: boolean = !!metadata.fromStructInfo?.annotationInfo?.hasComponentV2; + const isComponentCall: boolean = !!metadata.structDeclInfo?.annotationInfo?.hasComponent; + if (!(fromComponentV2 && isComponentCall)) { + return; + } + + // 只要当前struct 中有被"@Link" 修饰的属性就报错 + for (const member of struct.body) { + if (isAnnotatedProperty(member, DecoratorNames.LINK)) { + const declaration = struct.parent!; // Class Declaration has correct position information + this.report({ + node: declaration, + level: LogType.ERROR, + message: `A V2 component cannot be used with any member property annotated by '@Link' in a V1 component.`, + suggestion: createSuggestion('', ...getPositionRangeFromNode(declaration)), + }); + } + } +} diff --git a/arkui-plugins/collectors/ui-collectors/validators/rules/check-component-v2-mix-use.ts b/arkui-plugins/collectors/ui-collectors/validators/rules/check-component-v2-mix-use.ts new file mode 100644 index 000000000..2de7dfcd7 --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/validators/rules/check-component-v2-mix-use.ts @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { BaseValidator } from '../base'; +import { NormalClassRecord, RecordBuilder, StructPropertyInfo } from '../../records'; +import { DecoratorNames, LogType } from '../../../../common/predefines'; + +/** + * 校验规则:禁止在`@Component`中使用`@ObservedV2`装饰的类 + * + * 校验等级:error + */ +export function checkComponentV2MixUse( + this: BaseValidator, + classProperty: arkts.ClassProperty +): void { + const metadata = this.context ?? {}; + if (!metadata.structInfo?.annotationInfo?.hasComponent) { + return; + } + + const v1Decorators = findStructPropertyV1DecoratorsFromInfo(metadata); + if (v1Decorators.length === 0) { + return; + } + let decl: arkts.AstNode | undefined; + let expr: arkts.Identifier | undefined; + expr = findTypeRefIdentFromType(classProperty.typeAnnotation); + if (!expr && checkIsNewClass(classProperty.value)) { + expr = findTypeRefIdentFromType(classProperty.value.getTypeRef); + } + if (!!expr) { + decl = arkts.getPeerDecl(expr.peer); + } + if (!decl || !checkIsClassDef(decl) || !checkIsClassDecl(decl.parent)) { + return; + } + const classRecord = RecordBuilder.build(NormalClassRecord, decl.parent, { shouldIgnoreDecl: true }); // TODO: change back to false; + if (!classRecord.isCollected) { + classRecord.collect(decl.parent); + } + const classInfo = classRecord.toRecord(); + if (!classInfo?.annotationInfo?.hasObservedV2) { + return; + } + v1Decorators.forEach((info) => + this.report({ + node: info.annotation, + message: `The type of the ${info.name} property cannot be a class decorated with '@ObservedV2'.`, + level: LogType.ERROR, + }) + ); +} + +interface DecoratorInfo { + name: string; + annotation: arkts.AnnotationUsage; +} + +const v1ComponentDecorators: string[] = [ + DecoratorNames.STATE, + DecoratorNames.PROP_REF, + DecoratorNames.LINK, + DecoratorNames.PROVIDE, + DecoratorNames.CONSUME, + DecoratorNames.STORAGE_LINK, + DecoratorNames.STORAGE_PROP_REF, + DecoratorNames.LOCAL_STORAGE_LINK, +]; + +function findStructPropertyV1DecoratorsFromInfo(info: StructPropertyInfo): DecoratorInfo[] { + if (!info.annotationInfo || !info.annotations) { + return []; + } + return v1ComponentDecorators + .filter((name) => !!info.annotationInfo?.[`has${name}`]) + .map((name) => ({ + name, + annotation: info.annotations?.[name]!, + })); +} + +function checkIsClassDef(node: arkts.AstNode | undefined): node is arkts.ClassDefinition { + return !!node && arkts.isClassDefinition(node); +} + +function checkIsClassDecl(node: arkts.AstNode | undefined): node is arkts.ClassDeclaration { + return !!node && arkts.isClassDeclaration(node); +} + +function checkIsTypeRef(node: arkts.AstNode | undefined): node is arkts.ETSTypeReference { + return !!node && arkts.isETSTypeReference(node); +} + +function checkIsTypeRefPart(node: arkts.AstNode | undefined): node is arkts.ETSTypeReferencePart { + return !!node && arkts.isETSTypeReferencePart(node); +} + +function checkIsIdentifier(node: arkts.AstNode | undefined): node is arkts.Identifier { + return !!node && arkts.isIdentifier(node); +} + +function checkIsUnionType(node: arkts.AstNode | undefined): node is arkts.ETSUnionType { + return !!node && arkts.isETSUnionType(node); +} + +function checkIsNewClass(node: arkts.AstNode | undefined): node is arkts.ETSNewClassInstanceExpression { + return !!node && arkts.isETSNewClassInstanceExpression(node); +} + +function findTypeRefIdentFromType(node: arkts.AstNode | undefined): arkts.Identifier | undefined { + if (checkIsIdentifier(node)) { + return node; + } + if (checkIsTypeRef(node) && checkIsTypeRefPart(node.part) && checkIsIdentifier(node.part.name)) { + return node.part.name; + } + if (checkIsUnionType(node)) { + return node.types.map(findTypeRefIdentFromType).find((t) => !!t); + } + return undefined; +} diff --git a/arkui-plugins/collectors/ui-collectors/validators/rules/check-componentV2-state-usage.ts b/arkui-plugins/collectors/ui-collectors/validators/rules/check-componentV2-state-usage.ts new file mode 100644 index 000000000..1fe4c298a --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/validators/rules/check-componentV2-state-usage.ts @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { BaseValidator } from '../base'; +import { coerceToAstNode } from '../utils'; +import type { IntrinsicValidatorFunction, ModifiedValidatorFunction } from '../safe-types'; +import { + CallInfo, + CustomComponentInterfacePropertyRecord, + RecordBuilder, + StructMethodInfo, + StructPropertyInfo, +} from '../../records'; +import { checkIsNameStartWithBackingField } from '../../utils'; +import { DecoratorNames, LogType } from '../../../../common/predefines'; +import { createSuggestion, getPositionRangeFromNode } from '../../../../common/log-collector'; + +/** + * 校验规则: + * 1. 确保成员属性或方法不能同时被多个内置装饰器(`@Local`, `@Param`, `@Event`)装饰; + * 2. 当用`@Param`装饰的变量没有被分配默认值时,它也必须用`@Require`装饰; + * 3. 在被`@ComponentV2`装饰的结构体中,`@Require`只能与`@Param`一起使用; + * 4. 检查自定义组件中的`@Local`属性和无装饰器属性是否尝试在外部初始化; + * 5. 确保`@Local`,`@Param`,`@Event`装饰器只能用于成员属性,而不能用于方法。 + * + * 校验等级:error + */ +export function checkComponentV2StateUsage(this: BaseValidator, node: arkts.AstNode): void { + const nodeType = arkts.nodeType(node); + if (checkByType.has(nodeType)) { + checkByType.get(nodeType)!.bind(this)(node); + } +} + +function checkComponentV2StateUsageInClassProperty( + this: BaseValidator, + node: T +): void { + const metadata = this.context ?? {}; + if (!metadata.structInfo?.annotationInfo?.hasComponentV2) { + return; + } + const decorators = findStructAttributeBuiltInDecoratorsFromInfo(metadata); + // 成员属性不能同时被多个内置装饰器(`@Local`, `@Param`, `@Event`)装饰 + if (decorators.length > 1) { + decorators.forEach((info) => + this.report({ + node: info.annotation, + message: `The member property or method cannot be decorated by multiple built-in annotations.`, + level: LogType.ERROR, + }) + ); + } + // 当用`@Param`装饰的变量没有被分配默认值时,它也必须用`@Require`装饰 + if (checkIsParamNotPairedWithRequireFromInfo(metadata)) { + const position = node.startPosition; + this.report({ + node, + message: `When a variable decorated with '@Param' is not assigned a default value, it must also be decorated with '@Require'.`, + level: LogType.ERROR, + suggestion: createSuggestion(`@${DecoratorNames.REQUIRE}`, position, position), + }); + } + // 在被`@ComponentV2`装饰的结构体中,`@Require`只能与`@Param`一起使用 + if (checkIsRequireNotPariedWithParamOrBuilderParam(metadata)) { + const requiredDecorator = metadata.annotations?.[DecoratorNames.REQUIRE]!; + this.report({ + node: requiredDecorator, + message: `In a struct decorated with '@ComponentV2', '@Require' can only be used with '@Param' or '@BuilderParam'.`, + level: LogType.ERROR, + suggestion: createSuggestion('', ...getPositionRangeFromNode(requiredDecorator)), + }); + } +} + +function checkComponentV2StateUsageInStructCall( + this: BaseValidator, + node: T +): void { + const metadata = this.context ?? {}; + if (!metadata.structDeclInfo?.name) { + return; + } + const structName = metadata.structDeclInfo.name; + const call = coerceToAstNode(node); + const optionsArg = call.arguments.at(1); // Options is the second argument of a custom component call. + if (!optionsArg || !arkts.isObjectExpression(optionsArg)) { + return; + } + // 检查自定义组件中的`@Local`属性和无装饰器属性是否尝试在外部初始化 + (optionsArg.properties as arkts.Property[]).forEach((prop) => { + let decl: arkts.AstNode | undefined; + if (!prop.key || !prop.value || !(decl = arkts.getDecl(prop.key)) || !arkts.isMethodDefinition(decl)) { + return; + } + if (checkIsNameStartWithBackingField(decl.name)) { + return; + } + const structInterfacePropRecord = RecordBuilder.build(CustomComponentInterfacePropertyRecord, decl, { + shouldIgnoreDecl: true, + }); // TODO: change back to false; + if (!structInterfacePropRecord.isCollected) { + structInterfacePropRecord.collect(decl); + } + const propertyInfo = structInterfacePropRecord.toRecord(); + let reportDecoratorName: string | undefined; + if (propertyInfo?.annotationInfo?.hasLocal) { + reportDecoratorName = `@${DecoratorNames.LOCAL}`; + } else if (!propertyInfo?.annotationInfo || Object.keys(propertyInfo.annotationInfo).length === 0) { + reportDecoratorName = 'regular'; + } + if (!!reportDecoratorName) { + this.report({ + node: prop, + message: getLocalNeedNoInitReportMessage(reportDecoratorName, decl.name.name, structName), + level: LogType.ERROR, + suggestion: createSuggestion('', ...getPositionRangeFromNode(prop)), + }); + } + }); +} + +function checkComponentV2StateUsageInStructMethod( + this: BaseValidator, + node: T +): void { + const metadata = this.context ?? {}; + if (!metadata.structInfo?.annotationInfo?.hasComponentV2) { + return; + } + const decorators = findStructAttributeBuiltInDecoratorsFromInfo(metadata); + // 确保`@Local`,`@Param`,`@Event`装饰器只能用于成员属性,而不能用于方法 + if (decorators.length > 0) { + decorators.forEach((info) => + this.report({ + node: info.annotation, + message: `'@${info.name}' can only decorate member property.`, + level: LogType.ERROR, + suggestion: createSuggestion('', ...getPositionRangeFromNode(info.annotation)), + }) + ); + } +} + +const checkByType = new Map([ + [arkts.Es2pandaAstNodeType.AST_NODE_TYPE_CLASS_PROPERTY, checkComponentV2StateUsageInClassProperty], + [arkts.Es2pandaAstNodeType.AST_NODE_TYPE_CALL_EXPRESSION, checkComponentV2StateUsageInStructCall], + [arkts.Es2pandaAstNodeType.AST_NODE_TYPE_METHOD_DEFINITION, checkComponentV2StateUsageInStructMethod], +]); + +interface DecoratorInfo { + name: string; + annotation: arkts.AnnotationUsage; +} + +const builtInDecorators: string[] = [DecoratorNames.LOCAL, DecoratorNames.PARAM, DecoratorNames.EVENT]; + +function getLocalNeedNoInitReportMessage(decorator: string, key: string, component: string): string { + return `The '${decorator}' property '${key}' in the custom component '${component}' cannot be initialized here (forbidden to specify).`; +} + +function findStructAttributeBuiltInDecoratorsFromInfo(info: StructPropertyInfo | StructMethodInfo): DecoratorInfo[] { + if (!info.annotationInfo || !info.annotations) { + return []; + } + return builtInDecorators + .filter((name) => !!info.annotationInfo?.[`has${name}`]) + .map((name) => ({ + name, + annotation: info.annotations?.[name]!, + })); +} + +function checkIsParamNotPairedWithRequireFromInfo(info: StructPropertyInfo): boolean { + if (!info.annotationInfo) { + return false; + } + return !!info.annotationInfo.hasParam && !info.annotationInfo.hasRequire; +} + +function checkIsRequireNotPariedWithParamOrBuilderParam(info: StructPropertyInfo): boolean { + if (!info.annotationInfo || !info.annotationInfo.hasRequire) { + return false; + } + return !info.annotationInfo.hasParam && !info.annotationInfo.hasBuilderParam; +} diff --git a/arkui-plugins/collectors/ui-collectors/validators/rules/check-computed-decorator.ts b/arkui-plugins/collectors/ui-collectors/validators/rules/check-computed-decorator.ts new file mode 100644 index 000000000..545783f7d --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/validators/rules/check-computed-decorator.ts @@ -0,0 +1,246 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { BaseValidator } from '../base'; +import { coerceToAstNode } from '../utils'; +import type { ExtendedValidatorFunction, IntrinsicValidatorFunction } from '../safe-types'; +import { + CallInfo, + NormalClassMethodAnnotationRecord, + NormalClassMethodInfo, + NormalClassPropertyInfo, + RecordBuilder, + StructMethodInfo, + StructMethodRecord, + StructPropertyInfo, +} from '../../records'; +import { DecoratorNames, LogType, StructDecoratorNames } from '../../../../common/predefines'; +import { createSuggestion, getPositionRangeFromNode } from '../../../../common/log-collector'; + +/** + * 校验规则: + * 1. `@Computed`装饰器只能用来装饰获取器(即`getter`方法); + * 2. `@Computed`装饰器在已经被`@ObservedV2`装饰器装饰的类`class`中的成员方法上使用; + * 3. `@Computed`装饰器在已经被`@ComponentV2`装饰器修饰的结构体`struct`中使用 + * 4. `@Computed`装饰器修饰的属性不能与双向绑定语法一起使用; + * 5. `@Computed`装饰器修饰的属性不能定义一个设置器(即`setter`方法)。 + * + * 校验等级:error + */ +export function checkComputedDecorator( + this: BaseValidator, + node: arkts.AstNode, + other?: arkts.AstNode +): void { + const nodeType = arkts.nodeType(node); + if (checkByType.has(nodeType)) { + checkByType.get(nodeType)!.bind(this)(node, other); + } +} + +function checkComputedDecoratorInMethod< + T extends arkts.AstNode = arkts.MethodDefinition, + U extends arkts.AstNode = arkts.ClassDefinition +>(this: BaseValidator, node: T, classDecl?: U): void { + const metadata = this.context ?? {}; + const method = coerceToAstNode(node); + const classDef = classDecl ? coerceToAstNode(classDecl) : undefined; + const setter = findSetterMethod(method, metadata); + const getter = findGetterFromSetterMethod(setter, classDef); + if (!!setter && !!getter && checkIsMethodHasComputed(getter)) { + // `@Computed`装饰器修饰的属性不能定义一个设置器(即`setter`方法) + this.report({ + node: setter, + message: `A property decorated by '@Computed' cannot define a set method.`, + level: LogType.ERROR, + suggestion: createSuggestion('', ...getPositionRangeFromNode(setter)), + }); + } + if (!metadata.annotationInfo?.hasComputed) { + return; + } + const computedAnnotation = metadata.annotations?.[DecoratorNames.COMPUTED]!; + if (!checkIsGetterMethod(method, metadata)) { + // `@Computed`装饰器只能用来装饰获取器(即`getter`方法) + this.report({ + node: computedAnnotation, + message: `@${DecoratorNames.COMPUTED} can only decorate 'GetAccessor'.`, + level: LogType.ERROR, + suggestion: createSuggestion('', ...getPositionRangeFromNode(computedAnnotation)), + }); + } else if (!checkIsFromObervedV2InNormalClass(metadata) && !!classDef?.parent) { + // `@Computed`装饰器在已经被`@ObservedV2`装饰器装饰的类`class`中的成员方法上使用 + this.report({ + node: computedAnnotation, + message: `The '@Computed' can decorate only member method within a 'class' decorated with ObservedV2.`, + level: LogType.ERROR, + suggestion: createSuggestion( + `@${DecoratorNames.OBSERVED_V2}`, + ...getPositionRangeFromNode(classDef.parent) + ), + }); + } else if (!checkIsFromComponentV2InStruct(metadata) && !!classDef?.parent) { + // `@Computed`装饰器在已经被`@ComponentV2`装饰器修饰的结构体`struct`中使用 + this.report({ + node: computedAnnotation, + message: `The '@Computed' annotation can only be used in a 'struct' decorated with ComponentV2.`, + level: LogType.ERROR, + suggestion: createSuggestion( + `@${StructDecoratorNames.COMPONENT_V2}`, + ...getPositionRangeFromNode(classDef.parent) + ), + }); + } +} + +function checkComputedDecoratorInProperty( + this: BaseValidator, + node: T +): void { + const metadata = this.context ?? {}; + // Since property cannot have `@Computed`, it is an ignored annotation. + if (!metadata.ignoredAnnotationInfo?.hasComputed) { + return; + } + const computedAnnotation = metadata.ignoredAnnotations?.[DecoratorNames.COMPUTED]!; + // `@Computed`装饰器只能用来装饰获取器(即`getter`方法) + this.report({ + node: computedAnnotation, + message: `@${DecoratorNames.COMPUTED} can only decorate 'GetAccessor'.`, + level: LogType.ERROR, + suggestion: createSuggestion('', ...getPositionRangeFromNode(computedAnnotation)), + }); +} + +function checkComputedDecoratorInStructCall( + this: BaseValidator, + node: T +): void { + const metadata = this.context ?? {}; + if (!metadata.structDeclInfo?.name) { + return; + } + const call = coerceToAstNode(node); + const optionsArg = call.arguments.at(1); // Options is the second argument of a custom component call. + if (!optionsArg || !arkts.isObjectExpression(optionsArg)) { + return; + } + // `@Computed`装饰器修饰的属性不能与双向绑定语法一起使用 + (optionsArg.properties as arkts.Property[]).forEach((prop) => { + if (!prop.key || !prop.value || !arkts.isCallExpression(prop.value)) { + return; + } + const computedArgs = findComputedArgInBindablePropertyValue(prop.value.arguments); + computedArgs.forEach((node) => { + this.report({ + node, + message: `A property decorated by '@Computed' cannot be used with two-way bind syntax.`, + level: LogType.ERROR, + suggestion: createSuggestion('', ...getPositionRangeFromNode(node)), + }); + }); + }); +} + +const checkByType = new Map([ + [arkts.Es2pandaAstNodeType.AST_NODE_TYPE_METHOD_DEFINITION, checkComputedDecoratorInMethod], + [arkts.Es2pandaAstNodeType.AST_NODE_TYPE_CLASS_PROPERTY, checkComputedDecoratorInProperty], + [arkts.Es2pandaAstNodeType.AST_NODE_TYPE_CALL_EXPRESSION, checkComputedDecoratorInStructCall], +]); + +type MethodInfo = StructMethodInfo & NormalClassMethodInfo; + +type PropertyInfo = StructPropertyInfo & NormalClassPropertyInfo; + +function findComputedArgInBindablePropertyValue(args: readonly arkts.Expression[]): readonly arkts.Expression[] { + return args.filter((arg) => { + if (!arkts.isMemberExpression(arg) || !arkts.isThisExpression(arg.object)) { + return false; + } + const decl = arkts.getPeerDecl(arg.property.peer); + if (!decl || !arkts.isMethodDefinition(decl)) { + return false; + } + const structMethodRecord = RecordBuilder.build(StructMethodRecord, decl, { shouldIgnoreDecl: true }); // TODO: change back to false; + if (!structMethodRecord.isCollected) { + structMethodRecord.collect(decl); + } + const methodInfo = structMethodRecord.toRecord(); + return !!methodInfo?.annotationInfo?.hasComputed; + }); +} + +function checkIsFromComponentV2InStruct(info: MethodInfo | PropertyInfo): boolean { + if (!info.structInfo && !!info.classInfo) { + return true; // Skip checking. This implies from normal class rather than from struct. + } + return !!info.structInfo?.annotationInfo?.hasComponentV2; +} + +function checkIsFromObervedV2InNormalClass(info: MethodInfo | PropertyInfo): boolean { + if (!info.classInfo && !!info.structInfo) { + return true; // Skip checking. This implies from struct rather than from normal class. + } + return !!info.classInfo?.annotationInfo?.hasObservedV2; +} + +function checkIsGetterMethod(node: arkts.MethodDefinition, info?: MethodInfo): boolean { + if (info?.kind === arkts.Es2pandaMethodDefinitionKind.METHOD_DEFINITION_KIND_GET) { + return true; + } + let isGetter: boolean = node.kind === arkts.Es2pandaMethodDefinitionKind.METHOD_DEFINITION_KIND_GET; + if (!isGetter) { + isGetter = node.overloads.some((method) => checkIsGetterMethod(method)); + } + return isGetter; +} + +function findSetterMethod(node: arkts.MethodDefinition, info?: MethodInfo): arkts.MethodDefinition | undefined { + if (info?.kind === arkts.Es2pandaMethodDefinitionKind.METHOD_DEFINITION_KIND_SET) { + return node; + } + let isSetter: boolean = node.kind === arkts.Es2pandaMethodDefinitionKind.METHOD_DEFINITION_KIND_SET; + if (isSetter) { + return node; + } + return node.overloads.find((method) => findSetterMethod(method)); +} + +function findGetterFromSetterMethod( + setter: arkts.MethodDefinition | undefined, + classDecl: arkts.ClassDefinition | undefined +): arkts.MethodDefinition | undefined { + if (!setter || !classDecl) { + return undefined; + } + const getterInOverload = setter.overloads.find((method) => checkIsGetterMethod(method)); + if (!!getterInOverload) { + return getterInOverload; + } + const name = setter.name.name; + return classDecl.body.find( + (st) => arkts.isMethodDefinition(st) && checkIsGetterMethod(st) && st.name.name === name + ) as arkts.MethodDefinition | undefined; +} + +function checkIsMethodHasComputed(node: arkts.MethodDefinition): boolean { + const annotationRecord = new NormalClassMethodAnnotationRecord({ shouldIgnoreDecl: true }); // TODO: change back to false; + for (const annotation of node.scriptFunction.annotations) { + annotationRecord.collect(annotation); + } + const annotationInfo = annotationRecord.toRecord(); + return !!annotationInfo?.annotationInfo?.hasComputed; +} diff --git a/arkui-plugins/collectors/ui-collectors/validators/rules/index.ts b/arkui-plugins/collectors/ui-collectors/validators/rules/index.ts new file mode 100644 index 000000000..2464e904d --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/validators/rules/index.ts @@ -0,0 +1,20 @@ +/* + * 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 * from './check-builder-param'; +export * from './check-component-link-init'; +export * from './check-component-v2-mix-use'; +export * from './check-componentV2-state-usage'; +export * from './check-computed-decorator'; \ No newline at end of file diff --git a/arkui-plugins/collectors/ui-collectors/validators/safe-types.ts b/arkui-plugins/collectors/ui-collectors/validators/safe-types.ts new file mode 100644 index 000000000..2d69f1717 --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/validators/safe-types.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 * as arkts from '@koalaui/libarkts'; +import { BaseValidator } from './base'; + +export type IntrinsicValidatorFunction = (this: BaseValidator, node: T) => void; + +export type ModifiedValidatorFunction = ( + this: BaseValidator, + node: U +) => void; + +export type ExtendedValidatorFunction = ( + this: BaseValidator, + node: T, + other?: U +) => void; \ No newline at end of file diff --git a/arkui-plugins/collectors/ui-collectors/validators/struct-call-validator.ts b/arkui-plugins/collectors/ui-collectors/validators/struct-call-validator.ts new file mode 100644 index 000000000..aea14cf1f --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/validators/struct-call-validator.ts @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { BaseValidator } from './base'; +import { checkBuilderParam, checkComponentLinkInit, checkComponentV2StateUsage, checkComputedDecorator } from './rules'; +import { CallInfo } from '../records'; +import { checkIsCustomComponentFromInfo } from '../utils'; + +export class StructCallValidator extends BaseValidator { + reportIfViolated(node: arkts.CallExpression): void { + const metadata = this.context ?? {}; + // 只处理自定义组件 CallExpression的场景 + if (!checkIsCustomComponentFromInfo(metadata.structDeclInfo) || !metadata.structDeclInfo?.definitionPtr) { + return; + } + + checkComponentV2StateUsage.bind(this)(node); + checkComputedDecorator.bind(this)(node); + + const struct = arkts.classByPeer(metadata.structDeclInfo.definitionPtr); + checkBuilderParam.bind(this)(struct); + checkComponentLinkInit.bind(this)(struct); + } +} diff --git a/arkui-plugins/collectors/ui-collectors/validators/struct-method-validator.ts b/arkui-plugins/collectors/ui-collectors/validators/struct-method-validator.ts new file mode 100644 index 000000000..b2368def7 --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/validators/struct-method-validator.ts @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { BaseValidator } from './base'; +import { StructMethodInfo } from '../records'; +import { checkComponentV2StateUsage, checkComputedDecorator } from './rules'; + +export class StructMethodValidator extends BaseValidator { + reportIfViolated(node: arkts.MethodDefinition): void { + const metadata = this.context ?? {}; + if (!metadata.structInfo?.definitionPtr) { + return; + } + + checkComponentV2StateUsage.bind(this)(node); + + const struct = arkts.classByPeer(metadata.structInfo.definitionPtr); + checkComputedDecorator.bind(this)(node, struct); + } +} diff --git a/arkui-plugins/collectors/ui-collectors/validators/struct-property-validator.ts b/arkui-plugins/collectors/ui-collectors/validators/struct-property-validator.ts new file mode 100644 index 000000000..b07032dbb --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/validators/struct-property-validator.ts @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { BaseValidator } from './base'; +import { StructPropertyInfo } from '../records'; +import { checkComponentV2MixUse, checkComponentV2StateUsage, checkComputedDecorator } from './rules'; + +export class StructPropertyValidator extends BaseValidator { + reportIfViolated(node: arkts.ClassProperty): void { + const metadata = this.context ?? {}; + if (!metadata.structInfo?.definitionPtr) { + return; + } + + checkComponentV2MixUse.bind(this)(node); + checkComponentV2StateUsage.bind(this)(node); + + const struct = arkts.classByPeer(metadata.structInfo.definitionPtr); + checkComputedDecorator.bind(this)(node, struct); + } +} diff --git a/arkui-plugins/collectors/ui-collectors/validators/utils.ts b/arkui-plugins/collectors/ui-collectors/validators/utils.ts new file mode 100644 index 000000000..2a3cf8c22 --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/validators/utils.ts @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { getAnnotationName } from '../utils'; + +export function isAnnotatedProperty(node: arkts.AstNode, annotationName: string, ignoreDecl: boolean = false): node is arkts.ClassProperty { + if (!arkts.isClassProperty(node)) { + return false; + } + return !!getAnnotationByName(node.annotations, annotationName, ignoreDecl); +} + +export function getAnnotationByName( + annotations: readonly arkts.AnnotationUsage[], + name: string, + ignoreDecl: boolean = false +): arkts.AnnotationUsage | undefined { + return annotations.find((annotation: arkts.AnnotationUsage): boolean => { + return getAnnotationName(annotation, ignoreDecl) === name; + }); +} + +export function coerceToAstNode(node: arkts.AstNode): T { + return node as T; +} diff --git a/arkui-plugins/collectors/ui-collectors/validators/validator-builder.ts b/arkui-plugins/collectors/ui-collectors/validators/validator-builder.ts new file mode 100644 index 000000000..1fd84c369 --- /dev/null +++ b/arkui-plugins/collectors/ui-collectors/validators/validator-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. + */ + +import { Validator } from './base'; +import { clearValidatorCache, getOrPut } from './cache'; + +export class ValidatorBuilder { + static build(Validator: { name: string; new (): Validator }): Validator { + return getOrPut(Validator.name, () => new Validator()); + } + + static reset(): void { + clearValidatorCache(); + } +} diff --git a/arkui-plugins/common/arkts-utils.ts b/arkui-plugins/common/arkts-utils.ts index 59016f2fd..76fcc2b08 100644 --- a/arkui-plugins/common/arkts-utils.ts +++ b/arkui-plugins/common/arkts-utils.ts @@ -66,7 +66,7 @@ export function isDecoratorAnnotation( if (!decl) { return false; } - const moduleName: string = arkts.getProgramFromAstNode(decl).moduleName; + const moduleName = arkts.getProgramFromAstNode(decl)?.moduleName; if (!moduleName || !matchPrefix(ARKUI_IMPORT_PREFIX_NAMES, moduleName)) { return false; } @@ -82,24 +82,6 @@ export function removeAnnotationByName( return annotations.filter((it) => !isAnnotation(it, annoName)); } -export function expectName(node: arkts.AstNode | undefined): string { - if (!node) { - throw new Error('Expected an identifier, got empty node'); - } - if (!arkts.isIdentifier(node)) { - throw new Error('Expected an identifier, got: ' + arkts.nodeType(node).toString()); - } - return node.name; -} - -export function mangle(value: string): string { - return `__${value}`; -} - -export function backingField(originalName: string): string { - return mangle(`backing_${originalName}`); -} - export function filterDefined(value: (T | undefined)[]): T[] { return value.filter((it: T | undefined): it is T => it != undefined); } diff --git a/arkui-plugins/common/declaration-collector.ts b/arkui-plugins/common/declaration-collector.ts index d65f0d40d..9024810bc 100644 --- a/arkui-plugins/common/declaration-collector.ts +++ b/arkui-plugins/common/declaration-collector.ts @@ -62,7 +62,7 @@ export class DeclarationCollector { if (!declName) { return; } - let sourceName: string = arkts.getProgramFromAstNode(decl).moduleName; + let sourceName: string = arkts.getProgramFromAstNode(decl)?.moduleName; this.fromExternalSourceNameMap.set(declName, sourceName); this.fromExternalSourceNodePeerMap.set(decl.peer, sourceName); diff --git a/arkui-plugins/common/log-collector.ts b/arkui-plugins/common/log-collector.ts index 5fa9ed7af..775265282 100644 --- a/arkui-plugins/common/log-collector.ts +++ b/arkui-plugins/common/log-collector.ts @@ -16,20 +16,59 @@ import * as arkts from '@koalaui/libarkts'; import { LogType } from './predefines'; -interface LogInfo { - type: LogType; - message: string; - node: arkts.AstNode; +export interface SuggestionOptions { code: string; + range: [start: arkts.SourcePosition, end: arkts.SourcePosition]; + args?: string[]; +} + +export interface LogInfo { + node: arkts.AstNode; + level: LogType; + message: string; + args?: string[]; + position?: arkts.SourcePosition; + suggestion?: SuggestionOptions; + code?: string; +} + +export function createSuggestion( + code: string, + rangeStart: arkts.SourcePosition, + rangeEnd: arkts.SourcePosition, + ...args: string[] +): SuggestionOptions { + return { code, range: [rangeStart, rangeEnd], args }; +} + +export function getPositionRangeFromNode(node: arkts.AstNode): [arkts.SourcePosition, arkts.SourcePosition] { + return [node.startPosition, node.endPosition]; } export function generateDiagnosticKind(logItem: LogInfo): arkts.DiagnosticKind { - return arkts.DiagnosticKind.create( - `${logItem.code}: ${logItem.message}`, - logItem.type === LogType.ERROR + const message: string = !!logItem.code ? `${logItem.code}: ${logItem.message}` : logItem.message; + const level: arkts.PluginDiagnosticType = + logItem.level === LogType.ERROR ? arkts.PluginDiagnosticType.ES2PANDA_PLUGIN_ERROR - : arkts.PluginDiagnosticType.ES2PANDA_PLUGIN_WARNING - ); + : arkts.PluginDiagnosticType.ES2PANDA_PLUGIN_WARNING; + return arkts.DiagnosticKind.create(message, level); +} + +export function generateDiagnosticInfo(logItem: LogInfo): arkts.DiagnosticInfo { + const diagnosticArgs = logItem.args ?? []; + const diagnosticKind = generateDiagnosticKind(logItem); + return arkts.DiagnosticInfo.create(diagnosticKind, ...diagnosticArgs); +} + +export function generateSuggestionInfo(suggestion: SuggestionOptions, message: string): arkts.SuggestionInfo { + const suggestionArgs = suggestion.args ?? []; + const suggestionKind = arkts.DiagnosticKind.create(message, arkts.PluginDiagnosticType.ES2PANDA_PLUGIN_SUGGESTION); + return arkts.SuggestionInfo.create(suggestionKind, suggestion.code, ...suggestionArgs); +} + +export function generateSuggestionRange(suggestion: SuggestionOptions): arkts.SourceRange { + const [startPosition, endPosition] = suggestion.range; + return arkts.SourceRange.create(startPosition, endPosition); } export class LogCollector { @@ -49,6 +88,21 @@ export class LogCollector { return this.instance; } + private reportDiagnostic(log: LogInfo): void { + const args = log.args ?? []; + const position = log.position ?? arkts.getStartPosition(log.node); + const suggestion = log.suggestion; + if (!suggestion) { + const kind = generateDiagnosticKind(log); + arkts.Diagnostic.logDiagnostic(kind, position, ...args); + } else { + const info = generateDiagnosticInfo(log); + const suggestionInfo = generateSuggestionInfo(suggestion, log.message); + const suggestionRange = generateSuggestionRange(suggestion); + arkts.Diagnostic.logDiagnosticWithSuggestion(info, suggestionInfo, suggestionRange); + } + } + reset(): void { this.logInfos = []; this.ignoreError = false; @@ -63,7 +117,7 @@ export class LogCollector { return; } this.logInfos.forEach((logItem: LogInfo) => { - arkts.Diagnostic.logDiagnostic(generateDiagnosticKind(logItem), arkts.getStartPosition(logItem.node)); + this.reportDiagnostic(logItem); }); } diff --git a/arkui-plugins/common/predefines.ts b/arkui-plugins/common/predefines.ts index 87bdd9413..df1e63e11 100644 --- a/arkui-plugins/common/predefines.ts +++ b/arkui-plugins/common/predefines.ts @@ -27,11 +27,7 @@ export const EXTERNAL_SOURCE_PREFIX_NAMES: (string | RegExp)[] = [ /ability\..*/, ]; -export const EXTERNAL_SOURCE_PREFIX_NAMES_FOR_FRAMEWORK: (string | RegExp)[] = [ - 'std', - 'escompat', - /@arkts\..*/ -]; +export const EXTERNAL_SOURCE_PREFIX_NAMES_FOR_FRAMEWORK: (string | RegExp)[] = ['std', 'escompat', /@arkts\..*/]; export const ARKUI_IMPORT_PREFIX_NAMES: (string | RegExp)[] = [/arkui\..*/, /@ohos\..*/, /@kit\..*/]; @@ -90,13 +86,13 @@ export enum EntryWrapperNames { REGISTER_NAMED_ROUTER = 'RegisterNamedRouter', ROUTER_NAME = 'routerName', INSTANCE = 'instance', - PARAM = 'param' + PARAM = 'param', } export enum EntryParamNames { ENTRY_STORAGE = 'storage', ENTRY_USE_SHARED_STORAGE = 'useSharedStorage', - ENTRY_ROUTE_NAME = 'routeName' + ENTRY_ROUTE_NAME = 'routeName', } export enum InnerComponentNames { @@ -113,6 +109,7 @@ export enum DecoratorNames { CONSUME = 'Consume', OBJECT_LINK = 'ObjectLink', OBSERVED = 'Observed', + OBSERVED_V2 = 'ObservedV2', WATCH = 'Watch', BUILDER_PARAM = 'BuilderParam', BUILDER = 'Builder', @@ -123,11 +120,14 @@ export enum DecoratorNames { TRACK = 'Track', JSONSTRINGIFYIGNORE = 'JSONStringifyIgnore', JSONRENAME = 'JSONRename', - ANIMATABLE_EXTEND = 'AnimatableExtend' -} - -export enum DecoratorIntrinsicNames { - LINK = '__Link_intrinsic', + ANIMATABLE_EXTEND = 'AnimatableExtend', + PROP_REF = 'PropRef', + STORAGE_PROP_REF = 'StoragePropRef', + LOCAL = 'Local', + PARAM = 'Param', + EVENT = 'Event', + REQUIRE = 'Require', + COMPUTED = 'Computed', } export enum StateManagementTypes { @@ -149,6 +149,7 @@ export enum StateManagementTypes { RENDER_ID_TYPE = 'RenderIdType', OBSERVE = 'OBSERVE', META = '__meta', + BACKING = '__backing', SUBSCRIBED_WATCHES = 'ISubscribedWatches', STORAGE_LINK_STATE = 'StorageLinkState', OBSERVABLE_PROXY = 'observableProxy', @@ -184,6 +185,34 @@ export enum NavigationNames { INTEGRATED_HSP = 'integratedHsp', } +export enum CustomComponentNames { + COMPONENT_BUILD_ORI = 'build', + COMPONENT_CONSTRUCTOR_ORI = 'constructor', + COMPONENT_CLASS_NAME = 'CustomComponent', + COMPONENT_V2_CLASS_NAME = 'CustomComponentV2', + COMPONENT_INTERFACE_PREFIX = '__Options_', + COMPONENT_INITIALIZE_STRUCT = '__initializeStruct', + COMPONENT_UPDATE_STRUCT = '__updateStruct', + COMPONENT_INITIALIZERS_NAME = 'initializers', + BUILD_COMPATIBLE_NODE = '_buildCompatibleNode', + OPTIONS = 'options', + PAGE_LIFE_CYCLE = 'PageLifeCycle', + LAYOUT_CALLBACK = 'LayoutCallback', + CUSTOMDIALOG_ANNOTATION_NAME = 'CustomDialog', + CUSTOMDIALOG_CONTROLLER = 'CustomDialogController', + CUSTOMDIALOG_CONTROLLER_OPTIONS = 'CustomDialogControllerOptions', + SETDIALOGCONTROLLER_METHOD = '__setDialogController__', +} + +export enum BuilderLambdaNames { + ANNOTATION_NAME = 'ComponentBuilder', + ORIGIN_METHOD_NAME = '$_instantiate', + TRANSFORM_METHOD_NAME = '_instantiateImpl', + STYLE_PARAM_NAME = 'style', + STYLE_ARROW_PARAM_NAME = 'instance', + CONTENT_PARAM_NAME = 'content' +} + export const RESOURCE_TYPE: Record = { color: 10001, float: 10002, @@ -217,7 +246,14 @@ export const INTERMEDIATE_IMPORT_SOURCE: Map = new Map = new Map = new Map = new Map [StateManagementTypes.STORAGE_LINK_STATE, 'arkui.stateManagement.runtime'], [StateManagementTypes.OBSERVABLE_PROXY, 'arkui.stateManagement.runtime'], [StateManagementTypes.PROP_STATE, 'arkui.stateManagement.runtime'], - [AnimationNames.ANIMATABLE_ARITHMETIC, 'arkui.component.common'] + [AnimationNames.ANIMATABLE_ARITHMETIC, 'arkui.component.common'], ]); export enum GetSetTypes { @@ -279,7 +315,20 @@ export enum GetSetTypes { SET = 'set', } -export enum GenSymPrefix { - INTRINSIC = 'gensym%%', - UI = 'gensym__' +export enum NodeCacheNames { + MEMO = 'memo', + UI = 'ui', +} + +export enum BuiltInNames { + GENSYM_INTRINSIC_PREFIX = 'gensym%%', + GENSYM_UI_PREFIX = 'gensym__', + ETS_GLOBAL_CLASS = 'ETSGLOBAL', + GLOBAL_INIT_METHOD = '_$init$_', + GLOBAL_MAIN_METHOD = 'main', + IMPLEMENT_PROPETY_PREFIX = '' +} + +export enum ArkUIDeclInterfaceNames { + COMMON_METHOD = 'CommonMethod' } \ No newline at end of file diff --git a/arkui-plugins/common/safe-types.ts b/arkui-plugins/common/safe-types.ts index 899ef0c39..87345cf19 100644 --- a/arkui-plugins/common/safe-types.ts +++ b/arkui-plugins/common/safe-types.ts @@ -40,4 +40,6 @@ export type PickNested = { [P in keyof T]: P extends K ? T[P] : T[P] extends object ? NestedKey : T[P]; }; -export type PartialNestedExcept = PartialNested> & PickNested; \ No newline at end of file +export type PartialNestedExcept = PartialNested> & PickNested; + +export type AstNodePointer = arkts.AstNode['peer']; \ No newline at end of file diff --git a/arkui-plugins/memo-plugins/function-transformer.ts b/arkui-plugins/memo-plugins/function-transformer.ts index 4f2c55f1f..6caed7d5d 100644 --- a/arkui-plugins/memo-plugins/function-transformer.ts +++ b/arkui-plugins/memo-plugins/function-transformer.ts @@ -56,6 +56,7 @@ import { SignatureTransformer } from './signature-transformer'; import { moveToFront } from '../common/arkts-utils'; import { InternalsTransformer } from './internal-transformer'; import { CachedMetadata, rewriteByType } from './memo-cache-factory'; +import { NodeCacheNames } from '../common/predefines'; interface ScopeInfo extends MemoInfo { regardAsSameScope?: boolean; @@ -683,8 +684,8 @@ export class FunctionTransformer extends AbstractVisitor { private visitorWithCache(beforeChildren: arkts.AstNode): arkts.AstNode { const node = this.visitEachChild(beforeChildren); - if (arkts.NodeCache.getInstance().has(node)) { - const value = arkts.NodeCache.getInstance().get(node)!; + if (arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).has(node)) { + const value = arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).get(node)!; if (rewriteByType.has(value.type)) { this.modified = true; const metadata: CachedMetadata = { ...value.metadata, internalsTransformer: this.internalsTransformer }; diff --git a/arkui-plugins/memo-plugins/index.ts b/arkui-plugins/memo-plugins/index.ts index 5c19bc841..50593486b 100644 --- a/arkui-plugins/memo-plugins/index.ts +++ b/arkui-plugins/memo-plugins/index.ts @@ -20,7 +20,7 @@ import { PositionalIdTracker } from './utils'; import { ReturnTransformer } from './return-transformer'; import { ParameterTransformer } from './parameter-transformer'; import { ProgramVisitor } from '../common/program-visitor'; -import { EXTERNAL_SOURCE_PREFIX_NAMES, EXTERNAL_SOURCE_PREFIX_NAMES_FOR_FRAMEWORK } from '../common/predefines'; +import { EXTERNAL_SOURCE_PREFIX_NAMES, EXTERNAL_SOURCE_PREFIX_NAMES_FOR_FRAMEWORK, NodeCacheNames } from '../common/predefines'; import { debugDump, debugLog, getDumpFileName } from '../common/debug'; import { SignatureTransformer } from './signature-transformer'; import { InternalsTransformer } from './internal-transformer'; @@ -31,6 +31,7 @@ export function unmemoizeTransform(): Plugins { checked: checkedTransform, clean() { arkts.arktsGlobal.clearContext(); + arkts.NodeCacheFactory.getInstance().clear(); }, }; } @@ -95,6 +96,7 @@ function checkedProgramVisit( debugLog('[SKIP PHASE] phase: memo-checked, moduleName: ', program.moduleName); } else { debugLog('[CANT SKIP PHASE] phase: memo-checked, moduleName: ', program.moduleName); + // arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).visualize(); const positionalIdTracker = new PositionalIdTracker(arkts.getFileName(), false); const parameterTransformer = new ParameterTransformer({ positionalIdTracker }); const returnTransformer = new ReturnTransformer(); @@ -109,7 +111,7 @@ function checkedProgramVisit( returnTransformer, signatureTransformer, internalsTransformer, - useCache: arkts.NodeCache.getInstance().isCollected(), + useCache: arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).isCollected(), }); const skipPrefixNames = isFrameworkMode ? EXTERNAL_SOURCE_PREFIX_NAMES_FOR_FRAMEWORK @@ -122,7 +124,7 @@ function checkedProgramVisit( pluginContext, }); program = programVisitor.programVisitor(program); - arkts.NodeCache.getInstance().clear(); + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).clear(); } return program; } diff --git a/arkui-plugins/memo-plugins/memo-cache-factory.ts b/arkui-plugins/memo-plugins/memo-cache-factory.ts index e8151cb31..56e2d9469 100644 --- a/arkui-plugins/memo-plugins/memo-cache-factory.ts +++ b/arkui-plugins/memo-plugins/memo-cache-factory.ts @@ -28,9 +28,19 @@ import { PositionalIdTracker, } from './utils'; import { InternalsTransformer } from './internal-transformer'; -import { GenSymPrefix } from '../common/predefines'; +import { BuiltInNames } from '../common/predefines'; -export interface CachedMetadata extends arkts.AstNodeCacheValueMetadata { +export interface CacheValueMetadata { + callName?: string; + hasReceiver?: boolean; + isSetter?: boolean; + isGetter?: boolean; + hasMemoSkip?: boolean; + hasMemoIntrinsic?: boolean; + hasMemoEntry?: boolean; +} + +export interface CachedMetadata extends CacheValueMetadata { internalsTransformer?: InternalsTransformer; } @@ -108,7 +118,7 @@ export class RewriteFactory { static rewriteArrowFunction( node: arkts.ArrowFunctionExpression, - metadata?: arkts.AstNodeCacheValueMetadata, + metadata?: CachedMetadata, expectReturn?: arkts.TypeNode ): arkts.ArrowFunctionExpression { return arkts.factory.updateArrowFunction( @@ -264,7 +274,7 @@ export class RewriteFactory { node: arkts.Identifier, metadata?: CachedMetadata ): arkts.Identifier | arkts.MemberExpression { - if (!node.name.startsWith(GenSymPrefix.INTRINSIC) && !node.name.startsWith(GenSymPrefix.UI)) { + if (!node.name.startsWith(BuiltInNames.GENSYM_INTRINSIC_PREFIX) && !node.name.startsWith(BuiltInNames.GENSYM_UI_PREFIX)) { return factory.createMemoParameterAccess(node.name); } return node; diff --git a/arkui-plugins/ui-plugins/builder-lambda-translators/factory.ts b/arkui-plugins/ui-plugins/builder-lambda-translators/factory.ts index 4aede6309..21f333e52 100644 --- a/arkui-plugins/ui-plugins/builder-lambda-translators/factory.ts +++ b/arkui-plugins/ui-plugins/builder-lambda-translators/factory.ts @@ -14,9 +14,7 @@ */ import * as arkts from '@koalaui/libarkts'; -import { BuilderLambdaNames } from '../utils'; import { - backingField, filterDefined, isDecoratorAnnotation, removeAnnotationByName, @@ -45,11 +43,18 @@ import { buildSecondLastArgInfo, checkIsSpecialComponentAttributeFromType, } from './utils'; -import { isDecoratorIntrinsicAnnotation } from '../property-translators/utils'; +import { backingField, checkIsNameStartWithBackingField } from '../property-translators/utils'; import { factory as PropertyFactory } from '../property-translators/factory'; -import { AnimationNames, BindableDecl, DecoratorIntrinsicNames, DecoratorNames } from '../../common/predefines'; +import { + AnimationNames, + BindableDecl, + BuilderLambdaNames, + DecoratorNames, + NodeCacheNames, +} from '../../common/predefines'; import { ImportCollector } from '../../common/import-collector'; import { addMemoAnnotation, collectMemoableInfoInParameter } from '../../collectors/memo-collectors/utils'; +import { factory as MemoCollectFactory } from '../../collectors/memo-collectors/factory'; export class factory { /** @@ -205,7 +210,7 @@ export class factory { ); const returnStatement = arkts.factory.createReturnStatement(); - arkts.NodeCache.getInstance().collect(returnStatement); + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).collect(returnStatement); const body: arkts.BlockStatement = arkts.factory.createBlock([ arkts.factory.createExpressionStatement(lambdaBody), returnStatement, @@ -254,7 +259,7 @@ export class factory { arkts.factory.createIdentifier(BuilderLambdaNames.STYLE_PARAM_NAME, optionalFuncType), undefined ); - arkts.NodeCache.getInstance().collect(parameter); + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).collect(parameter); return parameter; } @@ -297,7 +302,10 @@ export class factory { if (!expr) { return arg; } - const properties = (expr.properties as arkts.Property[]).map((p) => factory.updatePropertiesInOptions(p)); + const properties = (expr.properties as arkts.Property[]).map((p) => { + MemoCollectFactory.findAndCollectMemoableProperty(p); + return factory.updatePropertiesInOptions(p); + }); const updatedExpr: arkts.ObjectExpression = arkts.ObjectExpression.updateObjectExpression( expr, arkts.Es2pandaAstNodeType.AST_NODE_TYPE_OBJECT_EXPRESSION, @@ -317,11 +325,12 @@ export class factory { } const returnType: arkts.TypeNode | undefined = decl.scriptFunction.returnTypeAnnotation; const isBindable: boolean = !!returnType && hasBindableProperty(returnType, BindableDecl.BINDABLE); - let isBuilderParam: boolean = false; - let isLinkIntrinsic: boolean = false; + const isNotBacking: boolean = !checkIsNameStartWithBackingField(decl.name); + let isBuilderParam: boolean = isNotBacking; + let isLink: boolean = isNotBacking; decl.scriptFunction.annotations.forEach((anno) => { - isBuilderParam ||= isDecoratorAnnotation(anno, DecoratorNames.BUILDER_PARAM); - isLinkIntrinsic ||= isDecoratorIntrinsicAnnotation(anno, DecoratorIntrinsicNames.LINK); + isBuilderParam &&= isDecoratorAnnotation(anno, DecoratorNames.BUILDER_PARAM); + isLink &&= isDecoratorAnnotation(anno, DecoratorNames.LINK); }); if (isBindable && isDoubleDollarCall(prop.value)) { @@ -330,7 +339,7 @@ export class factory { addMemoAnnotation(prop.value); return prop; } else if ( - isLinkIntrinsic && + isLink && arkts.isIdentifier(prop.key) && arkts.isMemberExpression(prop.value) && arkts.isThisExpression(prop.value.object) && @@ -512,7 +521,7 @@ export class factory { replaceBuilderLambdaDeclMethodName(node.name.name), externalSourceName ).setOverloads(newOverloads); - arkts.NodeCache.getInstance().collect(newNode); + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).collect(newNode); return newNode; } @@ -595,7 +604,7 @@ export class factory { instanceCalls = instanceCalls.reverse(); this.updateAnimation(instanceCalls); lambdaBody = arkts.factory.createIdentifier(BuilderLambdaNames.STYLE_ARROW_PARAM_NAME); - arkts.NodeCache.getInstance().collect(lambdaBody); + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).collect(lambdaBody); instanceCalls.forEach((callInfo) => { lambdaBody = this.createStyleLambdaBody(lambdaBody!, callInfo); }); @@ -603,7 +612,7 @@ export class factory { const args: (arkts.AstNode | undefined)[] = this.generateArgsInBuilderLambda(leaf, lambdaBody!, declInfo); const newNode = arkts.factory.updateCallExpression(node, replace, leaf.typeArguments, filterDefined(args)); - arkts.NodeCache.getInstance().collect(newNode); + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).collect(newNode); return newNode; } diff --git a/arkui-plugins/ui-plugins/builder-lambda-translators/utils.ts b/arkui-plugins/ui-plugins/builder-lambda-translators/utils.ts index f4975507f..1cd5da770 100644 --- a/arkui-plugins/ui-plugins/builder-lambda-translators/utils.ts +++ b/arkui-plugins/ui-plugins/builder-lambda-translators/utils.ts @@ -15,9 +15,15 @@ import * as arkts from '@koalaui/libarkts'; import { isAnnotation, matchPrefix } from '../../common/arkts-utils'; -import { BuilderLambdaNames, isCustomComponentAnnotation } from '../utils'; +import { isCustomComponentAnnotation } from '../utils'; import { DeclarationCollector } from '../../common/declaration-collector'; -import { ARKUI_IMPORT_PREFIX_NAMES, BindableDecl, Dollars, StructDecoratorNames } from '../../common/predefines'; +import { + ARKUI_IMPORT_PREFIX_NAMES, + BindableDecl, + BuilderLambdaNames, + Dollars, + StructDecoratorNames, +} from '../../common/predefines'; import { ImportCollector } from '../../common/import-collector'; export type BuilderLambdaDeclInfo = { @@ -300,7 +306,7 @@ export function findBuilderLambdaDecl(node: arkts.CallExpression | arkts.Identif if (!decl) { return undefined; } - const moduleName: string = arkts.getProgramFromAstNode(decl).moduleName; + const moduleName: string = arkts.getProgramFromAstNode(decl)?.moduleName; if (!moduleName) { return undefined; } @@ -328,7 +334,7 @@ export function findBuilderLambdaDeclInfo(decl: arkts.AstNode | undefined): Buil if (!decl) { return undefined; } - const moduleName: string = arkts.getProgramFromAstNode(decl).moduleName; + const moduleName: string = arkts.getProgramFromAstNode(decl)?.moduleName; if (!moduleName) { return undefined; } @@ -476,7 +482,7 @@ export function isDoubleDollarCall( if (!decl) { return false; } - const moduleName: string = arkts.getProgramFromAstNode(decl).moduleName; + const moduleName: string = arkts.getProgramFromAstNode(decl)?.moduleName; if (!moduleName || !matchPrefix(ARKUI_IMPORT_PREFIX_NAMES, moduleName)) { return false; } diff --git a/arkui-plugins/ui-plugins/component-transformer.ts b/arkui-plugins/ui-plugins/component-transformer.ts index 6c9642d8e..61a9bb356 100644 --- a/arkui-plugins/ui-plugins/component-transformer.ts +++ b/arkui-plugins/ui-plugins/component-transformer.ts @@ -27,24 +27,19 @@ import { isComponentStruct, } from './utils'; import { - backingField, - expectName, - annotation, filterDefined, collect, createAndInsertImportDeclaration, - isDecoratorAnnotation, } from '../common/arkts-utils'; import { ProjectConfig } from '../common/plugin-context'; import { getEntryParams } from './entry-translators/utils'; import { factory as entryFactory } from './entry-translators/factory'; -import { hasDecoratorName, findDecoratorInfos } from './property-translators/utils'; +import { hasDecoratorName, findDecoratorInfos, backingField, expectName } from './property-translators/utils'; import { factory } from './ui-factory'; import { factory as propertyFactory } from './property-translators/factory'; import { StructMap } from '../common/program-visitor'; import { CUSTOM_COMPONENT_IMPORT_SOURCE_NAME, - DecoratorIntrinsicNames, DecoratorNames, DECORATOR_TYPE_MAP, ENTRY_POINT_IMPORT_SOURCE_NAME, @@ -225,10 +220,6 @@ export class ComponentTransformer extends AbstractVisitor { return node; } const updateStatements: arkts.AstNode[] = []; - if (this.shouldAddLinkIntrinsic) { - const expr = arkts.factory.createIdentifier(DecoratorIntrinsicNames.LINK); - updateStatements.push(factory.createIntrinsicAnnotationDeclaration({ expr })); - } if (this.componentInterfaceCollection.length > 0) { if (!this.isCustomComponentImported) this.createImportDeclaration( @@ -315,7 +306,7 @@ export class ComponentTransformer extends AbstractVisitor { return arkts.factory.createMethodDefinition( arkts.Es2pandaMethodDefinitionKind.METHOD_DEFINITION_KIND_METHOD, - arkts.factory.createIdentifier(CustomComponentNames.BUILDCOMPATIBLENODE), + arkts.factory.createIdentifier(CustomComponentNames.BUILD_COMPATIBLE_NODE), script, arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_PUBLIC | arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_STATIC, false @@ -456,23 +447,19 @@ export class ComponentTransformer extends AbstractVisitor { } createInterfaceInnerMember(member: arkts.ClassProperty): arkts.ClassProperty[] { + const infos = findDecoratorInfos(member); + const annotations = infos.map((it) => it.annotation.clone()); const originalName: string = expectName(member.key); const originMember: arkts.ClassProperty = propertyFactory.createOptionalClassProperty( originalName, member, undefined, arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_PUBLIC - ); - const infos = findDecoratorInfos(member); - const buildParamInfo = infos.find((it) => - isDecoratorAnnotation(it.annotation, DecoratorNames.BUILDER_PARAM, true) - ); - if (!!buildParamInfo) { - originMember.setAnnotations([buildParamInfo.annotation.clone()]); - return [originMember]; - } - const targetInfo = infos.find((it) => DECORATOR_TYPE_MAP.has(it.name)); - if (!!targetInfo) { + ).setAnnotations(annotations); + const typedAnnotations = infos + .filter((it) => DECORATOR_TYPE_MAP.has(it.name)) + .map((it) => it.annotation.clone()); + if (typedAnnotations.length > 0) { const newName: string = backingField(originalName); const newMember: arkts.ClassProperty = propertyFactory .createOptionalClassProperty( @@ -481,11 +468,7 @@ export class ComponentTransformer extends AbstractVisitor { undefined, arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_PUBLIC ) - .setAnnotations([targetInfo.annotation.clone()]); - if (isDecoratorAnnotation(targetInfo.annotation, DecoratorNames.LINK, true)) { - this.shouldAddLinkIntrinsic = true; - originMember.setAnnotations([annotation(DecoratorIntrinsicNames.LINK)]); - } + .setAnnotations(typedAnnotations); return [originMember, newMember]; } return [originMember]; diff --git a/arkui-plugins/ui-plugins/entry-translators/factory.ts b/arkui-plugins/ui-plugins/entry-translators/factory.ts index e4dd29d22..d1b6ca2fa 100644 --- a/arkui-plugins/ui-plugins/entry-translators/factory.ts +++ b/arkui-plugins/ui-plugins/entry-translators/factory.ts @@ -16,7 +16,7 @@ import * as arkts from '@koalaui/libarkts'; import * as path from 'path'; import { annotation, createAndInsertImportDeclaration } from '../../common/arkts-utils'; -import { ENTRY_POINT_IMPORT_SOURCE_NAME, EntryWrapperNames, NavigationNames } from '../../common/predefines'; +import { ENTRY_POINT_IMPORT_SOURCE_NAME, EntryWrapperNames, NavigationNames, NodeCacheNames } from '../../common/predefines'; import { ProjectConfig } from '../../common/plugin-context'; import { factory as uiFactory } from '../ui-factory'; import { getRelativePagePath } from './utils'; @@ -220,7 +220,7 @@ export class factory { member.scriptFunction.id.name === EntryWrapperNames.ENTRY_FUNC ) { addMemoAnnotation(member.scriptFunction); - arkts.NodeCache.getInstance().collect(member); + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).collect(member); } }); } diff --git a/arkui-plugins/ui-plugins/index.ts b/arkui-plugins/ui-plugins/index.ts index 1785e4854..799f6ee4d 100644 --- a/arkui-plugins/ui-plugins/index.ts +++ b/arkui-plugins/ui-plugins/index.ts @@ -20,6 +20,8 @@ import { Plugins, PluginContext, ProjectConfig } from '../common/plugin-context' import { ProgramVisitor } from '../common/program-visitor'; import { EXTERNAL_SOURCE_PREFIX_NAMES } from '../common/predefines'; import { debugDump, debugLog, getDumpFileName } from '../common/debug'; +import { UIVisitor } from '../collectors/ui-collectors/ui-visitor'; +import { MemoVisitor } from '../collectors/memo-collectors/memo-visitor'; export function uiTransform(): Plugins { return { @@ -155,11 +157,14 @@ function checkedProgramVisit( if (projectConfig && !projectConfig.appResource) { projectConfig.ignoreError = true; } + const uiCollector = new UIVisitor(); + // const memoCollector = new MemoVisitor(); + const checkedTransformer = new CheckedTransformer(projectConfig); const programVisitor = new ProgramVisitor({ pluginName: uiTransform.name, state: arkts.Es2pandaContextState.ES2PANDA_STATE_CHECKED, - visitors: [checkedTransformer], + visitors: [uiCollector, checkedTransformer], skipPrefixNames: EXTERNAL_SOURCE_PREFIX_NAMES, pluginContext: context, }); diff --git a/arkui-plugins/ui-plugins/property-translators/builderParam.ts b/arkui-plugins/ui-plugins/property-translators/builderParam.ts index 41c616f60..6a95c4506 100644 --- a/arkui-plugins/ui-plugins/property-translators/builderParam.ts +++ b/arkui-plugins/ui-plugins/property-translators/builderParam.ts @@ -15,9 +15,8 @@ import * as arkts from '@koalaui/libarkts'; -import { backingField, expectName } from '../../common/arkts-utils'; -import { DecoratorNames } from '../../common/predefines'; -import { createGetter, createSetter, generateThisBacking, hasDecorator, removeDecorator, PropertyCache } from './utils'; +import { DecoratorNames, NodeCacheNames } from '../../common/predefines'; +import { createGetter, createSetter, generateThisBacking, hasDecorator, removeDecorator, PropertyCache, expectName, backingField } from './utils'; import { InterfacePropertyTranslator, InterfacePropertyTypes, PropertyTranslator } from './base'; import { GetterSetter, InitializerConstructor } from './types'; import { factory } from './factory'; @@ -49,7 +48,7 @@ export class BuilderParamTranslator extends PropertyTranslator implements Initia arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_PRIVATE, true ); - arkts.NodeCache.getInstance().collect(field); + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).collect(field); const thisSetValue: arkts.Expression = generateThisBacking(newName, false, false); const getter: arkts.MethodDefinition = this.translateGetter( originalName, @@ -58,9 +57,9 @@ export class BuilderParamTranslator extends PropertyTranslator implements Initia ? generateThisBacking(newName, false, false) : generateThisBacking(newName, false, true) ); - arkts.NodeCache.getInstance().collect(getter); + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).collect(getter); const setter: arkts.MethodDefinition = this.translateSetter(originalName, propertyType?.clone(), thisSetValue); - arkts.NodeCache.getInstance().collect(setter); + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).collect(setter); return [field, getter, setter]; } @@ -115,10 +114,10 @@ export class BuilderParamInterfaceTranslator e } static canBeTranslated(node: arkts.AstNode): node is InterfacePropertyTypes { - if (arkts.isMethodDefinition(node) && hasDecorator(node, DecoratorNames.BUILDER_PARAM)) { - return true; - } else if (arkts.isClassProperty(node) && hasDecorator(node, DecoratorNames.BUILDER_PARAM)) { - return true; + if (arkts.isMethodDefinition(node)) { + return hasDecorator(node, DecoratorNames.BUILDER_PARAM); + } else if (arkts.isClassProperty(node)) { + return hasDecorator(node, DecoratorNames.BUILDER_PARAM); } return false; } @@ -142,7 +141,7 @@ export class BuilderParamInterfaceTranslator e }); method.setOverloads(newOverLoads); removeDecorator(method, DecoratorNames.BUILDER_PARAM); - arkts.NodeCache.getInstance().collect(method, { isGetter: true }); + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).collect(method, { isGetter: true }); } else if (method.kind === arkts.Es2pandaMethodDefinitionKind.METHOD_DEFINITION_KIND_SET) { const param = method.scriptFunction.params.at(0)! as arkts.ETSParameterExpression; const type = param.type; @@ -150,7 +149,7 @@ export class BuilderParamInterfaceTranslator e addMemoAnnotation(type); } removeDecorator(method, DecoratorNames.BUILDER_PARAM); - arkts.NodeCache.getInstance().collect(method, { isSetter: true }); + arkts.NodeCacheFactory.getInstance().getCache(NodeCacheNames.MEMO).collect(method, { isSetter: true }); } return method; } diff --git a/arkui-plugins/ui-plugins/property-translators/consume.ts b/arkui-plugins/ui-plugins/property-translators/consume.ts index 8bd40ec2e..a4d87d930 100644 --- a/arkui-plugins/ui-plugins/property-translators/consume.ts +++ b/arkui-plugins/ui-plugins/property-translators/consume.ts @@ -15,7 +15,6 @@ import * as arkts from '@koalaui/libarkts'; -import { backingField, expectName } from '../../common/arkts-utils'; import { DecoratorNames, GetSetTypes, StateManagementTypes } from '../../common/predefines'; import { generateToRecord, @@ -26,6 +25,9 @@ import { getValueInAnnotation, hasDecorator, PropertyCache, + expectName, + backingField, + checkIsNameStartWithBackingField, } from './utils'; import { InterfacePropertyTranslator, InterfacePropertyTypes, PropertyTranslator } from './base'; import { GetterSetter, InitializerConstructor } from './types'; @@ -125,10 +127,10 @@ export class ConsumeInterfaceTranslator extend } static canBeTranslated(node: arkts.AstNode): node is InterfacePropertyTypes { - if (arkts.isMethodDefinition(node) && hasDecorator(node, DecoratorNames.CONSUME)) { - return true; - } else if (arkts.isClassProperty(node) && hasDecorator(node, DecoratorNames.CONSUME)) { - return true; + if (arkts.isMethodDefinition(node)) { + return checkIsNameStartWithBackingField(node.name) && hasDecorator(node, DecoratorNames.CONSUME); + } else if (arkts.isClassProperty(node)) { + return checkIsNameStartWithBackingField(node.key) && hasDecorator(node, DecoratorNames.CONSUME); } return false; } diff --git a/arkui-plugins/ui-plugins/property-translators/link.ts b/arkui-plugins/ui-plugins/property-translators/link.ts index 65217d539..e910821eb 100644 --- a/arkui-plugins/ui-plugins/property-translators/link.ts +++ b/arkui-plugins/ui-plugins/property-translators/link.ts @@ -15,7 +15,6 @@ import * as arkts from '@koalaui/libarkts'; -import { backingField, expectName } from '../../common/arkts-utils'; import { DecoratorNames, GetSetTypes, StateManagementTypes } from '../../common/predefines'; import { CustomComponentNames } from '../utils'; import { @@ -27,6 +26,9 @@ import { collectStateManagementTypeImport, hasDecorator, PropertyCache, + expectName, + backingField, + checkIsNameStartWithBackingField, } from './utils'; import { InterfacePropertyTranslator, InterfacePropertyTypes, PropertyTranslator } from './base'; import { GetterSetter, InitializerConstructor } from './types'; @@ -144,10 +146,10 @@ export class LinkInterfaceTranslator extends I } static canBeTranslated(node: arkts.AstNode): node is InterfacePropertyTypes { - if (arkts.isMethodDefinition(node) && hasDecorator(node, DecoratorNames.LINK)) { - return true; - } else if (arkts.isClassProperty(node) && hasDecorator(node, DecoratorNames.LINK)) { - return true; + if (arkts.isMethodDefinition(node)) { + return checkIsNameStartWithBackingField(node.name) && hasDecorator(node, DecoratorNames.LINK); + } else if (arkts.isClassProperty(node)) { + return checkIsNameStartWithBackingField(node.key) && hasDecorator(node, DecoratorNames.LINK); } return false; } diff --git a/arkui-plugins/ui-plugins/property-translators/localstoragelink.ts b/arkui-plugins/ui-plugins/property-translators/localstoragelink.ts index be59f9e6b..b3bf7ad74 100755 --- a/arkui-plugins/ui-plugins/property-translators/localstoragelink.ts +++ b/arkui-plugins/ui-plugins/property-translators/localstoragelink.ts @@ -15,7 +15,6 @@ import * as arkts from '@koalaui/libarkts'; -import { backingField, expectName } from '../../common/arkts-utils'; import { DecoratorNames, GetSetTypes, StateManagementTypes } from '../../common/predefines'; import { InterfacePropertyTranslator, InterfacePropertyTypes, PropertyTranslator } from './base'; import { GetterSetter, InitializerConstructor } from './types'; @@ -28,6 +27,9 @@ import { collectStateManagementTypeImport, hasDecorator, PropertyCache, + expectName, + backingField, + checkIsNameStartWithBackingField, } from './utils'; import { factory } from './factory'; @@ -88,7 +90,7 @@ export class LocalStorageLinkTranslator extends PropertyTranslator implements In arkts.factory.createStringLiteral(localStorageLinkValueStr), arkts.factory.create1StringLiteral(originalName), this.property.value ?? arkts.factory.createUndefinedLiteral(), - factory.createTypeFrom(this.property.typeAnnotation) + factory.createTypeFrom(this.property.typeAnnotation), ]; factory.judgeIfAddWatchFunc(args, this.property); collectStateManagementTypeImport(StateManagementTypes.LOCAL_STORAGE_LINK_DECORATED); @@ -161,10 +163,10 @@ export class LocalStorageLinkInterfaceTranslator< } static canBeTranslated(node: arkts.AstNode): node is InterfacePropertyTypes { - if (arkts.isMethodDefinition(node) && hasDecorator(node, DecoratorNames.LOCAL_STORAGE_LINK)) { - return true; - } else if (arkts.isClassProperty(node) && hasDecorator(node, DecoratorNames.LOCAL_STORAGE_LINK)) { - return true; + if (arkts.isMethodDefinition(node)) { + return checkIsNameStartWithBackingField(node.name) && hasDecorator(node, DecoratorNames.LOCAL_STORAGE_LINK); + } else if (arkts.isClassProperty(node)) { + return checkIsNameStartWithBackingField(node.key) && hasDecorator(node, DecoratorNames.LOCAL_STORAGE_LINK); } return false; } diff --git a/arkui-plugins/ui-plugins/property-translators/localstorageprop.ts b/arkui-plugins/ui-plugins/property-translators/localstorageprop.ts index 58eec31cc..7862526ac 100644 --- a/arkui-plugins/ui-plugins/property-translators/localstorageprop.ts +++ b/arkui-plugins/ui-plugins/property-translators/localstorageprop.ts @@ -15,10 +15,12 @@ import * as arkts from '@koalaui/libarkts'; -import { backingField, expectName } from '../../common/arkts-utils'; import { DecoratorNames, StateManagementTypes } from '../../common/predefines'; import { + backingField, + checkIsNameStartWithBackingField, collectStateManagementTypeImport, + expectName, generateToRecord, hasDecorator, PropertyCache, @@ -211,10 +213,10 @@ export class LocalStoragePropInterfaceTranslator< } static canBeTranslated(node: arkts.AstNode): node is InterfacePropertyTypes { - if (arkts.isMethodDefinition(node) && hasDecorator(node, DecoratorNames.LOCAL_STORAGE_PROP)) { - return true; - } else if (arkts.isClassProperty(node) && hasDecorator(node, DecoratorNames.LOCAL_STORAGE_PROP)) { - return true; + if (arkts.isMethodDefinition(node)) { + return checkIsNameStartWithBackingField(node.name) && hasDecorator(node, DecoratorNames.LOCAL_STORAGE_PROP); + } else if (arkts.isClassProperty(node)) { + return checkIsNameStartWithBackingField(node.key) && hasDecorator(node, DecoratorNames.LOCAL_STORAGE_PROP); } return false; } diff --git a/arkui-plugins/ui-plugins/property-translators/objectlink.ts b/arkui-plugins/ui-plugins/property-translators/objectlink.ts index 2c2089794..3704b1102 100644 --- a/arkui-plugins/ui-plugins/property-translators/objectlink.ts +++ b/arkui-plugins/ui-plugins/property-translators/objectlink.ts @@ -15,11 +15,13 @@ import * as arkts from '@koalaui/libarkts'; -import { backingField, expectName } from '../../common/arkts-utils'; import { DecoratorNames, GetSetTypes, StateManagementTypes } from '../../common/predefines'; import { CustomComponentNames } from '../utils'; import { + backingField, + checkIsNameStartWithBackingField, createGetter, + expectName, generateGetOrSetCall, generateThisBacking, generateToRecord, @@ -143,10 +145,10 @@ export class ObjectLinkInterfaceTranslator ext } static canBeTranslated(node: arkts.AstNode): node is InterfacePropertyTypes { - if (arkts.isMethodDefinition(node) && hasDecorator(node, DecoratorNames.OBJECT_LINK)) { - return true; + if (arkts.isMethodDefinition(node)) { + return checkIsNameStartWithBackingField(node.name) && hasDecorator(node, DecoratorNames.OBJECT_LINK); } else if (arkts.isClassProperty(node) && hasDecorator(node, DecoratorNames.OBJECT_LINK)) { - return true; + return checkIsNameStartWithBackingField(node.key) && hasDecorator(node, DecoratorNames.OBJECT_LINK); } return false; } diff --git a/arkui-plugins/ui-plugins/property-translators/observedTrack.ts b/arkui-plugins/ui-plugins/property-translators/observedTrack.ts index 28d0762e4..e26c2b296 100644 --- a/arkui-plugins/ui-plugins/property-translators/observedTrack.ts +++ b/arkui-plugins/ui-plugins/property-translators/observedTrack.ts @@ -14,9 +14,16 @@ */ import * as arkts from '@koalaui/libarkts'; -import { annotation, backingField, expectName } from '../../common/arkts-utils'; +import { annotation } from '../../common/arkts-utils'; import { DecoratorNames, StateManagementTypes } from '../../common/predefines'; -import { collectStateManagementTypeImport, hasDecorator, hasDecoratorName, removeDecorator } from './utils'; +import { + backingField, + collectStateManagementTypeImport, + expectName, + hasDecorator, + hasDecoratorName, + removeDecorator, +} from './utils'; import { ClassScopeInfo } from './types'; import { factory } from './factory'; @@ -57,7 +64,8 @@ export class ObservedTrackTranslator { const annotations: arkts.AnnotationUsage[] = [...this.property.annotations]; if ( !hasDecoratorName(this.property, DecoratorNames.JSONSTRINGIFYIGNORE) && - !hasDecoratorName(this.property, DecoratorNames.JSONRENAME)) { + !hasDecoratorName(this.property, DecoratorNames.JSONRENAME) + ) { annotations.push( annotation(DecoratorNames.JSONRENAME).addProperty( arkts.factory.createClassProperty( diff --git a/arkui-plugins/ui-plugins/property-translators/prop.ts b/arkui-plugins/ui-plugins/property-translators/prop.ts index df2bce793..9a7c452c8 100644 --- a/arkui-plugins/ui-plugins/property-translators/prop.ts +++ b/arkui-plugins/ui-plugins/property-translators/prop.ts @@ -15,7 +15,6 @@ import * as arkts from '@koalaui/libarkts'; -import { backingField, expectName } from '../../common/arkts-utils'; import { DecoratorNames, GetSetTypes, StateManagementTypes } from '../../common/predefines'; import { CustomComponentNames } from '../utils'; import { @@ -27,6 +26,9 @@ import { collectStateManagementTypeImport, hasDecorator, PropertyCache, + expectName, + backingField, + checkIsNameStartWithBackingField, } from './utils'; import { InterfacePropertyTranslator, InterfacePropertyTypes, PropertyTranslator } from './base'; import { GetterSetter, InitializerConstructor } from './types'; @@ -170,10 +172,10 @@ export class PropInterfaceTranslator extends I } static canBeTranslated(node: arkts.AstNode): node is InterfacePropertyTypes { - if (arkts.isMethodDefinition(node) && hasDecorator(node, DecoratorNames.PROP)) { - return true; - } else if (arkts.isClassProperty(node) && hasDecorator(node, DecoratorNames.PROP)) { - return true; + if (arkts.isMethodDefinition(node)) { + return checkIsNameStartWithBackingField(node.name) && hasDecorator(node, DecoratorNames.PROP); + } else if (arkts.isClassProperty(node)) { + return checkIsNameStartWithBackingField(node.key) && hasDecorator(node, DecoratorNames.PROP); } return false; } diff --git a/arkui-plugins/ui-plugins/property-translators/provide.ts b/arkui-plugins/ui-plugins/property-translators/provide.ts index c1141a5ac..158d240fd 100644 --- a/arkui-plugins/ui-plugins/property-translators/provide.ts +++ b/arkui-plugins/ui-plugins/property-translators/provide.ts @@ -15,7 +15,6 @@ import * as arkts from '@koalaui/libarkts'; -import { backingField, expectName } from '../../common/arkts-utils'; import { DecoratorNames, GetSetTypes, StateManagementTypes } from '../../common/predefines'; import { CustomComponentNames } from '../utils'; import { @@ -28,6 +27,9 @@ import { ProvideOptions, hasDecorator, PropertyCache, + expectName, + backingField, + checkIsNameStartWithBackingField, } from './utils'; import { InterfacePropertyTranslator, InterfacePropertyTypes, PropertyTranslator } from './base'; import { GetterSetter, InitializerConstructor } from './types'; @@ -137,10 +139,10 @@ export class ProvideInterfaceTranslator extend } static canBeTranslated(node: arkts.AstNode): node is InterfacePropertyTypes { - if (arkts.isMethodDefinition(node) && hasDecorator(node, DecoratorNames.PROVIDE)) { - return true; - } else if (arkts.isClassProperty(node) && hasDecorator(node, DecoratorNames.PROVIDE)) { - return true; + if (arkts.isMethodDefinition(node)) { + return checkIsNameStartWithBackingField(node.name) && hasDecorator(node, DecoratorNames.PROVIDE); + } else if (arkts.isClassProperty(node)) { + return checkIsNameStartWithBackingField(node.key) && hasDecorator(node, DecoratorNames.PROVIDE); } return false; } diff --git a/arkui-plugins/ui-plugins/property-translators/regularProperty.ts b/arkui-plugins/ui-plugins/property-translators/regularProperty.ts index 7960157cd..9cfa1ed99 100644 --- a/arkui-plugins/ui-plugins/property-translators/regularProperty.ts +++ b/arkui-plugins/ui-plugins/property-translators/regularProperty.ts @@ -15,12 +15,19 @@ import * as arkts from '@koalaui/libarkts'; -import { createGetter, generateToRecord, generateThisBacking, createSetter2, PropertyCache } from './utils'; +import { + createGetter, + generateToRecord, + generateThisBacking, + createSetter2, + PropertyCache, + expectName, + backingField, +} from './utils'; import { InterfacePropertyTranslator, InterfacePropertyTypes, PropertyTranslator } from './base'; import { GetterSetter, InitializerConstructor } from './types'; -import { backingField, expectName } from '../../common/arkts-utils'; import { factory } from './factory'; -import { updateArrow, updateNewClassInstanceExpression } from '../customdialog'; +import { updateNewClassInstanceExpression } from '../customdialog'; export class RegularPropertyTranslator extends PropertyTranslator implements InitializerConstructor, GetterSetter { translateMember(): arkts.AstNode[] { @@ -31,8 +38,12 @@ export class RegularPropertyTranslator extends PropertyTranslator implements Ini } isCustomDialogController(node: arkts.AstNode | undefined): boolean { - if ((node instanceof arkts.ETSNewClassInstanceExpression) && (node.getTypeRef instanceof arkts.ETSTypeReference) && - (node.getTypeRef?.part?.name instanceof arkts.Identifier) && (node.getTypeRef?.part?.name?.name === 'CustomDialogController')) { + if ( + node instanceof arkts.ETSNewClassInstanceExpression && + node.getTypeRef instanceof arkts.ETSTypeReference && + node.getTypeRef?.part?.name instanceof arkts.Identifier && + node.getTypeRef?.part?.name?.name === 'CustomDialogController' + ) { return true; } return false; @@ -41,7 +52,11 @@ export class RegularPropertyTranslator extends PropertyTranslator implements Ini cacheTranslatedInitializer(newName: string, originalName: string): void { const value = this.property.value; if (this.isCustomDialogController(value)) { - const newValue = updateNewClassInstanceExpression(value as arkts.ETSNewClassInstanceExpression, this.property.key?.name, true); + const newValue = updateNewClassInstanceExpression( + value as arkts.ETSNewClassInstanceExpression, + expectName(this.property.key), + true + ); const initializeStruct: arkts.AstNode = this.generateInitializeStruct(newName, originalName, newValue); PropertyCache.getInstance().collectInitializeStruct(this.structInfo.name, [initializeStruct]); } else { @@ -99,7 +114,7 @@ export class RegularPropertyTranslator extends PropertyTranslator implements Ini return createSetter2(originalName, typeAnnotation, statement); } - generateInitializeStruct(newName: string, originalName: string, value: arkts.Expression): arkts.AstNode { + generateInitializeStruct(newName: string, originalName: string, value: arkts.Expression | undefined): arkts.AstNode { const binaryItem = arkts.factory.createBinaryExpression( factory.createBlockStatementForOptionalExpression( arkts.factory.createIdentifier('initializers'), diff --git a/arkui-plugins/ui-plugins/property-translators/state.ts b/arkui-plugins/ui-plugins/property-translators/state.ts index 704a56f71..6a8a791ac 100644 --- a/arkui-plugins/ui-plugins/property-translators/state.ts +++ b/arkui-plugins/ui-plugins/property-translators/state.ts @@ -15,7 +15,6 @@ import * as arkts from '@koalaui/libarkts'; -import { backingField, expectName } from '../../common/arkts-utils'; import { DecoratorNames, GetSetTypes, StateManagementTypes } from '../../common/predefines'; import { CustomComponentNames } from '../utils'; import { @@ -27,6 +26,9 @@ import { hasDecorator, collectStateManagementTypeImport, PropertyCache, + expectName, + backingField, + checkIsNameStartWithBackingField, } from './utils'; import { InterfacePropertyTranslator, InterfacePropertyTypes, PropertyTranslator } from './base'; import { GetterSetter, InitializerConstructor } from './types'; @@ -130,10 +132,10 @@ export class StateInterfaceTranslator extends } static canBeTranslated(node: arkts.AstNode): node is InterfacePropertyTypes { - if (arkts.isMethodDefinition(node) && hasDecorator(node, DecoratorNames.STATE)) { - return true; - } else if (arkts.isClassProperty(node) && hasDecorator(node, DecoratorNames.STATE)) { - return true; + if (arkts.isMethodDefinition(node)) { + return checkIsNameStartWithBackingField(node.name) && hasDecorator(node, DecoratorNames.STATE); + } else if (arkts.isClassProperty(node)) { + return checkIsNameStartWithBackingField(node.key) && hasDecorator(node, DecoratorNames.STATE); } return false; } diff --git a/arkui-plugins/ui-plugins/property-translators/staticProperty.ts b/arkui-plugins/ui-plugins/property-translators/staticProperty.ts index 12e9535c5..5d86c0e24 100644 --- a/arkui-plugins/ui-plugins/property-translators/staticProperty.ts +++ b/arkui-plugins/ui-plugins/property-translators/staticProperty.ts @@ -15,10 +15,9 @@ import * as arkts from '@koalaui/libarkts'; -import { createGetter, createSetter } from './utils'; +import { backingField, createGetter, createSetter, expectName } from './utils'; import { PropertyTranslator } from './base'; import { GetterSetter, InitializerConstructor } from './types'; -import { backingField, expectName } from '../../common/arkts-utils'; export class staticPropertyTranslator extends PropertyTranslator implements InitializerConstructor, GetterSetter { translateMember(): arkts.AstNode[] { diff --git a/arkui-plugins/ui-plugins/property-translators/storageProp.ts b/arkui-plugins/ui-plugins/property-translators/storageProp.ts index 8d3cf948a..aea182326 100644 --- a/arkui-plugins/ui-plugins/property-translators/storageProp.ts +++ b/arkui-plugins/ui-plugins/property-translators/storageProp.ts @@ -15,7 +15,6 @@ import * as arkts from '@koalaui/libarkts'; -import { backingField, expectName } from '../../common/arkts-utils'; import { DecoratorNames, GetSetTypes, StateManagementTypes } from '../../common/predefines'; import { InterfacePropertyTranslator, InterfacePropertyTypes, PropertyTranslator } from './base'; import { GetterSetter, InitializerConstructor } from './types'; @@ -28,6 +27,9 @@ import { collectStateManagementTypeImport, hasDecorator, PropertyCache, + expectName, + backingField, + checkIsNameStartWithBackingField, } from './utils'; import { factory } from './factory'; @@ -160,10 +162,10 @@ export class StoragePropInterfaceTranslator ex } static canBeTranslated(node: arkts.AstNode): node is InterfacePropertyTypes { - if (arkts.isMethodDefinition(node) && hasDecorator(node, DecoratorNames.STORAGE_PROP)) { - return true; - } else if (arkts.isClassProperty(node) && hasDecorator(node, DecoratorNames.STORAGE_PROP)) { - return true; + if (arkts.isMethodDefinition(node)) { + return checkIsNameStartWithBackingField(node.name) && hasDecorator(node, DecoratorNames.STORAGE_PROP); + } else if (arkts.isClassProperty(node)) { + return checkIsNameStartWithBackingField(node.key) && hasDecorator(node, DecoratorNames.STORAGE_PROP); } return false; } diff --git a/arkui-plugins/ui-plugins/property-translators/storagelink.ts b/arkui-plugins/ui-plugins/property-translators/storagelink.ts index 8c43bd631..d79633ad7 100644 --- a/arkui-plugins/ui-plugins/property-translators/storagelink.ts +++ b/arkui-plugins/ui-plugins/property-translators/storagelink.ts @@ -15,7 +15,6 @@ import * as arkts from '@koalaui/libarkts'; -import { backingField, expectName } from '../../common/arkts-utils'; import { DecoratorNames, GetSetTypes, StateManagementTypes } from '../../common/predefines'; import { InterfacePropertyTranslator, InterfacePropertyTypes, PropertyTranslator } from './base'; import { GetterSetter, InitializerConstructor } from './types'; @@ -28,6 +27,9 @@ import { collectStateManagementTypeImport, hasDecorator, PropertyCache, + expectName, + backingField, + checkIsNameStartWithBackingField, } from './utils'; import { factory } from './factory'; @@ -159,10 +161,10 @@ export class StorageLinkInterfaceTranslator ex } static canBeTranslated(node: arkts.AstNode): node is InterfacePropertyTypes { - if (arkts.isMethodDefinition(node) && hasDecorator(node, DecoratorNames.STORAGE_LINK)) { - return true; - } else if (arkts.isClassProperty(node) && hasDecorator(node, DecoratorNames.STORAGE_LINK)) { - return true; + if (arkts.isMethodDefinition(node)) { + return checkIsNameStartWithBackingField(node.name) && hasDecorator(node, DecoratorNames.STORAGE_LINK); + } else if (arkts.isClassProperty(node)) { + return checkIsNameStartWithBackingField(node.key) && hasDecorator(node, DecoratorNames.STORAGE_LINK); } return false; } diff --git a/arkui-plugins/ui-plugins/property-translators/utils.ts b/arkui-plugins/ui-plugins/property-translators/utils.ts index 56d189490..6fa7eb547 100644 --- a/arkui-plugins/ui-plugins/property-translators/utils.ts +++ b/arkui-plugins/ui-plugins/property-translators/utils.ts @@ -17,9 +17,7 @@ import * as arkts from '@koalaui/libarkts'; import { ImportCollector } from '../../common/import-collector'; import { isDecoratorAnnotation } from '../../common/arkts-utils'; import { - DecoratorIntrinsicNames, DecoratorNames, - DECORATOR_TYPE_MAP, StateManagementTypes, GetSetTypes, } from '../../common/predefines'; @@ -34,13 +32,6 @@ export interface DecoratorInfo { name: DecoratorNames; } -export function isDecoratorIntrinsicAnnotation( - anno: arkts.AnnotationUsage, - decoratorName: DecoratorIntrinsicNames -): boolean { - return !!anno.expr && arkts.isIdentifier(anno.expr) && anno.expr.name === decoratorName; -} - export function removeDecorator( property: arkts.ClassProperty | arkts.ClassDefinition | arkts.MethodDefinition, decoratorName: DecoratorNames, @@ -137,15 +128,6 @@ export function findDecoratorInfos( return infos; } -export function getStateManagementType(decoratorInfo: DecoratorInfo): StateManagementTypes { - const decoratorName = decoratorInfo.name; - const typeName = DECORATOR_TYPE_MAP.get(decoratorName); - if (!!typeName) { - return typeName; - } - return StateManagementTypes.MUTABLE_STATE; -} - export function collectStateManagementTypeImport(type: StateManagementTypes): void { ImportCollector.getInstance().collectImport(type); } @@ -382,6 +364,27 @@ export function generateToRecord(newName: string, originalName: string): arkts.P ); } +export function expectName(node: arkts.AstNode | undefined): string { + if (!node) { + throw new Error('Expected an identifier, got empty node'); + } + if (!arkts.isIdentifier(node)) { + throw new Error('Expected an identifier, got: ' + arkts.nodeType(node).toString()); + } + return node.name; +} + +export function backingField(originalName: string): string { + return `${StateManagementTypes.BACKING}_${originalName}`; +} + +export function checkIsNameStartWithBackingField(node: arkts.AstNode | undefined): boolean { + if (!node || !arkts.isIdentifier(node)) { + return false; + } + return node.name.startsWith(StateManagementTypes.BACKING); +} + // CACHE export interface PropertyCachedBody { initializeBody?: arkts.AstNode[]; diff --git a/arkui-plugins/ui-plugins/struct-translators/utils.ts b/arkui-plugins/ui-plugins/struct-translators/utils.ts index bb3123e31..bd7275832 100644 --- a/arkui-plugins/ui-plugins/struct-translators/utils.ts +++ b/arkui-plugins/ui-plugins/struct-translators/utils.ts @@ -98,7 +98,7 @@ export function isResourceNode(node: arkts.CallExpression, ignoreDecl: boolean = if (!decl) { return false; } - const moduleName: string = arkts.getProgramFromAstNode(decl).moduleName; + const moduleName = arkts.getProgramFromAstNode(decl)?.moduleName; if (!moduleName || !matchPrefix(ARKUI_IMPORT_PREFIX_NAMES, moduleName)) { return false; } @@ -331,7 +331,7 @@ export function checkRawfileResource( ): void { if (!fromOtherModule && !rawfileSet.has(rawfileStr.str)) { LogCollector.getInstance().collectLogInfo({ - type: LogType.ERROR, + level: LogType.ERROR, node: resourceNode, message: `No such '${rawfileStr.str}' resource in current module.`, code: '10904333', @@ -367,7 +367,7 @@ export function preCheckResourceData( } if (!!code && !!message) { LogCollector.getInstance().collectLogInfo({ - type: LogType.ERROR, + level: LogType.ERROR, node: resourceNode, message: message, code: code, @@ -442,7 +442,7 @@ function resourceCheck( } if (!!code && !!message) { LogCollector.getInstance().collectLogInfo({ - type: logType, + level: logType, node: resourceNode, message: message, code: code, diff --git a/arkui-plugins/ui-plugins/ui-factory.ts b/arkui-plugins/ui-plugins/ui-factory.ts index f6718b652..644aa6409 100644 --- a/arkui-plugins/ui-plugins/ui-factory.ts +++ b/arkui-plugins/ui-plugins/ui-factory.ts @@ -15,14 +15,13 @@ import * as arkts from '@koalaui/libarkts'; import { - BuilderLambdaNames, CustomComponentAnontations, CustomComponentNames, hasNullOrUndefinedType, hasPropertyInAnnotation, } from './utils'; import { PartialExcept, PartialNested, PartialNestedExcept } from '../common/safe-types'; -import { DecoratorNames } from '../common/predefines'; +import { BuilderLambdaNames, DecoratorNames } from '../common/predefines'; import { needDefiniteOrOptionalModifier } from './property-translators/utils'; import { addMemoAnnotation } from '../collectors/memo-collectors/utils'; diff --git a/arkui-plugins/ui-plugins/utils.ts b/arkui-plugins/ui-plugins/utils.ts index 562c74f4e..13acc5698 100644 --- a/arkui-plugins/ui-plugins/utils.ts +++ b/arkui-plugins/ui-plugins/utils.ts @@ -27,7 +27,7 @@ export enum CustomComponentNames { COMPONENT_INITIALIZE_STRUCT = '__initializeStruct', COMPONENT_UPDATE_STRUCT = '__updateStruct', COMPONENT_INITIALIZERS_NAME = 'initializers', - BUILDCOMPATIBLENODE = '_buildCompatibleNode', + BUILD_COMPATIBLE_NODE = '_buildCompatibleNode', OPTIONS = 'options', PAGE_LIFE_CYCLE = 'PageLifeCycle', LAYOUT_CALLBACK = 'LayoutCallback', @@ -37,15 +37,6 @@ export enum CustomComponentNames { SETDIALOGCONTROLLER_METHOD = '__setDialogController__', } -export enum BuilderLambdaNames { - ANNOTATION_NAME = 'ComponentBuilder', - ORIGIN_METHOD_NAME = '$_instantiate', - TRANSFORM_METHOD_NAME = '_instantiateImpl', - STYLE_PARAM_NAME = 'style', - STYLE_ARROW_PARAM_NAME = 'instance', - CONTENT_PARAM_NAME = 'content' -} - // IMPORT export function findImportSourceByName(importName: string): string { const source = DeclarationCollector.getInstance().findExternalSourceFromName(importName); @@ -169,7 +160,7 @@ export function isCustomComponentAnnotation( if (!decl) { return false; } - const moduleName: string = arkts.getProgramFromAstNode(decl).moduleName; + const moduleName = arkts.getProgramFromAstNode(decl)?.moduleName; if (!moduleName || !matchPrefix(ARKUI_IMPORT_PREFIX_NAMES, moduleName)) { return false; } @@ -221,7 +212,7 @@ export function collectCustomComponentScopeInfo( return { name: definition.ident.name, isDecl, - annotations: annotations as CustomComponentAnontations, + annotations, }; } diff --git a/arkui-plugins/ui-syntax-plugins/utils/index.ts b/arkui-plugins/ui-syntax-plugins/utils/index.ts index cb9c231a5..7fa2be9f8 100644 --- a/arkui-plugins/ui-syntax-plugins/utils/index.ts +++ b/arkui-plugins/ui-syntax-plugins/utils/index.ts @@ -487,7 +487,7 @@ export function getAnnotationUsageByName( return false; } const program = arkts.getProgramFromAstNode(annotationDeclaration); - if (!isFromPresetModules(program.moduleName)) { + if (!program || !isFromPresetModules(program.moduleName)) { return false; } return true; diff --git a/koala-wrapper/native/src/common.cc b/koala-wrapper/native/src/common.cc index 1f4fee8aa..dd718ef8c 100644 --- a/koala-wrapper/native/src/common.cc +++ b/koala-wrapper/native/src/common.cc @@ -262,10 +262,77 @@ KNativePointer impl_AstNodeProgram(KNativePointer contextPtr, KNativePointer ins if (GetImpl()->AstNodeIsProgramConst(_context, _receiver)) { return GetImpl()->ETSModuleProgram(_context, _receiver); } - return impl_AstNodeProgram(_context, GetImpl()->AstNodeParent(_context, _receiver)); + auto parent = GetImpl()->AstNodeParent(_context, _receiver); + if (parent == nullptr) { + return nullptr; + } + return impl_AstNodeProgram(_context, parent); } KOALA_INTEROP_2(AstNodeProgram, KNativePointer, KNativePointer, KNativePointer) +thread_local KBoolean targetChildFound = false; +thread_local es2panda_AstNode *targetInnerChild = nullptr; +thread_local KInt targetAstNodeType = -1; + +static void findNodeInnerChild(es2panda_AstNode *node, void *arg) +{ + if (targetInnerChild == node) { + targetChildFound = true; + } +} + +KBoolean impl_AstNodeFindNodeInInnerChild(KNativePointer contextPtr, KNativePointer instancePtr, KNativePointer tartgetPtr) +{ + if (tartgetPtr == nullptr) { + return false; + } + auto _context = reinterpret_cast(contextPtr); + auto _receiver = reinterpret_cast(instancePtr); + auto _target = reinterpret_cast(tartgetPtr); + targetChildFound = false; + targetInnerChild = _target; + GetImpl()->AstNodeForEach(_receiver, findNodeInnerChild, _context); + return targetChildFound; +} +KOALA_INTEROP_3(AstNodeFindNodeInInnerChild, KBoolean, KNativePointer, KNativePointer, KNativePointer); + +static void findInnerChild(es2panda_AstNode *node, void *arg) +{ + auto *context = static_cast(arg); + if (targetAstNodeType == GetImpl()->AstNodeTypeConst(context, node)) { + targetInnerChild = node; + } +} + +KNativePointer impl_AstNodeFindInnerChild(KNativePointer contextPtr, KNativePointer instancePtr, KInt AstNodeType) +{ + auto _context = reinterpret_cast(contextPtr); + auto _receiver = reinterpret_cast(instancePtr); + targetAstNodeType = AstNodeType; + + GetImpl()->AstNodeForEach(_receiver, findInnerChild, _context); + return targetInnerChild; +} +KOALA_INTEROP_3(AstNodeFindInnerChild, KNativePointer, KNativePointer, KNativePointer, KInt); + +KNativePointer impl_AstNodeFindOuterParent(KNativePointer contextPtr, KNativePointer instancePtr, KInt AstNodeType) +{ + auto _context = reinterpret_cast(contextPtr); + auto _receiver = reinterpret_cast(instancePtr); + if (GetImpl()->AstNodeIsProgramConst(_context, _receiver)) { + return nullptr; + } + if (AstNodeType == GetImpl()->AstNodeTypeConst(_context, _receiver)) { + return _receiver; + } + auto parent = GetImpl()->AstNodeParent(_context, _receiver); + if (parent == nullptr) { + return nullptr; + } + return impl_AstNodeFindOuterParent(_context, parent, AstNodeType); +} +KOALA_INTEROP_3(AstNodeFindOuterParent, KNativePointer, KNativePointer, KNativePointer, KInt); + thread_local es2panda_AstNode *cachedParentNode; thread_local es2panda_Context *cachedContext; diff --git a/koala-wrapper/src/Es2pandaNativeModule.ts b/koala-wrapper/src/Es2pandaNativeModule.ts index 1fcdcdd4f..59fbabcb6 100644 --- a/koala-wrapper/src/Es2pandaNativeModule.ts +++ b/koala-wrapper/src/Es2pandaNativeModule.ts @@ -954,6 +954,18 @@ export class Es2pandaNativeModule { _CallExpressionIsTrailingCallConst(context: KNativePointer, node: KNativePointer): boolean { throw new Error('CallExpressionIsTrailingCallConst was not overloaded by native module initialization'); } + + _AstNodeFindNodeInInnerChild(context: KNativePointer, node: KNativePointer, target: KNativePointer): boolean { + throw new Error('AstNodeFindNodeInInnerChild was not overloaded by native module initialization'); + } + + _AstNodeFindInnerChild(context: KNativePointer, node: KNativePointer, nodeType: KInt): KNativePointer { + throw new Error('AstNodeFindInnerChild was not overloaded by native module initialization'); + } + + _AstNodeFindOuterParent(context: KNativePointer, node: KNativePointer, nodeType: KInt): KNativePointer { + throw new Error('AstNodeFindOuterParent was not overloaded by native module initialization'); + } } export function initEs2panda(): Es2pandaNativeModule { diff --git a/koala-wrapper/src/arkts-api/class-by-peer.ts b/koala-wrapper/src/arkts-api/class-by-peer.ts index a12afcba8..515502e68 100644 --- a/koala-wrapper/src/arkts-api/class-by-peer.ts +++ b/koala-wrapper/src/arkts-api/class-by-peer.ts @@ -13,28 +13,11 @@ * limitations under the License. */ -import { Es2pandaAstNodeType } from '../Es2pandaEnums'; +import { KNativePointer, nullptr } from '@koalaui/interop'; import { throwError } from '../utils'; import { global } from './static/global'; -import { KNativePointer, nullptr } from '@koalaui/interop'; import { AstNode, UnsupportedNode } from './peers/AstNode'; - -export const nodeByType = new Map([]); - -const cache = new Map(); -export function clearNodeCache(): void { - cache.clear(); -} - -export function getOrPut(peer: KNativePointer, create: (peer: KNativePointer) => AstNode): AstNode { - if (cache.has(peer)) { - return cache.get(peer)!; - } - - const newNode = create(peer); - cache.set(peer, newNode); - return newNode; -} +import { getOrPut, nodeByType } from './node-by-type'; export function classByPeer(peer: KNativePointer): T { if (peer === nullptr) { diff --git a/koala-wrapper/src/arkts-api/index.ts b/koala-wrapper/src/arkts-api/index.ts index eef8304a5..943ba899c 100644 --- a/koala-wrapper/src/arkts-api/index.ts +++ b/koala-wrapper/src/arkts-api/index.ts @@ -64,6 +64,7 @@ export * from '../generated/peers/ArrayExpression'; export * from '../generated/peers/TryStatement'; export * from '../generated/peers/ETSNullType'; +export * from './class-by-peer'; export * from './types'; export * from './utilities/private'; export * from './utilities/public'; diff --git a/koala-wrapper/src/arkts-api/node-by-type.ts b/koala-wrapper/src/arkts-api/node-by-type.ts new file mode 100644 index 000000000..b12dc0cf1 --- /dev/null +++ b/koala-wrapper/src/arkts-api/node-by-type.ts @@ -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. + */ + +import { KNativePointer } from '@koalaui/interop'; +import { Es2pandaAstNodeType } from '../Es2pandaEnums'; +import type { AstNode } from './peers/AstNode'; + +export const nodeByType = new Map([]); + +const cache = new Map(); +export function clearNodeCache(): void { + cache.clear(); +} + +export function getOrPut(peer: KNativePointer, create: (peer: KNativePointer) => AstNode): AstNode { + if (cache.has(peer)) { + return cache.get(peer)!; + } + + const newNode = create(peer); + cache.set(peer, newNode); + return newNode; +} \ No newline at end of file diff --git a/koala-wrapper/src/arkts-api/node-utilities/ArrowFunctionExpression.ts b/koala-wrapper/src/arkts-api/node-utilities/ArrowFunctionExpression.ts index a787698d7..8c38e1244 100644 --- a/koala-wrapper/src/arkts-api/node-utilities/ArrowFunctionExpression.ts +++ b/koala-wrapper/src/arkts-api/node-utilities/ArrowFunctionExpression.ts @@ -16,7 +16,7 @@ import { ScriptFunction } from '../../generated'; import { isSameNativeObject } from '../peers/ArktsObject'; import { ArrowFunctionExpression } from '../types'; -import { NodeCache } from '../utilities/nodeCache'; +import { NodeCacheFactory } from '../utilities/nodeCache'; import { attachModifiers, updateThenAttach } from '../utilities/private'; export function updateArrowFunctionExpression( @@ -33,8 +33,8 @@ export function updateArrowFunctionExpression( (node: ArrowFunctionExpression, original: ArrowFunctionExpression) => node.setAnnotations(original.annotations) ); const newNode = update(original, func); - if (NodeCache.getInstance().has(original)) { - NodeCache.getInstance().refresh(original, newNode); + if (NodeCacheFactory.getInstance().has(original)) { + NodeCacheFactory.getInstance().refresh(original, newNode); } return newNode; } diff --git a/koala-wrapper/src/arkts-api/node-utilities/CallExpression.ts b/koala-wrapper/src/arkts-api/node-utilities/CallExpression.ts index 843798827..1b43a99b9 100644 --- a/koala-wrapper/src/arkts-api/node-utilities/CallExpression.ts +++ b/koala-wrapper/src/arkts-api/node-utilities/CallExpression.ts @@ -17,7 +17,7 @@ import { TypeNode } from '../../generated'; import { isSameNativeObject } from '../peers/ArktsObject'; import { AstNode } from '../peers/AstNode'; import { CallExpression } from '../types'; -import { NodeCache } from '../utilities/nodeCache'; +import { NodeCacheFactory } from '../utilities/nodeCache'; import { attachModifiers, updateThenAttach } from '../utilities/private'; export function updateCallExpression( @@ -43,8 +43,8 @@ export function updateCallExpression( !!original.trailingBlock ? node.setTralingBlock(original.trailingBlock) : node ); const newNode = update(original, expression, typeArguments, args, isOptional, trailingComma); - if (NodeCache.getInstance().has(original)) { - NodeCache.getInstance().refresh(original, newNode); + if (NodeCacheFactory.getInstance().has(original)) { + NodeCacheFactory.getInstance().refresh(original, newNode); } return newNode; } diff --git a/koala-wrapper/src/arkts-api/node-utilities/ClassProperty.ts b/koala-wrapper/src/arkts-api/node-utilities/ClassProperty.ts index 1d31c2858..1ce11b242 100644 --- a/koala-wrapper/src/arkts-api/node-utilities/ClassProperty.ts +++ b/koala-wrapper/src/arkts-api/node-utilities/ClassProperty.ts @@ -18,7 +18,7 @@ import { isSameNativeObject } from '../peers/ArktsObject'; import { updateThenAttach } from '../utilities/private'; import { classPropertySetOptional, hasModifierFlag } from '../utilities/public'; import { Es2pandaModifierFlags } from '../../generated/Es2pandaEnums'; -import { NodeCache } from '../utilities/nodeCache'; +import { NodeCacheFactory } from '../utilities/nodeCache'; export function updateClassProperty( original: ClassProperty, @@ -49,8 +49,8 @@ export function updateClassProperty( } ); const newNode = update(original, key, value, typeAnnotation, modifiers, isComputed); - if (NodeCache.getInstance().has(original)) { - NodeCache.getInstance().refresh(original, newNode); + if (NodeCacheFactory.getInstance().has(original)) { + NodeCacheFactory.getInstance().refresh(original, newNode); } return newNode; } diff --git a/koala-wrapper/src/arkts-api/node-utilities/ETSFunctionType.ts b/koala-wrapper/src/arkts-api/node-utilities/ETSFunctionType.ts index c4e441974..622080ee1 100644 --- a/koala-wrapper/src/arkts-api/node-utilities/ETSFunctionType.ts +++ b/koala-wrapper/src/arkts-api/node-utilities/ETSFunctionType.ts @@ -17,7 +17,7 @@ import { ETSFunctionType, FunctionSignature } from '../../generated'; import { isSameNativeObject } from '../peers/ArktsObject'; import { attachModifiers, updateThenAttach } from '../utilities/private'; import { Es2pandaScriptFunctionFlags } from '../../generated/Es2pandaEnums'; -import { NodeCache } from '../utilities/nodeCache'; +import { NodeCacheFactory } from '../utilities/nodeCache'; export function updateETSFunctionType( original: ETSFunctionType, @@ -40,8 +40,8 @@ export function updateETSFunctionType( (node: ETSFunctionType, original: ETSFunctionType) => node.setAnnotations(original.annotations) ); const newNode = update(original, signature, funcFlags); - if (NodeCache.getInstance().has(original)) { - NodeCache.getInstance().refresh(original, newNode); + if (NodeCacheFactory.getInstance().has(original)) { + NodeCacheFactory.getInstance().refresh(original, newNode); } return newNode; } diff --git a/koala-wrapper/src/arkts-api/node-utilities/ETSParameterExpression.ts b/koala-wrapper/src/arkts-api/node-utilities/ETSParameterExpression.ts index 23923cd75..bef1df368 100644 --- a/koala-wrapper/src/arkts-api/node-utilities/ETSParameterExpression.ts +++ b/koala-wrapper/src/arkts-api/node-utilities/ETSParameterExpression.ts @@ -17,7 +17,7 @@ import { Identifier } from '../../generated'; import { isSameNativeObject } from '../peers/ArktsObject'; import { AstNode } from '../peers/AstNode'; import { ETSParameterExpression } from '../types'; -import { NodeCache } from '../utilities/nodeCache'; +import { NodeCacheFactory } from '../utilities/nodeCache'; import { attachModifiers, updateThenAttach } from '../utilities/private'; export function updateETSParameterExpression( @@ -41,8 +41,8 @@ export function updateETSParameterExpression( }, ); const newNode = update(original, identifier, initializer); - if (NodeCache.getInstance().has(original)) { - NodeCache.getInstance().refresh(original, newNode); + if (NodeCacheFactory.getInstance().has(original)) { + NodeCacheFactory.getInstance().refresh(original, newNode); } return newNode; } diff --git a/koala-wrapper/src/arkts-api/node-utilities/ETSUnionType.ts b/koala-wrapper/src/arkts-api/node-utilities/ETSUnionType.ts index 07ab40282..c2a2ce687 100644 --- a/koala-wrapper/src/arkts-api/node-utilities/ETSUnionType.ts +++ b/koala-wrapper/src/arkts-api/node-utilities/ETSUnionType.ts @@ -15,7 +15,7 @@ import { ETSUnionType, TypeNode } from '../../generated'; import { isSameNativeObject } from '../peers/ArktsObject'; -import { NodeCache } from '../utilities/nodeCache'; +import { NodeCacheFactory } from '../utilities/nodeCache'; import { attachModifiers, updateThenAttach } from '../utilities/private'; export function updateETSUnionType(original: ETSUnionType, types: readonly TypeNode[]): ETSUnionType { @@ -25,8 +25,8 @@ export function updateETSUnionType(original: ETSUnionType, types: readonly TypeN const update = updateThenAttach(ETSUnionType.updateETSUnionType, attachModifiers); const newNode = update(original, types); - if (NodeCache.getInstance().has(original)) { - NodeCache.getInstance().refresh(original, newNode); + if (NodeCacheFactory.getInstance().has(original)) { + NodeCacheFactory.getInstance().refresh(original, newNode); } return newNode; } diff --git a/koala-wrapper/src/arkts-api/node-utilities/Identifier.ts b/koala-wrapper/src/arkts-api/node-utilities/Identifier.ts index 3a1fff783..98928baf9 100644 --- a/koala-wrapper/src/arkts-api/node-utilities/Identifier.ts +++ b/koala-wrapper/src/arkts-api/node-utilities/Identifier.ts @@ -15,7 +15,7 @@ import { Identifier, TypeNode } from '../../generated'; import { isSameNativeObject } from '../peers/ArktsObject'; -import { NodeCache } from '../utilities/nodeCache'; +import { NodeCacheFactory } from '../utilities/nodeCache'; import { attachModifiers, updateThenAttach } from '../utilities/private'; export function updateIdentifier(original: Identifier, name: string, typeAnnotation?: TypeNode): Identifier { @@ -25,8 +25,8 @@ export function updateIdentifier(original: Identifier, name: string, typeAnnotat const update = updateThenAttach(Identifier.update2Identifier, attachModifiers); const newNode = update(original, name, typeAnnotation); - if (NodeCache.getInstance().has(original)) { - NodeCache.getInstance().refresh(original, newNode); + if (NodeCacheFactory.getInstance().has(original)) { + NodeCacheFactory.getInstance().refresh(original, newNode); } return newNode; } diff --git a/koala-wrapper/src/arkts-api/node-utilities/MethodDefinition.ts b/koala-wrapper/src/arkts-api/node-utilities/MethodDefinition.ts index 6318a0c84..6c8e52b84 100644 --- a/koala-wrapper/src/arkts-api/node-utilities/MethodDefinition.ts +++ b/koala-wrapper/src/arkts-api/node-utilities/MethodDefinition.ts @@ -20,7 +20,7 @@ import { MethodDefinition } from '../types'; import { updateThenAttach } from '../utilities/private'; import { Es2pandaMethodDefinitionKind } from '../../generated/Es2pandaEnums'; import { ScriptFunction } from '../../generated'; -import { NodeCache } from '../utilities/nodeCache'; +import { NodeCacheFactory } from '../utilities/nodeCache'; export function updateMethodDefinition( original: MethodDefinition, @@ -44,8 +44,8 @@ export function updateMethodDefinition( node.setOverloads(original.overloads) ); const newNode = update(original, kind, key, value, modifiers, isComputed); - if (NodeCache.getInstance().has(original)) { - NodeCache.getInstance().refresh(original, newNode); + if (NodeCacheFactory.getInstance().has(original)) { + NodeCacheFactory.getInstance().refresh(original, newNode); } return newNode; } diff --git a/koala-wrapper/src/arkts-api/node-utilities/Property.ts b/koala-wrapper/src/arkts-api/node-utilities/Property.ts index fdb1443c5..7812409bf 100644 --- a/koala-wrapper/src/arkts-api/node-utilities/Property.ts +++ b/koala-wrapper/src/arkts-api/node-utilities/Property.ts @@ -15,7 +15,7 @@ import { Expression, Property } from '../../generated'; import { isSameNativeObject } from '../peers/ArktsObject'; -import { NodeCache } from '../utilities/nodeCache'; +import { NodeCacheFactory } from '../utilities/nodeCache'; import { attachModifiers, updateThenAttach } from '../utilities/private'; export function updateProperty(original: Property, key?: Expression, value?: Expression): Property { @@ -25,8 +25,8 @@ export function updateProperty(original: Property, key?: Expression, value?: Exp const update = updateThenAttach(Property.updateProperty, attachModifiers); const newNode = update(original, key, value); - if (NodeCache.getInstance().has(original)) { - NodeCache.getInstance().refresh(original, newNode); + if (NodeCacheFactory.getInstance().has(original)) { + NodeCacheFactory.getInstance().refresh(original, newNode); } return newNode; diff --git a/koala-wrapper/src/arkts-api/node-utilities/ReturnStatement.ts b/koala-wrapper/src/arkts-api/node-utilities/ReturnStatement.ts index 984e0aa5d..c34bd24f3 100644 --- a/koala-wrapper/src/arkts-api/node-utilities/ReturnStatement.ts +++ b/koala-wrapper/src/arkts-api/node-utilities/ReturnStatement.ts @@ -15,7 +15,7 @@ import { Expression, ReturnStatement } from '../../generated'; import { isSameNativeObject } from '../peers/ArktsObject'; -import { NodeCache } from '../utilities/nodeCache'; +import { NodeCacheFactory } from '../utilities/nodeCache'; import { attachModifiers, updateThenAttach } from '../utilities/private'; export function updateReturnStatement(original: ReturnStatement, argument?: Expression): ReturnStatement { @@ -25,8 +25,8 @@ export function updateReturnStatement(original: ReturnStatement, argument?: Expr const update = updateThenAttach(ReturnStatement.update1ReturnStatement, attachModifiers); const newNode = update(original, argument); - if (NodeCache.getInstance().has(original)) { - NodeCache.getInstance().refresh(original, newNode); + if (NodeCacheFactory.getInstance().has(original)) { + NodeCacheFactory.getInstance().refresh(original, newNode); } return newNode; } diff --git a/koala-wrapper/src/arkts-api/node-utilities/ScriptFunction.ts b/koala-wrapper/src/arkts-api/node-utilities/ScriptFunction.ts index 16e30342a..6f5f55bc9 100644 --- a/koala-wrapper/src/arkts-api/node-utilities/ScriptFunction.ts +++ b/koala-wrapper/src/arkts-api/node-utilities/ScriptFunction.ts @@ -16,7 +16,7 @@ import { FunctionSignature, ScriptFunction } from '../../generated'; import { isSameNativeObject } from '../peers/ArktsObject'; import { AstNode } from '../peers/AstNode'; -import { NodeCache } from '../utilities/nodeCache'; +import { NodeCacheFactory } from '../utilities/nodeCache'; import { updateThenAttach } from '../utilities/private'; export function updateScriptFunction( @@ -44,8 +44,8 @@ export function updateScriptFunction( (node: ScriptFunction, original: ScriptFunction) => node.setAnnotations(original.annotations) ); const newNode = update(original, databody, datasignature, datafuncFlags, dataflags); - if (NodeCache.getInstance().has(original)) { - NodeCache.getInstance().refresh(original, newNode); + if (NodeCacheFactory.getInstance().has(original)) { + NodeCacheFactory.getInstance().refresh(original, newNode); } return newNode; } diff --git a/koala-wrapper/src/arkts-api/node-utilities/TSTypeAliasDeclaration.ts b/koala-wrapper/src/arkts-api/node-utilities/TSTypeAliasDeclaration.ts index a1d47e0e1..16812cd1f 100644 --- a/koala-wrapper/src/arkts-api/node-utilities/TSTypeAliasDeclaration.ts +++ b/koala-wrapper/src/arkts-api/node-utilities/TSTypeAliasDeclaration.ts @@ -15,7 +15,7 @@ import { Identifier, TSTypeAliasDeclaration, TSTypeParameterDeclaration, TypeNode } from '../../generated'; import { isSameNativeObject } from '../peers/ArktsObject'; -import { NodeCache } from '../utilities/nodeCache'; +import { NodeCacheFactory } from '../utilities/nodeCache'; import { attachModifiers, updateThenAttach } from '../utilities/private'; export function updateTSTypeAliasDeclaration( @@ -38,8 +38,8 @@ export function updateTSTypeAliasDeclaration( (node: TSTypeAliasDeclaration, original: TSTypeAliasDeclaration) => node.setAnnotations(original.annotations) ); const newNode = update(original, id, typeParams, typeAnnotation); - if (NodeCache.getInstance().has(original)) { - NodeCache.getInstance().refresh(original, newNode); + if (NodeCacheFactory.getInstance().has(original)) { + NodeCacheFactory.getInstance().refresh(original, newNode); } return newNode; } diff --git a/koala-wrapper/src/arkts-api/node-utilities/VariableDeclarator.ts b/koala-wrapper/src/arkts-api/node-utilities/VariableDeclarator.ts index 1f1739da2..ebb556c3d 100644 --- a/koala-wrapper/src/arkts-api/node-utilities/VariableDeclarator.ts +++ b/koala-wrapper/src/arkts-api/node-utilities/VariableDeclarator.ts @@ -19,7 +19,7 @@ import { VariableDeclarator } from '../types'; import { attachModifiers, updateThenAttach } from '../utilities/private'; import { Es2pandaVariableDeclaratorFlag } from '../../generated/Es2pandaEnums'; import { AstNode } from '../peers/AstNode'; -import { NodeCache } from '../utilities/nodeCache'; +import { NodeCacheFactory } from '../utilities/nodeCache'; export function updateVariableDeclarator( original: VariableDeclarator, @@ -37,8 +37,8 @@ export function updateVariableDeclarator( const update = updateThenAttach(VariableDeclarator.update, attachModifiers); const newNode = update(original, flag, name, initializer); - if (NodeCache.getInstance().has(original)) { - NodeCache.getInstance().refresh(original, newNode); + if (NodeCacheFactory.getInstance().has(original)) { + NodeCacheFactory.getInstance().refresh(original, newNode); } return newNode; } diff --git a/koala-wrapper/src/arkts-api/peers/AstNode.ts b/koala-wrapper/src/arkts-api/peers/AstNode.ts index 2a8b90fa6..43f0b676b 100644 --- a/koala-wrapper/src/arkts-api/peers/AstNode.ts +++ b/koala-wrapper/src/arkts-api/peers/AstNode.ts @@ -21,6 +21,7 @@ import { Es2pandaModifierFlags } from '../../generated/Es2pandaEnums'; import { ArktsObject } from './ArktsObject'; import { Es2pandaAstNodeType } from '../../Es2pandaEnums'; import { SourcePosition } from './SourcePosition'; +import { nodeByType } from '../node-by-type'; export abstract class AstNode extends ArktsObject { protected constructor(peer: KNativePointer) { @@ -104,6 +105,28 @@ export abstract class AstNode extends ArktsObject { return clonedNode as this; } + public findNodeInInnerChild(node: AstNode): boolean { + return global.es2panda._AstNodeFindNodeInInnerChild(global.context, this.peer, node.peer); + } + + public findInnerChild(nodeType: Es2pandaAstNodeType): T | undefined { + const childNodePeer = global.es2panda._AstNodeFindInnerChild(global.context, this.peer, nodeType); + if (childNodePeer === nullptr) { + return undefined; + } + const Node = nodeByType.get(nodeType) ?? UnsupportedNode; + return new Node(childNodePeer) as T; + } + + public findOuterParent(nodeType: Es2pandaAstNodeType): T | undefined { + const parentNodePeer = global.es2panda._AstNodeFindOuterParent(global.context, this.peer, nodeType); + if (parentNodePeer === nullptr) { + return undefined; + } + const Node = nodeByType.get(nodeType) ?? UnsupportedNode; + return new Node(parentNodePeer) as T; + } + public get parent(): AstNode | undefined { const parent = global.generatedEs2panda._AstNodeParent(global.context, this.peer); return unpackNode(parent); diff --git a/koala-wrapper/src/arkts-api/static/globalUtils.ts b/koala-wrapper/src/arkts-api/static/globalUtils.ts index 8e3a91074..29a9123ab 100644 --- a/koala-wrapper/src/arkts-api/static/globalUtils.ts +++ b/koala-wrapper/src/arkts-api/static/globalUtils.ts @@ -16,7 +16,7 @@ import { KNativePointer } from "@koalaui/interop"; import { Context } from "../peers/Context"; import { global } from "./global"; -import { clearNodeCache } from "../class-by-peer"; +import { clearNodeCache } from "../node-by-type"; export function getOrUpdateGlobalContext(peer: KNativePointer): Context { if (!global.compilerContext || global.context !== peer) { diff --git a/koala-wrapper/src/arkts-api/types.ts b/koala-wrapper/src/arkts-api/types.ts index d73f539d0..772fc39fb 100644 --- a/koala-wrapper/src/arkts-api/types.ts +++ b/koala-wrapper/src/arkts-api/types.ts @@ -39,11 +39,9 @@ import { import { proceedToState } from './utilities/public'; import { Es2pandaAstNodeType } from '../Es2pandaEnums'; import { AstNode } from './peers/AstNode'; -import { ArktsObject } from './peers/ArktsObject'; import { Config } from './peers/Config'; import { Context } from './peers/Context'; -import * as path from 'node:path'; -import { nodeByType } from './class-by-peer'; +import { nodeByType } from './node-by-type'; import { MemberExpression } from './to-be-generated/MemberExpression'; import { AnnotationUsage, diff --git a/koala-wrapper/src/arkts-api/utilities/nodeCache.ts b/koala-wrapper/src/arkts-api/utilities/nodeCache.ts index 0843ee6b6..d08fdb129 100644 --- a/koala-wrapper/src/arkts-api/utilities/nodeCache.ts +++ b/koala-wrapper/src/arkts-api/utilities/nodeCache.ts @@ -17,7 +17,7 @@ import { KNativePointer } from '@koalaui/interop'; import { Es2pandaAstNodeType } from '../../Es2pandaEnums'; import { AstNode, UnsupportedNode } from '../peers/AstNode'; import { global } from '../static/global'; -import { getOrPut, nodeByType } from '../class-by-peer'; +import { getOrPut, nodeByType } from '../node-by-type'; export interface AstNodeCacheValue { peer: KNativePointer; @@ -25,32 +25,18 @@ export interface AstNodeCacheValue { metadata?: AstNodeCacheValueMetadata; } -export interface AstNodeCacheValueMetadata { - callName?: string; - hasReceiver?: boolean; - isSetter?: boolean; - isGetter?: boolean; - hasMemoSkip?: boolean; - hasMemoIntrinsic?: boolean; - hasMemoEntry?: boolean; +export type AstNodeCacheValueMetadata = { + [key in string]?: any; } export class NodeCache { private _isCollected: boolean = false; private cacheMap: Map; - private static instance: NodeCache; - private constructor() { + constructor() { this.cacheMap = new Map(); } - static getInstance(): NodeCache { - if (!this.instance) { - this.instance = new NodeCache(); - } - return this.instance; - } - collect(node: AstNode, metadata?: AstNodeCacheValueMetadata): void { const peer = node.peer; const type = global.generatedEs2panda._AstNodeTypeConst(global.context, node.peer); @@ -101,3 +87,45 @@ export class NodeCache { }); } } + +export class NodeCacheFactory { + private cacheMap: Map; + private static instance: NodeCacheFactory; + + private constructor() { + this.cacheMap = new Map(); + } + + static getInstance(): NodeCacheFactory { + if (!this.instance) { + this.instance = new NodeCacheFactory(); + } + return this.instance; + } + + getCache(key: string): NodeCache { + if (!this.cacheMap.has(key)) { + this.cacheMap.set(key, new NodeCache()); + } + return this.cacheMap.get(key)!; + } + + clear(): void { + this.cacheMap.forEach((cache) => { + cache.clear(); + }); + this.cacheMap = new Map(); + } + + has(node: AstNode): boolean { + return Array.from(this.cacheMap.values()).some((cache) => cache.has(node)); + } + + refresh(original: AstNode, node: AstNode): void { + this.cacheMap.forEach((cache) => { + if (cache.has(original)) { + cache.refresh(original, node); + } + }); + } +} \ No newline at end of file diff --git a/koala-wrapper/src/arkts-api/utilities/public.ts b/koala-wrapper/src/arkts-api/utilities/public.ts index c630d2ac8..0096fba5b 100644 --- a/koala-wrapper/src/arkts-api/utilities/public.ts +++ b/koala-wrapper/src/arkts-api/utilities/public.ts @@ -39,7 +39,7 @@ import { type AnnotationUsage, } from '../../generated'; import { Program } from '../peers/Program'; -import { clearNodeCache } from '../class-by-peer'; +import { clearNodeCache } from '../node-by-type'; import { SourcePosition } from '../peers/SourcePosition'; import { MemberExpression } from '../to-be-generated/MemberExpression'; import { Es2pandaAstNodeType } from '../../Es2pandaEnums'; @@ -217,8 +217,12 @@ export function importDeclarationInsert(node: ETSImportDeclaration, program: Pro global.es2panda._InsertETSImportDeclarationAndParse(global.context, program.peer, node.peer); } -export function getProgramFromAstNode(node: AstNode): Program { - return new Program(global.es2panda._AstNodeProgram(global.context, node.peer)); +export function getProgramFromAstNode(node: AstNode): Program | undefined { + const programPeer = global.es2panda._AstNodeProgram(global.context, node.peer); + if (programPeer === nullptr) { + return undefined; + } + return new Program(programPeer); } export function hasModifierFlag(node: AstNode, flag: Es2pandaModifierFlags): boolean { diff --git a/koala-wrapper/src/reexport-for-generated.ts b/koala-wrapper/src/reexport-for-generated.ts index 37e3c6f56..cefb8764c 100644 --- a/koala-wrapper/src/reexport-for-generated.ts +++ b/koala-wrapper/src/reexport-for-generated.ts @@ -27,6 +27,6 @@ export { unpackObject, assertValidPeer } from "./arkts-api/utilities/private" -export { nodeByType } from "./arkts-api/class-by-peer" +export { nodeByType } from "./arkts-api/node-by-type" export { global } from "./arkts-api/static/global" export { Es2pandaMemberExpressionKind } from "./generated/Es2pandaEnums" -- Gitee