* 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 { CustomComponentInfo, isKnownMethodDefinition } from '../utils';
import { matchPrefix } from '../../common/arkts-utils';
import {
ARKUI_IMPORT_PREFIX_NAMES,
Dollars,
ModuleType,
DefaultConfiguration,
LogType,
RESOURCE_TYPE,
DecoratorNames,
CustomComponentNames,
CustomDialogNames,
ETSGLOBAL,
BuilderLambdaNames,
ReusableOptions,
} from '../../common/predefines';
import { DeclarationCollector } from '../../common/declaration-collector';
import { ProjectConfig, ResourceList } from '../../common/plugin-context';
import { LogCollector } from '../../common/log-collector';
import { StructAnnotationInfo } from '../../collectors/ui-collectors/records';
import { MetaDataCollector } from '../../common/metadata-collector';
import {
CustomComponentInfo as CustomComponentRecordInfo,
NormalClassInfo,
} from '../../collectors/ui-collectors/records';
import { hasDecorator } from '../property-translators/utils';
export enum StructType {
STRUCT,
CUSTOM_COMPONENT_DECL,
}
export type StructAnnotationPropertyFields = {
[BuilderLambdaNames.USE_SHARED_STORAGE_PARAM_NAME]?: arkts.Expression;
[ReusableOptions.MEMORY_OPT_STRATEGY]?: arkts.Expression;
}
export type FromStructInfo = {
isFromCustomDialog?: boolean;
isFromComponent?: boolean;
isFromComponentV2?: boolean;
isFromReusable?: boolean;
}
export type ScopeInfoCollection = {
customComponents: CustomComponentScopeInfo[];
};
export type CustomComponentScopeInfo = CustomComponentInfo & {
hasInitializeStruct?: boolean;
hasUpdateStruct?: boolean;
hasReusableRebind?: boolean;
startPosition?: arkts.SourcePosition;
endPosition?: arkts.SourcePosition;
};
export interface ResourceParameter {
id: number;
type: number;
params: arkts.Expression[];
}
export interface ObservedAnnoInfo {
className: string;
isObserved: boolean;
isObservedV2: boolean;
classHasTrace: boolean;
classHasTrack: boolean;
}
export type ClassScopeInfo = ObservedAnnoInfo & {
getters: arkts.MethodDefinition[];
};
export function getCustomComponentNameFromInfo(info: CustomComponentScopeInfo): string {
if (!!info.annotations.componentV2) {
return CustomComponentNames.COMPONENT_V2_CLASS_NAME;
} else if (!!info.annotations.customDialog) {
return CustomComponentNames.BASE_CUSTOM_DIALOG_NAME;
}
return CustomComponentNames.COMPONENT_CLASS_NAME;
}
export function getResourceParams(id: number, type: number, params: arkts.Expression[]): ResourceParameter {
return { id, type, params };
}
* Determine whether it is resource node begin with `$r` or `$rawfile`.
*
* @param node call expression node
*/
export function isResourceNode(node: arkts.CallExpression, ignoreDecl: boolean = false): boolean {
if (
!(
arkts.isIdentifier(node.callee) &&
(node.callee.name === Dollars.DOLLAR_RESOURCE || node.callee.name === Dollars.DOLLAR_RAWFILE)
)
) {
return false;
}
if (!ignoreDecl) {
const decl = arkts.getPeerIdentifierDecl(node.callee.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;
}
* Verify whether the rawfile resource exists in the current module.
*
* @param resourceNode resource node.
* @param rawfileStr rawfile string.
* @param fromOtherModule flag about whether it is a resource for other modules.
* @param rawfileSet a set that records all the rawfile resources.
*/
export function checkRawfileResource(
resourceNode: arkts.CallExpression,
rawfileStr: arkts.StringLiteral,
fromOtherModule: boolean,
rawfileSet: Set<string>
): void {
if (!fromOtherModule && !rawfileSet.has(rawfileStr.str)) {
LogCollector.getInstance().collectLogInfo({
level: LogType.ERROR,
node: resourceNode,
message: `No such '${rawfileStr.str}' resource in current module.`,
code: '10904333',
});
}
}
* Check the format and the existance of resource string literal.
*
* @param resourceData array of resource string literals.
* @param resourcesList resources including app, sys and hsp.
* @param literalArg string literal argument node.
* @param fromOtherModule flag about whether it is a resource for other modules.
* @param projectConfig configuration information of the project.
*/
export function preCheckResourceData(
resourceNode: arkts.CallExpression,
resourceData: string[],
resourcesList: ResourceList,
fromOtherModule: boolean,
projectConfig: ProjectConfig
): boolean {
let code: string | undefined;
let message: string | undefined;
if (resourceData.length !== 3) {
message = 'The input parameter is not supported.';
code = '10905332';
}
if (!RESOURCE_TYPE[resourceData[1]]) {
message = `The resource type ${resourceData[1]} is not supported.`;
code = '10906334';
}
if (!!code && !!message) {
LogCollector.getInstance().collectLogInfo({
level: LogType.ERROR,
node: resourceNode,
message: message,
code: code,
});
return false;
}
return preCheckResourceDataExistance(resourceNode, resourceData, resourcesList, fromOtherModule, projectConfig);
}
* Check the existance of resource string literal when the format of the string literal is correct.
*
* @param resourceData array of resource string literals.
* @param resourcesList resources including app, sys and hsp.
* @param literalArg string literal argument node.
* @param fromOtherModule flag about whether it is a resource for other modules.
* @param projectConfig configuration information of the project.
*/
export function preCheckResourceDataExistance(
resourceNode: arkts.CallExpression,
resourceData: string[],
resourcesList: ResourceList,
fromOtherModule: boolean,
projectConfig: ProjectConfig
): boolean {
if (fromOtherModule) {
if (/^\[.*\]$/.test(resourceData[0]) && projectConfig.hspResourcesMap) {
const resourceDataFirst: string = resourceData[0].replace(/^\[/, '').replace(/\]$/, '').trim();
return resourceCheck(resourceNode, resourceData, resourcesList, true, resourceDataFirst, false);
} else {
return resourceCheck(resourceNode, resourceData, resourcesList, false, resourceData[0], true);
}
} else {
return resourceCheck(resourceNode, resourceData, resourcesList, false, resourceData[0], false);
}
}
* Verify whether the app resource exists in the current module.
*
* @param resourceNode resource node.
* @param resourceData array of resource string literals.
* @param resourcesList resources including app, sys and hsp.
* @param isHarHspResourceModule flag about whether it is from hsp or har module.
* @param resourceDataFirst the first element of resource string literals.
* @param noHspResourcesMap the non-existence of hspResourcesMap.
*/
function resourceCheck(
resourceNode: arkts.CallExpression,
resourceData: string[],
resourcesList: ResourceList,
isHarHspResourceModule: boolean,
resourceDataFirst: string,
noHspResourcesMap: boolean
): boolean {
let checkResult: boolean = true;
const logType: LogType = isHarHspResourceModule ? LogType.WARN : LogType.ERROR;
let code: string | undefined;
let message: string | undefined;
if (!noHspResourcesMap && !resourcesList[resourceDataFirst]) {
code = '10903331';
message = `Unknown resource source '${resourceDataFirst}'.`;
checkResult = isHarHspResourceModule ? checkResult : false;
} else if (!noHspResourcesMap && !resourcesList[resourceDataFirst].get(resourceData[1])) {
code = '10903330';
message = `Unknown resource type '${resourceData[1]}'.`;
checkResult = isHarHspResourceModule ? checkResult : false;
} else if (!noHspResourcesMap && !resourcesList[resourceDataFirst].get(resourceData[1])![resourceData[2]]) {
code = '10903329';
message = `Unknown resource name '${resourceData[2]}'.`;
checkResult = isHarHspResourceModule ? checkResult : false;
}
if (!!code && !!message) {
LogCollector.getInstance().collectLogInfo({
level: logType,
node: resourceNode,
message: message,
code: code,
});
}
return checkResult;
}
* generate bundleName for $r and $rawfile.
*
* @param projectConfig project config.
* @param isDynamicBundleOrModule a flag for determining whether to use dynamic module name and bundle name.
*/
export function generateResourceBundleName(projectConfig: ProjectConfig, isDynamicBundleOrModule: boolean): string {
if (projectConfig.resetBundleName || projectConfig.allowEmptyBundleName) {
return '';
}
if (isDynamicBundleOrModule) {
return DefaultConfiguration.DYNAMIC_BUNDLE_NAME;
}
return projectConfig.moduleType === ModuleType.HAR
? DefaultConfiguration.HAR_DEFAULT_BUNDLE_NAME
: projectConfig.bundleName
? projectConfig.bundleName
: '';
}
* generate moduleName for $r and $rawfile.
*
* @param projectConfig project config.
* @param isDynamicBundleOrModule a flag for determining whether to use dynamic module name and bundle name.
*/
export function generateResourceModuleName(
projectConfig: ProjectConfig,
isDynamicBundleOrModule: boolean = false,
resourceModuleName: string,
fromOtherModule: boolean
): string {
if (fromOtherModule && resourceModuleName) {
return resourceModuleName.replace(/^\[|\]$/g, '');
}
if (isDynamicBundleOrModule) {
return DefaultConfiguration.DYNAMIC_MODULE_NAME;
}
return projectConfig.moduleType === ModuleType.HAR
? DefaultConfiguration.HAR_DEFAULT_MODULE_NAME
: projectConfig.moduleName
? projectConfig.moduleName
: '';
}
* Determine whether to use dynamic module name and bundle name.
*
* @param projectConfig project config.
*/
export function isDynamicName(projectConfig: ProjectConfig): boolean {
const isByteCodeHar: boolean = projectConfig.moduleType === ModuleType.HAR && projectConfig.byteCodeHar;
const uiTransformOptimization: boolean = !!projectConfig.uiTransformOptimization;
return uiTransformOptimization ? uiTransformOptimization : isByteCodeHar;
}
export function getCustomDialogController(node: arkts.ClassProperty): string {
const key = node.key;
let controllerName: string = '';
if (!(key && arkts.isIdentifier(key) && node.typeAnnotation)) {
return '';
}
const typeAnno = node.typeAnnotation;
if (arkts.isETSUnionType(typeAnno)) {
for (const type of typeAnno.types) {
if (arkts.isETSTypeReference(type)) {
controllerName = hasController(type, key.name);
}
}
} else if (arkts.isETSTypeReference(typeAnno)) {
controllerName = hasController(typeAnno, key.name);
}
return controllerName;
}
export function hasController(node: arkts.ETSTypeReference, keyName: string): string {
const ident = node.part?.name;
if (ident && arkts.isIdentifier(ident) && ident.name === CustomDialogNames.CUSTOM_DIALOG_CONTROLLER) {
return keyName;
}
return '';
}
export function isInvalidDialogControllerOptions(args: readonly arkts.Expression[]): boolean {
const firstOptionsParameter: arkts.AstNode = args[0];
return (
args.length <= 0 ||
!(isObjectAsExpression(firstOptionsParameter) || arkts.isObjectExpression(firstOptionsParameter))
);
}
function isObjectAsExpression(param: arkts.AstNode): boolean {
return arkts.isTSAsExpression(param) && !!param.expr && arkts.isObjectExpression(param.expr);
}
export function isComputedMethod(node: arkts.AstNode): boolean {
return arkts.isMethodDefinition(node) && hasDecorator(node, DecoratorNames.COMPUTED);
}
export function findBuilderIndexInControllerOptions(properties: readonly arkts.Expression[]): number {
return properties.findIndex((item: arkts.Expression) => {
return (
arkts.isProperty(item) &&
!!item.key &&
arkts.isIdentifier(item.key) &&
item.key.name === CustomDialogNames.OPTIONS_BUILDER
);
});
}
export function getNoTransformationMembersInClass(
definition: arkts.ClassDefinition,
ObservedAnno: ObservedAnnoInfo
): arkts.AstNode[] {
return definition.body.filter(
(member) =>
!arkts.isClassProperty(member) &&
!(
arkts.isMethodDefinition(member) &&
arkts.hasModifierFlag(member, arkts.Es2pandaModifierFlags.MODIFIER_FLAGS_GETTER)
) &&
!(
ObservedAnno.isObservedV2 &&
arkts.isMethodDefinition(member) &&
(hasDecorator(member, DecoratorNames.COMPUTED) ||
isKnownMethodDefinition(member, CustomComponentNames.COMPONENT_CONSTRUCTOR_ORI))
)
);
}
* process from-struct info to the following fields:
* - isFromCustomDialog: struct is from `@CustomDialog`
* - isFromComponent: struct is from `@Component` or `@Reusable`
* - isFromComponentV2: struct is from `@ComponentV2` or `@ReusableV2`
* - isFromReusable: struct is from `@Reusable` or `@ReusableV2`
*/
export function processFromStructInfo(annotationInfo: StructAnnotationInfo | undefined): FromStructInfo {
return {
isFromCustomDialog: !!annotationInfo?.hasCustomDialog,
isFromComponent: !!annotationInfo?.hasComponent || !!annotationInfo?.hasReusable,
isFromComponentV2: !!annotationInfo?.hasComponentV2 || !!annotationInfo?.hasReusableV2,
isFromReusable: !!annotationInfo?.hasReusable || !!annotationInfo?.hasReusableV2
}
}
export function checkIsDeclFromStructInfo(info: CustomComponentRecordInfo | undefined): boolean {
return !!MetaDataCollector.getInstance().isDeclaration || !!info?.isDecl;
}
export function checkIsDeclFromNormalClassInfo(info: NormalClassInfo | undefined): boolean {
return !!MetaDataCollector.getInstance().isDeclaration || !!info?.isDecl;
}