1/* 2 * Copyright (c) 2025 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16import * as arkts from '@koalaui/libarkts'; 17import { getIdentifierName } from '../utils'; 18import { UISyntaxRule, UISyntaxRuleContext } from './ui-syntax-rule'; 19 20function checkInvalidChildInText(node: arkts.AstNode, context: UISyntaxRuleContext, textChild: string[]): void { 21 // Check if the current node is an identifier, and name is 'Text' 22 if (!arkts.isIdentifier(node)) { 23 return; 24 } 25 if (getIdentifierName(node) !== 'Text') { 26 return; 27 } 28 if (!node.parent) { 29 return; 30 } 31 const parentNode = node.parent; 32 if (!arkts.isCallExpression(parentNode)) { 33 return; 34 } 35 // If the BlockStatement contains a child component that should not exist under the text, an error will be reported 36 parentNode.getChildren().forEach(member => { 37 if (!arkts.isBlockStatement(member)) { 38 return; 39 } 40 member.getChildren().forEach(sibling => { 41 if (!arkts.isExpressionStatement(sibling) || !arkts.isCallExpression(sibling.expression)) { 42 return; 43 } 44 const childComponentName = sibling.expression.expression.dumpSrc(); 45 if (!textChild.includes(childComponentName)) { 46 context.report({ 47 node: node, 48 message: rule.messages.invalidChildInText 49 }); 50 return; 51 } 52 }); 53 }); 54} 55function checkOneChildInButton(node: arkts.AstNode, context: UISyntaxRuleContext): void { 56 // Check if the current node is an identifier, and name is 'Button' 57 if (!arkts.isIdentifier(node)) { 58 return; 59 } 60 if (getIdentifierName(node) !== 'Button') { 61 return; 62 } 63 if (!node.parent) { 64 return; 65 } 66 const parentNode = node.parent; 67 if (!arkts.isCallExpression(parentNode)) { 68 return; 69 } 70 // If there is more than one subcomponent in the BlockStatement, an error is reported 71 parentNode.getChildren().forEach(member => { 72 if (!arkts.isBlockStatement(member)) { 73 return; 74 } 75 if (member.statements.length > 1) { 76 context.report({ 77 node: node, 78 message: rule.messages.oneChildInButton 79 }); 80 } 81 }); 82} 83 84function checkListItem(node: arkts.AstNode, context: UISyntaxRuleContext): void { 85 // Check if the current node is an identifier, and name is 'ListItem' 86 if (!arkts.isIdentifier(node)) { 87 return; 88 } 89 if (getIdentifierName(node) !== 'ListItem') { 90 return; 91 } 92 if (!node.parent || !node.parent.parent) { 93 return; 94 } 95 let curNode: arkts.AstNode = node.parent.parent; 96 do { 97 while (!arkts.isCallExpression(curNode)) { 98 if (!curNode.parent) { 99 return; 100 } 101 curNode = curNode.parent; 102 } 103 const parentName: string = curNode.expression.dumpSrc(); 104 if (parentName === 'List') { // If the parent component's name is 'List', exit directly 105 break; 106 } else if (parentName !== 'ForEach') { // If the parent component's name is not 'List' or 'ForEach', throw an error 107 context.report({ 108 node: node, 109 message: rule.messages.listItemCannotInOther, 110 data: { parentName: parentName } 111 }); 112 context.report({ 113 node: node, 114 message: rule.messages.listItemMustInList 115 }); 116 break; 117 } 118 // In the remaining case, the parent component is 'ForEach', continue traversing upwards for further checks 119 if (!curNode.parent) { 120 return; 121 } 122 curNode = curNode.parent; 123 } while (true); 124} 125 126function checkSpan(node: arkts.AstNode, context: UISyntaxRuleContext): void { 127 // Check if the current node is an identifier, and name is 'Span' 128 if (!arkts.isIdentifier(node)) { 129 return; 130 } 131 if (getIdentifierName(node) !== 'Span') { 132 return; 133 } 134 let parentNode = node.parent; 135 if (!arkts.isCallExpression(parentNode)) { 136 return; 137 } 138 // If there are subcomponents in the BlockStatement, an error is reported 139 parentNode.getChildren().forEach(sibling => { 140 if (!arkts.isBlockStatement(sibling)) { 141 return; 142 } 143 if (sibling.statements.length > 0) { 144 context.report({ 145 node: node, 146 message: rule.messages.noChildInSpan 147 }); 148 } 149 }); 150 if (!node.parent || !node.parent.parent) { 151 return; 152 } 153 parentNode = parentNode.parent; 154 while (!arkts.isCallExpression(parentNode)) { 155 parentNode = parentNode.parent; 156 } 157 const parentName: string = parentNode.expression.dumpSrc(); 158 // If the name of the parent component is not 'Text', an error is reported 159 if (parentName !== 'Text' && parentName !== 'RichEditor' && parentName !== 'ContainerSpan') { 160 context.report({ 161 node: node, 162 message: rule.messages.spanMustInText 163 }); 164 } 165} 166// The 'Text' component can have only the Span, ImageSpan, ContainerSpan and SymbolSpan child component. 167 168const rule: UISyntaxRule = { 169 name: 'nested-relationship', 170 messages: { 171 invalidChildInText: `The 'Text' component can have only the Span, ImageSpan, ContainerSpan and SymbolSpan child component.`, 172 oneChildInButton: `The 'Button' component can have only one child component.`, 173 listItemMustInList: `The 'ListItem' component can only be nested in the List and ListItemGroup parent component.`, 174 listItemCannotInOther: `The 'ListItem' component cannot be a child component of the '{{parentName}}' component.`, 175 noChildInSpan: `No child component is allowed in the 'Span' component. `, 176 spanMustInText: `The 'Span' component can only be nested in the Text, RichEditor and ContainerSpan parent component. `, 177 }, 178 setup(context) { 179 const textChild: string[] = ['Span', 'ImageSpan', 'ContainerSpan', 'SymbolSpan']; 180 return { 181 parsed: (node): void => { 182 checkInvalidChildInText(node, context, textChild); 183 checkOneChildInButton(node, context); 184 checkListItem(node, context); 185 checkSpan(node, context); 186 }, 187 }; 188 }, 189}; 190 191export default rule;