* 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 { expectNameInTypeReference, matchPrefix } from './arkts-utils';
import { ARKUI_IMPORT_PREFIX_NAMES, EXTERNAL_SOURCE_ARKTS_BUILTIN } from './predefines';
* Result for a single path segment.
*/
export interface PropertyPathSegmentResult {
segment: string;
type: arkts.TypeNode | null;
resolver: arkts.ClassPropertyResolver | arkts.MethodDefinitionResolver | null;
}
* Branch in property path tree. Multiple branches handle union types.
*/
export class TypePathBranch {
private _next: PropertyPathNode | null = null;
constructor(
public readonly type: arkts.TypeNode | null,
public readonly declaration: arkts.ClassDefinition | arkts.TSInterfaceDeclaration | null,
public readonly isTerminal: boolean
) {}
get next(): PropertyPathNode | null {
return this._next;
}
setNext(node: PropertyPathNode | null): void {
this._next = node;
}
getDeclaration(): arkts.ClassDefinition | arkts.TSInterfaceDeclaration | null {
return this.declaration;
}
}
* Node in property path tree.
*/
export class PropertyPathNode {
private _branches: TypePathBranch[] = [];
constructor(
public readonly propertyName: string,
public readonly property: arkts.ClassPropertyResolver | arkts.MethodDefinitionResolver | null,
public readonly exists: boolean
) {
if (!exists) {
this._branches = [];
}
}
get branches(): readonly TypePathBranch[] {
return this._branches;
}
addBranch(branch: TypePathBranch): void {
this._branches.push(branch);
}
hasBranches(): boolean {
return this._branches.length > 0;
}
get branchCount(): number {
return this._branches.length;
}
* Get all type paths to target depth. Only fully resolved paths included.
* @internal
*/
getAllTypePaths(targetDepth: number | undefined, currentDepth: number = 0): (arkts.TypeNode | null)[][] {
const paths: (arkts.TypeNode | null)[][] = [];
if (!this.exists || this._branches.length === 0) {
return paths;
}
const collectPaths = (
branch: TypePathBranch,
currentPath: (arkts.TypeNode | null)[],
depth: number
): void => {
const newPath = [...currentPath, branch.type];
const isAtTarget = targetDepth !== undefined && depth === targetDepth;
const canContinue = !branch.isTerminal && branch.next;
if (isAtTarget) {
paths.push(newPath);
return;
}
if (canContinue) {
for (const nextBranch of branch.next.branches) {
collectPaths(nextBranch, newPath, depth + 1);
}
}
};
for (const branch of this._branches) {
collectPaths(branch, [], currentDepth);
}
return paths;
}
* Get all resolver paths to target depth. Only fully resolved paths included.
* @internal
*/
getAllResolverPaths(targetDepth: number | undefined, currentDepth: number = 0): (arkts.ClassPropertyResolver | arkts.MethodDefinitionResolver | null)[][] {
const paths: (arkts.ClassPropertyResolver | arkts.MethodDefinitionResolver | null)[][] = [];
if (!this.exists || this._branches.length === 0) {
return paths;
}
const collectResolverPaths = (
node: PropertyPathNode,
branch: TypePathBranch,
currentPath: (arkts.ClassPropertyResolver | arkts.MethodDefinitionResolver | null)[],
depth: number
): void => {
const newPath = [...currentPath, node.property];
const isAtTarget = targetDepth !== undefined && depth === targetDepth;
const nextNode = branch.next;
const canContinue = !branch.isTerminal && nextNode;
if (isAtTarget) {
paths.push(newPath);
return;
}
if (canContinue) {
for (const nextBranch of nextNode.branches) {
collectResolverPaths(nextNode, nextBranch, newPath, depth + 1);
}
}
};
for (const branch of this._branches) {
collectResolverPaths(this, branch, [], currentDepth);
}
return paths;
}
* Get all result paths to target depth. Only fully resolved paths included.
* @internal
*/
getAllResultPaths(targetDepth: number | undefined, currentDepth: number = 0): PropertyPathSegmentResult[][] {
const paths: PropertyPathSegmentResult[][] = [];
if (!this.exists || this._branches.length === 0) {
return paths;
}
const collectResultPaths = (
node: PropertyPathNode,
branch: TypePathBranch,
currentPath: PropertyPathSegmentResult[],
depth: number
): void => {
const segmentResult: PropertyPathSegmentResult = {
segment: node.propertyName,
type: branch.type,
resolver: node.property
};
const newPath = [...currentPath, segmentResult];
const isAtTarget = targetDepth !== undefined && depth === targetDepth;
const nextNode = branch.next;
const canContinue = !branch.isTerminal && nextNode;
if (isAtTarget) {
paths.push(newPath);
return;
}
if (canContinue) {
for (const nextBranch of nextNode.branches) {
collectResultPaths(nextNode, nextBranch, newPath, depth + 1);
}
}
};
for (const branch of this._branches) {
collectResultPaths(this, branch, [], currentDepth);
}
return paths;
}
* Check if any branch can reach target depth.
*/
canReachEnd(targetDepth: number, currentDepth: number = 0): boolean {
if (currentDepth === targetDepth) {
return this.exists;
}
for (const branch of this._branches) {
if (!branch.isTerminal && branch.next) {
if (branch.next.canReachEnd(targetDepth, currentDepth + 1)) {
return true;
}
}
}
return false;
}
toString(indent: string = ''): string {
return PropertyPathFormatter.formatNode(this, indent);
}
}
export interface PropertyPathOptions {
enableWildcard?: boolean;
}
class PropertyPathFormatter {
private static readonly NOT_FOUND_LABEL = ': [NOT FOUND]';
private static readonly FOUND_LABEL = ': [FOUND]';
private static readonly BRANCHES_LABEL = ' branch(es)';
private static readonly PRIMITIVE_TYPE_LABEL = 'Primitive/Non-class';
private static readonly CLASS_TYPE_LABEL = 'Class: ';
private static readonly INTERFACE_TYPE_LABEL = 'Interface: ';
private static readonly UNKNOWN_TYPE_LABEL = '<unknown>';
private static readonly BRANCH_PREFIX = ' └─ Branch: ';
private static readonly TERMINAL_SUFFIX = ' [TERMINAL]';
private static readonly INDENT_STEP = ' ';
private static readonly RESULT_PREFIX = 'PropertyPathResult (segments: ';
private static readonly SEGMENTS_LABEL = ', fullyResolved: ';
private static readonly POSSIBLY_RESOLVED_LABEL = ', possiblyResolved: ';
private static readonly RESULT_SUFFIX = ')';
static formatNode(node: PropertyPathNode, indent: string = ''): string {
if (!node.exists) {
return `${indent}${node.propertyName}${PropertyPathFormatter.NOT_FOUND_LABEL}\n`;
}
let result = `${indent}${node.propertyName}${PropertyPathFormatter.FOUND_LABEL} (${node.branchCount}${PropertyPathFormatter.BRANCHES_LABEL})\n`;
for (const branch of node.branches) {
const typeInfo = PropertyPathFormatter.getTypeInfo(branch);
result += `${indent}${PropertyPathFormatter.BRANCH_PREFIX}${typeInfo}`;
if (branch.isTerminal) {
result += `${PropertyPathFormatter.TERMINAL_SUFFIX}\n`;
} else if (branch.next) {
result += '\n' + PropertyPathFormatter.formatNode(branch.next, indent + PropertyPathFormatter.INDENT_STEP);
} else {
result += '\n';
}
}
return result;
}
private static getTypeInfo(branch: TypePathBranch): string {
if (branch.declaration) {
if (arkts.isClassDefinition(branch.declaration)) {
const ident = branch.declaration.ident;
const name = arkts.isIdentifier(ident) ? ident.name : PropertyPathFormatter.UNKNOWN_TYPE_LABEL;
return `${PropertyPathFormatter.CLASS_TYPE_LABEL}${name}`;
}
if (arkts.isTSInterfaceDeclaration(branch.declaration)) {
const id = branch.declaration.id;
const name = arkts.isIdentifier(id) ? id.name : PropertyPathFormatter.UNKNOWN_TYPE_LABEL;
return `${PropertyPathFormatter.INTERFACE_TYPE_LABEL}${name}`;
}
}
if (branch.type && arkts.isETSTypeReference(branch.type)) {
const baseName = branch.type.baseName;
if (baseName && arkts.isIdentifier(baseName)) {
return `Type: ${baseName.name}`;
}
}
return PropertyPathFormatter.PRIMITIVE_TYPE_LABEL;
}
static formatResult(result: PropertyPathResult): string {
const possiblyResolvedPart = result.possiblyResolved ?
`${PropertyPathFormatter.POSSIBLY_RESOLVED_LABEL}${result.possiblyResolved}` : '';
return `${PropertyPathFormatter.RESULT_PREFIX}${result.segmentCount}${PropertyPathFormatter.SEGMENTS_LABEL}${result.fullyResolved}${possiblyResolvedPart}${PropertyPathFormatter.RESULT_SUFFIX}\n` +
PropertyPathFormatter.formatNode(result.root);
}
}
* Result of property path resolution.
*/
export class PropertyPathResult {
private _root: PropertyPathNode;
private _possiblyResolved: boolean = false;
constructor(root: PropertyPathNode, public readonly segmentCount: number) {
this._root = root;
}
get root(): PropertyPathNode {
return this._root;
}
setPossiblyResolved(value: boolean): void {
this._possiblyResolved = value;
}
get possiblyResolved(): boolean {
return this._possiblyResolved;
}
get fullyResolved(): boolean {
if (this.segmentCount === 0) {
return false;
}
if (this.segmentCount === 1) {
return this._root.exists;
}
return this._root.canReachEnd(this.segmentCount - 1);
}
getAllTypePaths(): (arkts.TypeNode | null)[][] {
return this._root.getAllTypePaths(this.segmentCount - 1);
}
getAllResolverPaths(): (arkts.ClassPropertyResolver | arkts.MethodDefinitionResolver | null)[][] {
return this._root.getAllResolverPaths(this.segmentCount - 1);
}
getAllResultPaths(): PropertyPathSegmentResult[][] {
return this._root.getAllResultPaths(this.segmentCount - 1);
}
toString(): string {
return PropertyPathFormatter.formatResult(this);
}
}
interface NextSegmentContext {
type: arkts.TypeNode;
pathSegments: string[];
segmentIndex: number;
currentNode: PropertyPathNode;
isLastSegment: boolean;
}
interface HandleResult {
handled: boolean;
branch: TypePathBranch | null;
}
interface NextSegmentHandler {
canHandle(context: NextSegmentContext, nextSegment: string | undefined): boolean;
handle(context: NextSegmentContext, builder: PropertyPathTreeBuilder): HandleResult;
}
export class ArrayIndexSegmentHandler implements NextSegmentHandler {
private static readonly NUMERIC_SEGMENT_PATTERN = /^\d+$/;
canHandle(context: NextSegmentContext, nextSegment: string | undefined): boolean {
if (nextSegment === undefined || !ArrayIndexSegmentHandler.isNumericSegment(nextSegment)) {
return false;
}
const testResolver = arkts.ArrayTypeResolver.resolve(context.type);
if (testResolver !== null) {
return true;
}
return extendsArrayType(context.type);
}
handle(context: NextSegmentContext, builder: PropertyPathTreeBuilder): HandleResult {
const { type, pathSegments, segmentIndex } = context;
const arrayIndexNode = builder.buildArrayElementNode(
type,
pathSegments,
segmentIndex + 1
);
const branch = new TypePathBranch(type, null, false);
branch.setNext(arrayIndexNode);
return { handled: true, branch };
}
static isNumericSegment(segment: string): boolean {
return ArrayIndexSegmentHandler.NUMERIC_SEGMENT_PATTERN.test(segment);
}
}
export class ArrayLengthSegmentHandler implements NextSegmentHandler {
private static readonly LENGTH_PROPERTY = 'length';
canHandle(context: NextSegmentContext, nextSegment: string | undefined): boolean {
if (nextSegment !== ArrayLengthSegmentHandler.LENGTH_PROPERTY) {
return false;
}
const testResolver = arkts.ArrayTypeResolver.resolve(context.type);
if (testResolver !== null) {
return true;
}
return extendsArrayType(context.type);
}
handle(context: NextSegmentContext, builder: PropertyPathTreeBuilder): HandleResult {
const { type, pathSegments, segmentIndex } = context;
const lengthNode = builder.buildLengthNode(type, pathSegments, segmentIndex + 1);
const branch = new TypePathBranch(type, null, false);
branch.setNext(lengthNode);
return { handled: true, branch };
}
}
export class CollectionSizeSegmentHandler implements NextSegmentHandler {
private static readonly SIZE_PROPERTY = 'size';
private static readonly SIZE_COLLECTION_TYPES = ['Set', 'Map'];
canHandle(context: NextSegmentContext, nextSegment: string | undefined): boolean {
if (nextSegment !== CollectionSizeSegmentHandler.SIZE_PROPERTY) {
return false;
}
if (arkts.isETSTypeReference(context.type)) {
const baseName = context.type.baseName;
if (arkts.isIdentifier(baseName)) {
if (CollectionSizeSegmentHandler.SIZE_COLLECTION_TYPES.includes(baseName.name)) {
return true;
}
}
}
return CollectionSizeSegmentHandler.SIZE_COLLECTION_TYPES.some(collectionType =>
extendsCollectionType(context.type, collectionType as 'Set' | 'Map')
);
}
handle(context: NextSegmentContext, builder: PropertyPathTreeBuilder): HandleResult {
const { type, pathSegments, segmentIndex } = context;
const sizeNode = builder.buildSizeNode(type, pathSegments, segmentIndex + 1);
const branch = new TypePathBranch(type, null, false);
branch.setNext(sizeNode);
return { handled: true, branch };
}
}
function findSuperClassFromArkUI(type: arkts.Expression): boolean {
if (!arkts.isETSTypeReference(type)) {
return false;
}
const nameNode = expectNameInTypeReference(type);
const decl = nameNode ? arkts.getPeerIdentifierDecl(nameNode.peer) : undefined;
const moduleName = decl ? arkts.getProgramFromAstNode(decl)?.moduleName : undefined;
return !!moduleName && matchPrefix(ARKUI_IMPORT_PREFIX_NAMES, moduleName);
}
* Extract element types from a class extending Array<T> or ObservedArray<T>.
* First tries ObservedArray superClass, then falls back to Array.
*/
function getArrayElementTypesFromExtendingClass(type: arkts.TypeNode): arkts.TypeNode[] {
if (!arkts.isETSTypeReference(type)) {
return [];
}
const baseName = type.baseName;
if (!baseName || !arkts.isIdentifier(baseName)) {
return [];
}
const decl = arkts.getPeerIdentifierDecl(baseName.peer);
if (!decl || !arkts.isClassDefinition(decl)) {
return [];
}
let superClass = arkts.findSuperClassByName(decl, 'ObservedArray');
if (!superClass || !arkts.isETSTypeReference(superClass) || !findSuperClassFromArkUI(superClass)) {
superClass = arkts.findSuperClassByName(decl, 'Array');
} else {
superClass = arkts.factory.createETSTypeReference(
arkts.factory.createETSTypeReferencePart(
arkts.factory.createIdentifier('Array'),
superClass.part?.typeParams?.clone()
)
);
}
if (!superClass || !arkts.isETSTypeReference(superClass)) {
return [];
}
const resolver = arkts.ArrayTypeResolver.resolve(superClass);
if (resolver) {
return resolver.resolvedElementTypes;
}
return [];
}
function extendsArrayType(type: arkts.TypeNode): boolean {
const elementTypes = getArrayElementTypesFromExtendingClass(type);
return elementTypes.length > 0;
}
function extendsCollectionType(type: arkts.TypeNode, collectionName: 'Set' | 'Map'): boolean {
if (arkts.isETSTypeReference(type)) {
const baseName = type.baseName;
if (arkts.isIdentifier(baseName)) {
const decl = arkts.getPeerIdentifierDecl(baseName.peer);
if (decl && arkts.isClassDefinition(decl)) {
const superClass = arkts.findSuperClassByName(decl, collectionName);
return superClass !== null;
}
}
}
return false;
}
class WildcardSegmentHandler implements NextSegmentHandler {
private static readonly WILDCARD_SYMBOL = '*';
private static readonly MINIMUM_PATH_LENGTH = 2;
private static readonly WILDCARD_COLLECTION_TYPES = ['Array', 'Map', 'Set'];
canHandle(context: NextSegmentContext, nextSegment: string | undefined): boolean {
if (nextSegment !== WildcardSegmentHandler.WILDCARD_SYMBOL) {
return false;
}
const nextSegmentIndex = context.segmentIndex + 1;
const isNextSegmentLast = nextSegmentIndex === context.pathSegments.length - 1;
if (!isNextSegmentLast) {
return false;
}
if (context.pathSegments.length < WildcardSegmentHandler.MINIMUM_PATH_LENGTH) {
return false;
}
return this.isSupportedType(context.type);
}
private isSupportedType(type: arkts.TypeNode): boolean {
if (arkts.ArrayTypeResolver.resolve(type) !== null) {
return true;
}
if (arkts.isETSTypeReference(type)) {
const baseName = type.baseName;
if (arkts.isIdentifier(baseName)) {
return WildcardSegmentHandler.WILDCARD_COLLECTION_TYPES.includes(baseName.name) ||
this.getClassDefinitionFromType(type) !== null;
}
}
return false;
}
private getClassDefinitionFromType(type: arkts.TypeNode): arkts.ClassDefinition | arkts.TSInterfaceDeclaration | null {
if (!arkts.isETSTypeReference(type)) {
return null;
}
const baseName = type.baseName;
if (!arkts.isIdentifier(baseName)) {
return null;
}
const decl = arkts.getPeerIdentifierDecl(baseName.peer);
if (!decl) {
return null;
}
if (arkts.isClassDefinition(decl)) {
return decl;
}
if (arkts.isTSInterfaceDeclaration(decl)) {
return decl;
}
return null;
}
handle(context: NextSegmentContext, builder: PropertyPathTreeBuilder): HandleResult {
const { type, pathSegments, segmentIndex } = context;
const wildcardNode = builder.buildWildcardNode(type, pathSegments, segmentIndex + 1);
const branch = new TypePathBranch(type, null, false);
branch.setNext(wildcardNode);
return { handled: true, branch };
}
}
export class DefaultPropertyHandler implements NextSegmentHandler {
canHandle(_context: NextSegmentContext, _nextSegment: string | undefined): boolean {
return true;
}
handle(context: NextSegmentContext, builder: PropertyPathTreeBuilder): HandleResult {
const { type, pathSegments, segmentIndex, isLastSegment } = context;
const nextDeclaration = PropertyPathTreeBuilder.getClassDefinitionFromType(type);
const isTerminal = !nextDeclaration || isLastSegment || PropertyPathTreeBuilder.isPrimitiveWrapperClass(nextDeclaration);
const branch = new TypePathBranch(type, nextDeclaration, isTerminal);
if (!isTerminal && nextDeclaration) {
branch.setNext(builder.buildNode(nextDeclaration, pathSegments, segmentIndex + 1));
}
return { handled: true, branch };
}
}
export class PropertyPathTreeBuilder {
private static readonly PATH_SEPARATOR = '.';
private static readonly MINIMUM_SEGMENT_LENGTH = 1;
private static readonly EMPTY_PROPERTY_NAME = '';
private segmentHandlers: NextSegmentHandler[] = [];
private options: PropertyPathOptions;
private hasExtendedArrayType: boolean = false;
constructor(options: PropertyPathOptions = {}) {
this.options = options;
this.registerHandlers();
}
private registerHandlers(): void {
this.segmentHandlers = [
new ArrayIndexSegmentHandler(),
new ArrayLengthSegmentHandler(),
new CollectionSizeSegmentHandler(),
];
if (this.options.enableWildcard) {
this.segmentHandlers.push(new WildcardSegmentHandler());
}
this.segmentHandlers.push(new DefaultPropertyHandler());
}
private findProperty(
classDef: arkts.ClassDefinition,
name: string
): arkts.ClassPropertyResolver | arkts.MethodDefinitionResolver | null {
const resolver = arkts.ClassDefinitionResolver.resolve(classDef);
return resolver.findPropertyResolver(name);
}
private findPropertyInInterface(
interfaceDecl: arkts.TSInterfaceDeclaration,
name: string
): arkts.ClassPropertyResolver | arkts.MethodDefinitionResolver | null {
const resolver = arkts.TSInterfaceDeclarationResolver.resolve(interfaceDecl);
return resolver.findPropertyResolver(name);
}
private findPropertyInDeclaration(
declaration: arkts.ClassDefinition | arkts.TSInterfaceDeclaration,
name: string
): arkts.ClassPropertyResolver | arkts.MethodDefinitionResolver | null {
if (arkts.isClassDefinition(declaration)) {
return this.findProperty(declaration, name);
} else if (arkts.isTSInterfaceDeclaration(declaration)) {
return this.findPropertyInInterface(declaration, name);
}
return null;
}
private getResolvedTypes(property: arkts.ClassPropertyResolver | arkts.MethodDefinitionResolver): arkts.TypeNode[] {
return property.resolvedTypes;
}
* Get ClassDefinition or TSInterfaceDeclaration from a TypeNode.
* @internal
*/
static getClassDefinitionFromType(type: arkts.TypeNode): arkts.ClassDefinition | arkts.TSInterfaceDeclaration | null {
if (!arkts.isETSTypeReference(type)) {
return null;
}
const baseName = type.baseName;
if (!arkts.isIdentifier(baseName)) {
return null;
}
const decl = arkts.getPeerIdentifierDecl(baseName.peer);
if (!decl) {
return null;
}
if (arkts.isClassDefinition(decl)) {
return decl;
}
if (arkts.isTSInterfaceDeclaration(decl)) {
return decl;
}
return null;
}
* Check if declaration is from ArkTS builtin module (String, Number, Boolean, etc.).
* @internal
*/
static isPrimitiveWrapperClass(declaration: arkts.ClassDefinition | arkts.TSInterfaceDeclaration | null): boolean {
if (!declaration) {
return false;
}
const moduleName = arkts.getProgramFromAstNode(declaration)?.moduleName;
return !!moduleName && matchPrefix(EXTERNAL_SOURCE_ARKTS_BUILTIN, moduleName);
}
static isArrayType(type: arkts.TypeNode): boolean {
const resolver = arkts.ArrayTypeResolver.resolve(type);
return resolver !== null;
}
* Resolve array element types (direct T[]/Array<T>, ObservedArray<T>, or classes extending them).
* @internal
*/
static resolveArrayElementTypes(type: arkts.TypeNode): arkts.TypeNode[] {
const observedArrayElementTypes = PropertyPathTreeBuilder.extractElementTypesFromObservedArray(type);
if (observedArrayElementTypes.length > 0) {
return observedArrayElementTypes;
}
const resolver = arkts.ArrayTypeResolver.resolve(type);
if (resolver) {
return resolver.resolvedElementTypes;
}
return getArrayElementTypesFromExtendingClass(type);
}
* Extract element types from direct ObservedArray<T> type reference.
* Prevents ArrayTypeResolver from traversing up to Array<T>.
* @internal
*/
private static extractElementTypesFromObservedArray(type: arkts.TypeNode): arkts.TypeNode[] {
if (!arkts.isETSTypeReference(type)) {
return [];
}
const baseName = type.baseName;
if (!baseName || !arkts.isIdentifier(baseName) || baseName.name !== 'ObservedArray') {
return [];
}
const part = type.part;
if (!part || !part.typeParams || !part.typeParams.params || part.typeParams.params.length === 0) {
return [];
}
return [part.typeParams.params[0]];
}
* Check if element types came from an extended Array class (not direct Array type).
* @internal
*/
static isExtendedArrayType(type: arkts.TypeNode): boolean {
const resolver = arkts.ArrayTypeResolver.resolve(type);
if (resolver) {
return false;
}
const elementTypes = getArrayElementTypesFromExtendingClass(type);
return elementTypes.length > 0;
}
* Build node for array element access (e.g., "a.0.b").
* @internal
*/
buildArrayElementNode(
type: arkts.TypeNode,
pathSegments: string[],
segmentIndex: number
): PropertyPathNode {
const arrayIndex = pathSegments[segmentIndex];
const { elementTypes, isExtendedArray } = this.resolveElementTypes(type);
if (isExtendedArray) {
this.hasExtendedArrayType = true;
}
if (elementTypes.length === 0) {
return new PropertyPathNode(arrayIndex, null, false);
}
const node = new PropertyPathNode(arrayIndex, null, true);
const isLastSegment = segmentIndex === pathSegments.length - 1;
for (const elementType of elementTypes) {
const nextDeclaration = PropertyPathTreeBuilder.getClassDefinitionFromType(elementType);
const nextSegment = isLastSegment ? undefined : pathSegments[segmentIndex + 1];
const isNextSegmentNumeric = nextSegment !== undefined && ArrayIndexSegmentHandler.isNumericSegment(nextSegment);
const isElementArrayType = PropertyPathTreeBuilder.isArrayType(elementType);
const isTerminal: boolean = (!nextDeclaration && !isElementArrayType) ||
(isLastSegment && !isNextSegmentNumeric) ||
(!!nextDeclaration && !isElementArrayType && PropertyPathTreeBuilder.isPrimitiveWrapperClass(nextDeclaration));
const branch = new TypePathBranch(elementType, nextDeclaration, isTerminal);
node.addBranch(branch);
if (!isTerminal) {
if (isElementArrayType && isNextSegmentNumeric) {
branch.setNext(this.buildArrayElementNode(elementType, pathSegments, segmentIndex + 1));
} else if (nextDeclaration) {
branch.setNext(this.buildNode(nextDeclaration, pathSegments, segmentIndex + 1));
}
}
}
if (isExtendedArray && node.branchCount > 0 && !isLastSegment) {
this.addPlaceholderBranchIfNeeded(node, pathSegments, segmentIndex);
}
return node;
}
private resolveElementTypes(type: arkts.TypeNode): { elementTypes: arkts.TypeNode[], isExtendedArray: boolean } {
const observedArrayElementTypes = PropertyPathTreeBuilder.extractElementTypesFromObservedArray(type);
if (observedArrayElementTypes.length > 0) {
const elementTypes = observedArrayElementTypes[0] instanceof arkts.ETSUnionType
? [...observedArrayElementTypes[0].types]
: observedArrayElementTypes;
return { elementTypes, isExtendedArray: true };
}
const directResolver = arkts.ArrayTypeResolver.resolve(type);
if (directResolver) {
return { elementTypes: directResolver.resolvedElementTypes, isExtendedArray: false };
}
const elementTypes = getArrayElementTypesFromExtendingClass(type);
return { elementTypes, isExtendedArray: elementTypes.length > 0 };
}
private addPlaceholderBranchIfNeeded(
node: PropertyPathNode,
pathSegments: string[],
segmentIndex: number
): void {
let canContinue = false;
for (const branch of node.branches) {
if (!branch.isTerminal && branch.next) {
canContinue = true;
break;
}
}
if (!canContinue) {
const placeholderBranch = new TypePathBranch(null, null, false);
placeholderBranch.setNext(this.createPlaceholderNodes(pathSegments, segmentIndex + 1));
node.addBranch(placeholderBranch);
}
}
* Create placeholder nodes for remaining segments when element type can't be resolved.
* @internal
*/
private createPlaceholderNodes(pathSegments: string[], startIndex: number): PropertyPathNode {
const segmentName = pathSegments[startIndex];
const isLast = startIndex === pathSegments.length - 1;
const node = new PropertyPathNode(segmentName, null, true);
const branch = new TypePathBranch(null, null, isLast);
node.addBranch(branch);
if (!isLast) {
branch.setNext(this.createPlaceholderNodes(pathSegments, startIndex + 1));
}
return node;
}
* Build node for 'length' property (e.g., "a.length").
* @internal
*/
buildLengthNode(
_type: arkts.TypeNode,
pathSegments: string[],
segmentIndex: number
): PropertyPathNode {
const lengthName = pathSegments[segmentIndex];
const node = new PropertyPathNode(lengthName, null, true);
const branch = new TypePathBranch(null, null, true);
node.addBranch(branch);
return node;
}
* Build node for 'size' property (e.g., "a.size").
* @internal
*/
buildSizeNode(
_type: arkts.TypeNode,
pathSegments: string[],
segmentIndex: number
): PropertyPathNode {
const sizeName = pathSegments[segmentIndex];
const node = new PropertyPathNode(sizeName, null, true);
const branch = new TypePathBranch(null, null, true);
node.addBranch(branch);
return node;
}
* Build node for wildcard segment "*" (e.g., "a.*").
* @internal
*/
buildWildcardNode(
type: arkts.TypeNode,
pathSegments: string[],
segmentIndex: number
): PropertyPathNode {
const wildcardName = pathSegments[segmentIndex];
const isLastSegment = segmentIndex === pathSegments.length - 1;
const declaration = PropertyPathTreeBuilder.getClassDefinitionFromType(type);
const node = new PropertyPathNode(wildcardName, null, true);
const isTerminal = isLastSegment || PropertyPathTreeBuilder.isPrimitiveWrapperClass(declaration);
const branch = new TypePathBranch(null, declaration, isTerminal);
node.addBranch(branch);
if (!isTerminal && declaration) {
branch.setNext(this.buildNode(declaration, pathSegments, segmentIndex + 1));
}
return node;
}
* Build tree recursively using handler chain.
* @internal
*/
buildNode(
declaration: arkts.ClassDefinition | arkts.TSInterfaceDeclaration,
pathSegments: string[],
segmentIndex: number
): PropertyPathNode {
const propertyName = pathSegments[segmentIndex];
const property = this.findPropertyInDeclaration(declaration, propertyName);
const isLastSegment = segmentIndex === pathSegments.length - 1;
if (!property) {
return new PropertyPathNode(propertyName, null, false);
}
const node = new PropertyPathNode(propertyName, property, true);
const resolvedTypes = this.getResolvedTypes(property);
const nextSegment = isLastSegment ? undefined : pathSegments[segmentIndex + 1];
for (const type of resolvedTypes) {
const context: NextSegmentContext = {
type,
pathSegments,
segmentIndex,
currentNode: node,
isLastSegment
};
for (const handler of this.segmentHandlers) {
if (handler.canHandle(context, nextSegment)) {
const result = handler.handle(context, this);
if (result.handled && result.branch) {
node.addBranch(result.branch);
}
break;
}
}
}
return node;
}
* Build property path tree from class definition and path string.
*/
build(
classDef: arkts.ClassDefinition,
path: string
): PropertyPathResult {
const segments = path.split(PropertyPathTreeBuilder.PATH_SEPARATOR).filter(s => s.length >= PropertyPathTreeBuilder.MINIMUM_SEGMENT_LENGTH);
if (segments.length === 0) {
return new PropertyPathResult(
new PropertyPathNode(PropertyPathTreeBuilder.EMPTY_PROPERTY_NAME, null, false),
0
);
}
this.hasExtendedArrayType = false;
const root = this.buildNode(classDef, segments, 0);
const result = new PropertyPathResult(root, segments.length);
if (this.hasExtendedArrayType) {
result.setPossiblyResolved(true);
}
return result;
}
}
* Resolve property path like "a.b.c" into tree structure.
* Handles union types by creating multiple branches.
*/
export function resolvePropertyPath(
classDef: arkts.ClassDefinition,
path: string,
options?: PropertyPathOptions
): PropertyPathResult {
const builder = new PropertyPathTreeBuilder(options);
return builder.build(classDef, path);
}