* 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 path from 'path';
import * as arkts from '@koalaui/libarkts';
import { matchPrefix, removeRelativePathSuffix } from '../common/arkts-utils';
import {
ARKUI_IMPORT_PREFIX_NAMES,
CUSTOM_DIALOG_CONTROLLER_SOURCE_NAME,
CustomComponentNames,
CustomDialogNames,
DecoratorNames,
StructDecoratorNames,
GlobalReusePoolNames,
} from '../common/predefines';
import { DeclarationCollector } from '../common/declaration-collector';
import { MetaDataCollector } from '../common/metadata-collector';
import { hasDecorator } from './property-translators/utils';
import { ImportCollector } from '../common/import-collector';
import { getDeclOriPath } from './interop/utils';
export type EntryAnnoInfo = {
startPosition: arkts.SourcePosition;
endPosition: arkts.SourcePosition;
name: string;
annotation?: arkts.AnnotationUsage;
};
export type LocalImportInfo = {
symbolName: string;
localSource: string;
localTypeName: string;
}
export function getSystemResourcePath(): string {
return path.resolve(__dirname, './sysResource.js');
}
export function findImportSourceByName(importName: string): string {
const source = DeclarationCollector.getInstance().findExternalSourceFromName(importName);
if (!source) {
throw new Error(`cannot find import source by name: "${importName}".`);
}
return source;
}
export function findImportSourceByNode(declNode: arkts.AstNode): string {
const source = DeclarationCollector.getInstance().findExternalSourceFromNode(declNode);
if (!source) {
throw new Error(`cannot find import source by peer.`);
}
return source;
}
export function findLocalImport(
node: arkts.ETSImportDeclaration,
sourceName: string,
importedName: string
): arkts.Identifier | undefined {
const isFromSource = !!node.source && node.source.str === sourceName;
if (!isFromSource) return undefined;
const importSpecifier = node.specifiers.find(
(spec) => arkts.isImportSpecifier(spec) && !!spec.imported && spec.imported.name === importedName
) as arkts.ImportSpecifier | undefined;
return importSpecifier?.local ?? importSpecifier?.imported;
}
export function isStatic(node: arkts.AstNode): boolean {
return node.isStatic;
}
* Determine whether the type node includes null or undefined type.
*
* @param type type node
*/
export function hasNullOrUndefinedType(type: arkts.TypeNode): boolean {
let res: boolean = false;
if (arkts.isETSUnionType(type)) {
type.types.forEach((item: arkts.TypeNode) => {
res = res || hasNullOrUndefinedType(item);
});
}
if (arkts.isETSUndefinedType(type) || arkts.isETSNullType(type)) {
res = true;
}
return res;
}
export function getTypeParamsFromClassDecl(node: arkts.ClassDeclaration | undefined): readonly arkts.TSTypeParameter[] {
return node?.definition?.typeParams?.params ?? [];
}
export function getTypeNameFromTypeParameter(node: arkts.TSTypeParameter | undefined): string | undefined {
return node?.name?.name;
}
export function getGettersFromClassDecl(definition: arkts.ClassDefinition): arkts.MethodDefinition[] {
return definition.body.filter(
(member) =>
arkts.isMethodDefinition(member) &&
arkts.hasModifierFlag(member, arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_GETTER) &&
!hasDecorator(member, DecoratorNames.COMPUTED)
) as arkts.MethodDefinition[];
}
export function hasPropertyInAnnotation(annotation: arkts.AnnotationUsage, propertyName: string, allowDefault?: boolean): boolean {
return !!getPropertyInAnnotation(annotation, propertyName, allowDefault);
}
export function getPropertyInAnnotation(
annotation: arkts.AnnotationUsage,
propertyName: string,
allowDefault?: boolean
): arkts.ClassProperty | undefined {
return (allowDefault ? arkts.getAnnotationDeclarationProperties(annotation) : annotation.properties).find(
(annoProp: arkts.AstNode) =>
arkts.isClassProperty(annoProp) &&
annoProp.key &&
arkts.isIdentifier(annoProp.key) &&
annoProp.key.name === propertyName
) as arkts.ClassProperty | undefined;
}
export type CustomComponentInfo = {
name: string;
isDecl: boolean;
isCustomComponentClass: boolean;
annotations: CustomComponentAnontations;
lastBuilderParam?: string;
};
export type CustomComponentAnontations = {
component?: arkts.AnnotationUsage;
componentV2?: arkts.AnnotationUsage;
entry?: arkts.AnnotationUsage;
reusable?: arkts.AnnotationUsage;
reusableV2?: arkts.AnnotationUsage;
customLayout?: arkts.AnnotationUsage;
customDialog?: arkts.AnnotationUsage;
};
export type StructAnnoationInfo = {
isComponent: boolean;
isComponentV2: boolean;
isEntry: boolean;
isReusable: boolean;
isReusableV2: boolean;
isCustomLayout: boolean;
isCustomDialog: boolean;
};
export type ComponentType = {
hasEntry: boolean;
hasComponent: boolean;
hasComponentV2: boolean;
hasCustomDialog: boolean;
hasCustomLayout: boolean;
hasReusable: boolean;
hasReusableV2: boolean;
};
export type ClassInfo = {
className: string;
isFromStruct: boolean;
};
export function isCustomComponentAnnotation(
anno: arkts.AnnotationUsage,
decoratorName: StructDecoratorNames,
ignoreDecl?: boolean
): boolean {
if (!(!!anno.expr && arkts.isIdentifier(anno.expr) && anno.expr.name === decoratorName)) {
return false;
}
if (!ignoreDecl) {
const decl = arkts.getPeerIdentifierDecl(anno.expr.peer);
if (!decl) {
return false;
}
const moduleName = arkts.getProgramFromAstNode(decl)?.moduleName;
if (!moduleName || !matchPrefix(ARKUI_IMPORT_PREFIX_NAMES, moduleName)) {
return false;
}
DeclarationCollector.getInstance().collect(decl);
}
return true;
}
export function isBaseComponentClass(name: string): boolean {
return (
name === CustomComponentNames.COMPONENT_CLASS_NAME ||
name === CustomComponentNames.COMPONENT_V2_CLASS_NAME ||
name === CustomComponentNames.BASE_CUSTOM_DIALOG_NAME
);
}
export function collectCustomComponentScopeInfo(
node: arkts.ClassDeclaration | arkts.ETSStructDeclaration
): CustomComponentInfo | undefined {
const definition: arkts.ClassDefinition | undefined = node.definition;
if (!definition || !definition?.ident?.name) {
return undefined;
}
const isStruct = definition.isFromStruct || arkts.isETSStructDeclaration(node);
const isDecl: boolean = MetaDataCollector.getInstance().isDeclaration ||
arkts.hasModifierFlag(node, arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_DECLARE);
const isCustomComponentClass = !isStruct && isDecl && isBaseComponentClass(definition.ident.name);
const shouldIgnoreDecl = isStruct || isDecl;
if (!isStruct && !isCustomComponentClass) {
return undefined;
}
let annotations: CustomComponentAnontations = {};
if (!isCustomComponentClass) {
let isCustomComponent: boolean = false;
for (const anno of definition.annotations) {
const { isComponent, isComponentV2, isEntry, isReusable, isReusableV2, isCustomLayout, isCustomDialog } =
getAnnotationInfoForStruct(anno, shouldIgnoreDecl);
isCustomComponent ||= isComponent || isComponentV2 || isCustomDialog;
annotations = {
...annotations,
...(isComponent && !annotations?.component && { component: anno }),
...(isComponentV2 && !annotations?.componentV2 && { componentV2: anno }),
...(isEntry && !annotations?.entry && { entry: anno }),
...(isReusable && !annotations?.reusable && { reusable: anno }),
...(isReusableV2 && !annotations?.reusableV2 && { reusableV2: anno }),
...(isCustomLayout && !annotations?.customLayout && { customLayout: anno }),
...(isCustomDialog && !annotations?.customDialog && { customDialog: anno }),
};
}
if (!isCustomComponent) {
return undefined;
}
checkGlobalReuseAnnotation(annotations);
}
return {
name: definition.ident.name,
isDecl,
isCustomComponentClass,
annotations: annotations as CustomComponentAnontations,
};
}
function checkGlobalReuseAnnotation(annotations: CustomComponentAnontations): void {
const componentAnno = annotations.component ?? annotations.componentV2;
if (!componentAnno || componentAnno.properties.length === 0) {
return;
}
const hasReusePool = hasPropertyInAnnotation(componentAnno, GlobalReusePoolNames.REUSE_POOL);
const hasPoolAccepts = hasPropertyInAnnotation(componentAnno, GlobalReusePoolNames.POOL_ACCEPTS);
checkReusePoolAndPoolAcceptsBothSet(componentAnno, hasReusePool, hasPoolAccepts);
checkPoolAcceptsNotEmpty(componentAnno, hasReusePool, hasPoolAccepts);
}
function checkReusePoolAndPoolAcceptsBothSet(
componentAnno: arkts.AnnotationUsage,
hasReusePool: boolean,
hasPoolAccepts: boolean
): void {
if (hasReusePool === hasPoolAccepts) {
return;
}
const message = `'reusePool' and 'poolAccepts' must be both set. Neither can be omitted when using the global reuse pool.`;
const diagnosticKind = arkts.createDiagnosticKind(message, arkts.Es2pandaPluginDiagnosticType.ES2PANDA_PLUGIN_ERROR);
const diagnosticInfo = arkts.createDiagnosticInfo(diagnosticKind, componentAnno.startPosition);
if (!hasReusePool && hasPoolAccepts) {
const poolAcceptsProp = getPropertyInAnnotation(componentAnno, GlobalReusePoolNames.POOL_ACCEPTS);
if (!poolAcceptsProp || !poolAcceptsProp.value) {
return;
}
const annoName = (componentAnno.expr as arkts.Identifier).name;
const fixCode = `@${annoName}({ reusePool: ReusePoolOwnership.SHARED, poolAccepts: ${poolAcceptsProp.value.dumpSrc()} })`;
const startPos = arkts.createSourcePosition(componentAnno.startPosition.getIndex() - 1, componentAnno.startPosition.getLine());
const annoRange = arkts.createSourceRange(startPos, componentAnno.endPosition);
const suggestionKind = arkts.createDiagnosticKind(message, arkts.Es2pandaPluginDiagnosticType.ES2PANDA_PLUGIN_SUGGESTION);
const suggestionInfo = arkts.createSuggestionInfo(suggestionKind, fixCode, `Add 'reusePool' property`, annoRange);
arkts.logDiagnosticWithSuggestion(diagnosticInfo, suggestionInfo);
} else {
arkts.logDiagnostic(diagnosticKind, componentAnno.startPosition);
}
}
function checkPoolAcceptsNotEmpty(
componentAnno: arkts.AnnotationUsage,
hasReusePool: boolean,
hasPoolAccepts: boolean
): void {
if (!hasPoolAccepts || !hasReusePool) {
return;
}
const poolAcceptsProp = getPropertyInAnnotation(componentAnno, GlobalReusePoolNames.POOL_ACCEPTS);
if (
!poolAcceptsProp ||
!arkts.isClassProperty(poolAcceptsProp) ||
!poolAcceptsProp.value ||
!arkts.isArrayExpression(poolAcceptsProp.value)
) {
return;
}
const poolAcceptsValue = poolAcceptsProp.value;
if (poolAcceptsValue.elements.length !== 0) {
return;
}
const reusePoolProp = getPropertyInAnnotation(componentAnno, GlobalReusePoolNames.REUSE_POOL);
if (
!reusePoolProp ||
!reusePoolProp.value ||
!arkts.isMemberExpression(reusePoolProp.value) ||
!arkts.isIdentifier(reusePoolProp.value.property) ||
reusePoolProp.value.property.name === GlobalReusePoolNames.REUSE_POOL_OWNERSHIP_OFF
) {
return;
}
const message = `'poolAccepts' cannot be an empty array. Provide at least one '@Reusable' or '@ReusableV2' component.`;
const diagnosticKind = arkts.createDiagnosticKind(message, arkts.Es2pandaPluginDiagnosticType.ES2PANDA_PLUGIN_ERROR);
arkts.logDiagnostic(diagnosticKind, componentAnno.startPosition);
}
export function getAnnotationInfoForStruct(
anno: arkts.AnnotationUsage,
shouldIgnoreDecl: boolean
): StructAnnoationInfo {
const isComponent = isCustomComponentAnnotation(anno, StructDecoratorNames.COMPONENT, shouldIgnoreDecl);
const isComponentV2 = isCustomComponentAnnotation(anno, StructDecoratorNames.COMPONENT_V2, shouldIgnoreDecl);
const isEntry = isCustomComponentAnnotation(anno, StructDecoratorNames.ENTRY, shouldIgnoreDecl);
const isReusable = isCustomComponentAnnotation(anno, StructDecoratorNames.RESUABLE, shouldIgnoreDecl);
const isReusableV2 = isCustomComponentAnnotation(anno, StructDecoratorNames.RESUABLE_V2, shouldIgnoreDecl);
const isCustomLayout = isCustomComponentAnnotation(anno, StructDecoratorNames.CUSTOM_LAYOUT, shouldIgnoreDecl);
const isCustomDialog = isCustomComponentAnnotation(anno, StructDecoratorNames.CUSTOMDIALOG, shouldIgnoreDecl);
return { isComponent, isComponentV2, isEntry, isReusable, isReusableV2, isCustomLayout, isCustomDialog };
}
export function isComponentStruct(node: arkts.ETSStructDeclaration, scopeInfo: CustomComponentInfo): boolean {
return scopeInfo.name === node.definition!.ident?.name;
}
* Determine whether it is a custom component.
*
* @param node class declaration node
*/
export function isCustomComponentClass(node: arkts.ClassDeclaration, scopeInfo: CustomComponentInfo): boolean {
const classDef = node.definition;
if (!classDef?.ident?.name) {
return false;
}
const name: string = classDef.ident.name;
return name === scopeInfo.name;
}
export function isCustomComponentInnerClass(node: arkts.ClassDeclaration): boolean {
const classDef = node.definition;
const checkPrefix = !!classDef?.ident?.name.startsWith(CustomComponentNames.COMPONENT_INTERFACE_PREFIX);
const checkComponent = !!classDef?.annotations.some(
(anno) =>
isCustomComponentAnnotation(anno, StructDecoratorNames.COMPONENT) ||
isCustomComponentAnnotation(anno, StructDecoratorNames.COMPONENT_V2) ||
isCustomComponentAnnotation(anno, StructDecoratorNames.CUSTOMDIALOG)
);
return checkPrefix && checkComponent;
}
export function getCustomComponentOptionsName(className: string): string {
return `${CustomComponentNames.COMPONENT_INTERFACE_PREFIX}${className}`;
}
export function getStructNameFromOptionsName(optionsName: string): string {
return optionsName.slice(CustomComponentNames.COMPONENT_INTERFACE_PREFIX.length);
}
* Determine whether it is method with specified name.
*
* @param method method definition node
* @param name specified method name
*/
export function isKnownMethodDefinition(method: arkts.MethodDefinition, name: string): boolean {
if (!method || !arkts.isMethodDefinition(method)) {
return false;
}
const isNameMatched: boolean = method.id?.name === name;
return isNameMatched;
}
export function isSpecificNewClass(node: arkts.ETSNewClassInstanceExpression, className: string): boolean {
if (
node.typeRef &&
arkts.isETSTypeReference(node.typeRef) &&
node.typeRef.part &&
arkts.isETSTypeReferencePart(node.typeRef.part) &&
node.typeRef.part.name &&
arkts.isIdentifier(node.typeRef.part.name) &&
node.typeRef.part.name.name === className
) {
return true;
}
return false;
}
export function isCustomDialogControllerOptions(
newNode: arkts.TSInterfaceDeclaration,
externalSourceName: string | undefined
): boolean {
return (
externalSourceName === CUSTOM_DIALOG_CONTROLLER_SOURCE_NAME &&
!!newNode.id &&
newNode.id.name === CustomDialogNames.CUSTOM_DIALOG_CONTROLLER_OPTIONS
);
}
export function getComponentExtendsName(annotations: CustomComponentAnontations, componentType: ComponentType): string {
if (!!annotations.reusable) {
componentType.hasReusable ||= true;
} else if (!!annotations.reusableV2) {
componentType.hasReusableV2 ||= true;
}
if (!!annotations.entry) {
componentType.hasEntry ||= true;
}
if (!!annotations.customLayout) {
componentType.hasCustomLayout ||= true;
}
if (!!annotations.customDialog) {
componentType.hasCustomDialog ||= true;
return CustomComponentNames.BASE_CUSTOM_DIALOG_NAME;
}
if (!!annotations.componentV2) {
componentType.hasComponentV2 ||= true;
return CustomComponentNames.COMPONENT_V2_CLASS_NAME;
}
componentType.hasComponent ||= true;
return CustomComponentNames.COMPONENT_CLASS_NAME;
}
export function computedField(name: string): string {
return `__computed_${name}`;
}
export function monitorField(name: string): string {
return `__monitor_${name}`;
}
export function syncMonitorField(name: string): string {
return `__SyncMonitor_${name}`;
}
export function optionsHasField(name: string): string {
return `__options_has_${name}`;
}
* @deprecated
*/
export function getClassPropertyType(property: arkts.ClassProperty): arkts.TypeNode | undefined {
const type = property.typeAnnotation;
if (!!type) {
return type.clone();
}
return inferTypeFromValue(property.value);
}
* @deprecated
*/
export function inferTypeFromValue(value: arkts.AstNode | undefined): arkts.TypeNode | undefined {
let inferredType: arkts.AstNode | undefined = value ? arkts.createTypeNodeFromTsType(value) : undefined;
if (inferredType && arkts.isTypeNode(inferredType)) {
return inferredType;
}
return undefined;
}
export function expectNameInTypeReference(node: arkts.TypeNode | undefined): arkts.Identifier | undefined {
if (!node || !arkts.isETSTypeReference(node)) {
return undefined;
}
const part = node.part;
if (!part || !arkts.isETSTypeReferencePart(part)) {
return undefined;
}
const nameNode = part.name;
if (!nameNode || !arkts.isIdentifier(nameNode)) {
return undefined;
}
return nameNode;
}
* Get the value of the corresponding property from the annotation.
* @param anno annotation.
* @param decoratorName annotation name.
* @param key property key name.
*/
export function getValueInObjectAnnotation(
anno: arkts.AnnotationUsage | undefined,
decoratorName: string,
key: string,
allowDefault?: boolean
): arkts.Expression | undefined {
if (!anno) {
return undefined;
}
const isSuitableAnnotation: boolean =
!!anno.expr && arkts.isIdentifier(anno.expr) && anno.expr.name === decoratorName;
if (!isSuitableAnnotation) {
return undefined;
}
const keyItem: arkts.AstNode | undefined = (allowDefault
? arkts.getAnnotationDeclarationProperties(anno)
: anno.properties
).find(
(annoProp: arkts.AstNode) =>
arkts.isClassProperty(annoProp) &&
annoProp.key &&
arkts.isIdentifier(annoProp.key) &&
annoProp.key.name === key
);
if (keyItem && arkts.isClassProperty(keyItem)) {
return keyItem.value;
}
return undefined;
}
export function findLocalSourceNameToImport(
symbolName: string,
declModuleName: string | undefined,
declRelativePath: string | undefined,
isDeclForDynamicStaticInterop?: boolean
): string | undefined {
const localSourceName: string | undefined = ImportCollector.getInstance().getLocalSource(symbolName);
if (!!localSourceName) {
return localSourceName;
}
if (isDeclForDynamicStaticInterop) {
const declOriPath = getDeclOriPath(declRelativePath);
return !!declOriPath ? removeRelativePathSuffix(declOriPath) : undefined;
}
if (!!declModuleName && matchPrefix(ARKUI_IMPORT_PREFIX_NAMES, declModuleName)) {
return declModuleName;
}
if (!!declRelativePath) {
return removeRelativePathSuffix(declRelativePath);
}
return undefined;
}