* 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 {
getIdentifierName,
isAtomicComponent,
isBuildInComponent,
isSingleChildComponent,
listToString,
SINGLE_CHILD_COMPONENT
} from '../utils';
import { AbstractUISyntaxRule, UISyntaxRuleLevel } from './ui-syntax-rule';
const renderingControlComponents: Set<string> = new Set<string>([
'ForEach',
'LazyForEach',
'Repeat'
]);
class NestedRelationshipRule extends AbstractUISyntaxRule {
public setup(): Record<string, string> {
return {
noChildComponent: `The component '{{componentName}}' can't have any child.`,
singleChildComponent: `The '{{componentName}}' component can have only one child component.`,
delegateChildrenComponentChildren: `The '{{childComponentName}}' component cannot be a child component of the {{componentName}} component.`,
delegateChildrenComponentParent: `The component '{{parentComponentName}}' can only have the child component '{{childComponentList}}'.`,
delegateParentComponent: `The '{{componentName}}' component can only be nested in the '{{parentComponentList}}' parent component.`,
};
}
public parsed(node: arkts.ETSStructDeclaration): void {
this.checkValidParentComponent(node);
this.checkValidChildComponent(node);
this.checkSingleChildComponent(node);
this.checkNoChildComponent(node);
}
private checkValidParentComponent(node: arkts.AstNode): void {
if (!arkts.isIdentifier(node) || !this.context.componentsInfo) {
return;
}
const componentName: string = getIdentifierName(node);
if (
!this.context.componentsInfo.validParentComponent.has(componentName) ||
!node.parent ||
!node.parent.parent
) {
return;
}
let curNode = node.parent.parent;
let foundRenderingComponent: boolean = false;
while (
!arkts.isCallExpression(curNode) ||
!arkts.isIdentifier(curNode.callee) ||
!isBuildInComponent(this.context, curNode.callee.name)
) {
if (!curNode.parent) {
return;
}
if (
arkts.isCallExpression(curNode) &&
arkts.isIdentifier(curNode.callee) &&
renderingControlComponents.has(curNode.callee.name)
) {
foundRenderingComponent = true;
}
curNode = curNode.parent;
}
const parentComponentName = curNode.callee.name;
if (this.context.componentsInfo.validParentComponent.get(componentName)!.includes(parentComponentName)) {
return;
}
const parentComponentListArray: string[] = this.context.componentsInfo.validParentComponent.get(componentName)!;
if (foundRenderingComponent) {
this.reportDelegateParentComponent(node, 'warn', componentName, parentComponentListArray);
} else {
this.reportDelegateParentComponent(node, 'error', componentName, parentComponentListArray);
}
}
private reportDelegateParentComponent(
parentNode: arkts.Identifier,
level: UISyntaxRuleLevel,
componentName: string,
parentComponentList: string[]
): void {
this.report({
node: parentNode,
level: level,
message: this.messages.delegateParentComponent,
data: {
componentName: componentName,
parentComponentList: listToString(parentComponentList),
},
});
}
private checkValidChildComponent(node: arkts.AstNode): void {
if (!arkts.isIdentifier(node) || !this.context.componentsInfo) {
return;
}
const componentName: string = getIdentifierName(node);
if (!this.context.componentsInfo.validChildComponent.has(componentName) || !node.parent ||
!arkts.isCallExpression(node.parent) || !arkts.isIdentifier(node.parent.callee)) {
return;
}
let parentNode = node.parent;
const childComponentListArray = this.context.componentsInfo.validChildComponent.get(componentName);
if (!childComponentListArray) {
return;
}
let reportFlag: boolean = false;
parentNode.getChildren().forEach(member => {
if (!arkts.isBlockStatement(member)) {
return;
}
member.statements.forEach(statement => {
const childComponentNode = this.findChildComponentNode(statement);
if (!childComponentNode) {
return;
}
const childComponentName = getIdentifierName(childComponentNode);
if (
childComponentListArray.includes(childComponentName) ||
!isBuildInComponent(this.context, childComponentName) ||
renderingControlComponents.has(childComponentName)
) {
return;
}
reportFlag = true;
this.reportDelegateChildrenComponentChildren(childComponentNode, childComponentName, componentName);
});
});
if (!reportFlag) {
return;
}
this.report({
node: node,
message: this.messages.delegateChildrenComponentParent,
data: {
parentComponentName: componentName,
childComponentList: listToString(childComponentListArray),
},
});
}
private findChildComponentNode(stmt: arkts.AstNode): arkts.Identifier | undefined {
if (
!arkts.isExpressionStatement(stmt) ||
!stmt.expression ||
!arkts.isCallExpression(stmt.expression) ||
!stmt.expression.callee
) {
return undefined;
}
if (arkts.isIdentifier(stmt.expression.callee)) {
return stmt.expression.callee;
}
if (
arkts.isMemberExpression(stmt.expression.callee) &&
arkts.isCallExpression(stmt.expression.callee.object) &&
arkts.isIdentifier(stmt.expression.callee.object.callee)
) {
return stmt.expression.callee.object.callee;
}
return undefined;
}
private reportDelegateChildrenComponentChildren(
parentNode: arkts.Identifier,
childComponentName: string,
componentName: string
): void {
this.report({
node: parentNode,
message: this.messages.delegateChildrenComponentChildren,
data: {
childComponentName: childComponentName,
componentName: componentName,
},
});
}
private checkSingleChildComponent(node: arkts.AstNode): void {
if (!arkts.isIdentifier(node)) {
return;
}
const componentName: string = getIdentifierName(node);
if (!isSingleChildComponent(this.context, componentName) || !node.parent) {
return;
}
const parentNode = node.parent;
if (!arkts.isCallExpression(parentNode)) {
return;
}
parentNode.getChildren().forEach(member => {
if (!arkts.isBlockStatement(member)) {
return;
}
if (member.statements.length > SINGLE_CHILD_COMPONENT) {
this.report({
node: node,
message: this.messages.singleChildComponent,
data: { componentName: componentName }
});
}
});
}
private checkNoChildComponent(node: arkts.AstNode): void {
if (!arkts.isIdentifier(node)) {
return;
}
const componentName: string = getIdentifierName(node);
if (!isAtomicComponent(this.context, componentName) || !node.parent) {
return;
}
let parentNode = node.parent;
if (!arkts.isCallExpression(parentNode)) {
return;
}
parentNode.getChildren().forEach(member => {
if (!arkts.isBlockStatement(member)) {
return;
}
if (member.statements.length > 0) {
this.report({
node: node,
message: this.messages.noChildComponent,
data: { componentName: componentName }
});
}
});
}
}
export default NestedRelationshipRule;