• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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;