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 { getAnnotationUsage, MultiMap, PresetDecorators } from '../utils'; 18import { UISyntaxRule, UISyntaxRuleContext } from './ui-syntax-rule'; 19// Traverse the member variables of the struct, recording the members of the @Consumer modifications 20function processStructMembers( 21 node: arkts.StructDeclaration, 22 structName: string, 23 componentv2WithConsumer: MultiMap<string, string> 24): void { 25 node.definition.body.forEach((member) => { 26 // When a member variable is @consumer modified, it is stored to mark fields that cannot be initialized 27 if (arkts.isClassProperty(member)) { 28 const memberName = member?.key?.dumpSrc(); 29 structName && memberName ? componentv2WithConsumer.add(structName, memberName) : null; 30 } 31 }); 32} 33function rememberStructName(node: arkts.AstNode, componentv2WithConsumer: MultiMap<string, string>): void { 34 // First it has to be of the struct type 35 if (arkts.isStructDeclaration(node)) { 36 node?.definition?.annotations.forEach((anno) => { 37 // Second, it must be decorated with a @component v2 decorator 38 if (anno.expr?.dumpSrc() === PresetDecorators.COMPONENT_V2) { 39 const structName = node.definition.ident?.name ?? ''; 40 processStructMembers(node, structName, componentv2WithConsumer); 41 } 42 }); 43 } 44} 45function findDecorator(member: arkts.ClassProperty, decorator: string): arkts.AnnotationUsage | undefined { 46 return member.annotations?.find(annotation => 47 annotation.expr && 48 annotation.expr.dumpSrc() === decorator 49 ); 50} 51// Verify that the @Consumer decorator is used on the method 52function validateConsumerOnMethod(member: arkts.MethodDefinition, context: UISyntaxRuleContext): void { 53 const annotationNode = member.scriptFunction.annotations?.find(annotation => 54 annotation.expr && annotation.expr.dumpSrc() === PresetDecorators.CONSUMER 55 ); 56 if (annotationNode) { 57 context.report({ 58 node: annotationNode, 59 message: rule.messages.consumerOnlyOnMember, 60 fix: (annotationNode) => { 61 const startPosition = arkts.getStartPosition(annotationNode); 62 const endPosition = arkts.getEndPosition(annotationNode); 63 return { 64 range: [startPosition, endPosition], 65 code: '', 66 }; 67 } 68 }); 69 } 70} 71// @Consumer Bugs that conflict with other decorators 72function reportMultipleBuiltInDecorators( 73 hasConsumeDecorator: arkts.AnnotationUsage, 74 otherDecorators: arkts.AnnotationUsage, 75 context: UISyntaxRuleContext, 76): void { 77 context.report({ 78 node: hasConsumeDecorator, 79 message: rule.messages.multipleBuiltInDecorators, 80 fix: (hasConsumeDecorator) => { 81 const startPosition = arkts.getStartPosition(otherDecorators); 82 const endPosition = arkts.getEndPosition(otherDecorators); 83 return { 84 range: [startPosition, endPosition], 85 code: '', 86 }; 87 } 88 }); 89} 90// Report a bug where @Provider is missing @ComponentV2 91function reportProviderRequiresComponentV2( 92 hasProviderDecorator: arkts.AnnotationUsage, 93 hasComponent: arkts.AnnotationUsage | undefined, 94 node: arkts.AstNode, 95 context: UISyntaxRuleContext, 96): void { 97 if (hasComponent) { 98 context.report({ 99 node: hasProviderDecorator, 100 message: rule.messages.providerRequiresComponentV2, 101 fix: (hasProviderDecorator) => { 102 const startPosition = arkts.getStartPosition(hasComponent); 103 const endPosition = arkts.getEndPosition(hasComponent); 104 return { 105 range: [startPosition, endPosition], 106 code: `@${PresetDecorators.COMPONENT_V2}`, 107 }; 108 } 109 }); 110 } else { 111 context.report({ 112 node: hasProviderDecorator, 113 message: rule.messages.providerRequiresComponentV2, 114 fix: (hasProviderDecorator) => { 115 const startPosition = arkts.getStartPosition(node); 116 const endPosition = startPosition; 117 return { 118 range: [startPosition, endPosition], 119 code: `@${PresetDecorators.COMPONENT_V2}\n`, 120 }; 121 } 122 }); 123 } 124} 125// Verify decorator conflicts on member variables 126function validateMemberDecorators(member: arkts.ClassProperty, 127 hasComponentV2: arkts.AnnotationUsage | undefined, 128 hasComponent: arkts.AnnotationUsage | undefined, 129 node: arkts.AstNode, 130 context: UISyntaxRuleContext 131): void { 132 const hasConsumeDecorator = findDecorator(member, PresetDecorators.CONSUMER); 133 const hasProviderDecorator = findDecorator(member, PresetDecorators.PROVIDER); 134 const otherDecorators = member.annotations?.find(annotation => 135 annotation.expr && 136 annotation.expr.dumpSrc() !== PresetDecorators.CONSUMER 137 ); 138 if (hasConsumeDecorator && otherDecorators) { 139 reportMultipleBuiltInDecorators(hasConsumeDecorator, otherDecorators, context); 140 } 141 if (hasProviderDecorator && !hasComponentV2) { 142 reportProviderRequiresComponentV2(hasProviderDecorator, hasComponent, node, context); 143 } 144} 145// Verify that @Provider is being used in the class 146function validateProviderInClass(member: arkts.ClassProperty, context: UISyntaxRuleContext): void { 147 const hasProviderDecorator = findDecorator(member, PresetDecorators.PROVIDER); 148 if (hasProviderDecorator) { 149 context.report({ 150 node: hasProviderDecorator, 151 message: rule.messages.providerOnlyInStruct, 152 fix: (hasProviderDecorator) => { 153 const startPosition = arkts.getStartPosition(hasProviderDecorator); 154 const endPosition = arkts.getEndPosition(hasProviderDecorator); 155 return { 156 range: [startPosition, endPosition], 157 code: '', 158 }; 159 } 160 }); 161 } 162} 163// Verify that the current identifier is an illegally initialized @Consumer member variable 164function reportValidateConsumer( 165 currentNode: arkts.Identifier, 166 callExpName: string, 167 componentv2WithConsumer: MultiMap<string, string>, 168 context: UISyntaxRuleContext 169): void { 170 if (componentv2WithConsumer.get(callExpName).includes(currentNode.dumpSrc())) { 171 context.report({ 172 node: currentNode.parent, 173 message: rule.messages.forbiddenInitialization, 174 data: { 175 value: currentNode.dumpSrc(), 176 structName: callExpName 177 }, 178 fix: () => { 179 const startPosition = arkts.getStartPosition(currentNode.parent); 180 const endPosition = arkts.getEndPosition(currentNode.parent); 181 return { 182 range: [startPosition, endPosition], 183 code: '', 184 }; 185 } 186 }); 187 } 188} 189// Verify that the @Consumer-decorated property is initialized 190function validateConsumerInitialization(node: arkts.CallExpression, componentv2WithConsumer: MultiMap<string, string>, 191 context: UISyntaxRuleContext): void { 192 const callExpName: string = node.expression.dumpSrc(); 193 if (componentv2WithConsumer.has(callExpName)) { 194 const queue: Array<arkts.AstNode> = [node]; 195 while (queue.length > 0) { 196 const currentNode: arkts.AstNode = queue.shift() as arkts.AstNode; 197 if (arkts.isIdentifier(currentNode)) { 198 reportValidateConsumer(currentNode, callExpName, componentv2WithConsumer, context); 199 } 200 const children = currentNode.getChildren(); 201 for (const child of children) { 202 queue.push(child); 203 } 204 } 205 } 206} 207function collectStructsWithConsumer(node: arkts.AstNode, componentv2WithConsumer: MultiMap<string, string>): void { 208 // Used to document all V2 structs that use '@Consumer' 209 if (arkts.nodeType(node) === arkts.Es2pandaAstNodeType.AST_NODE_TYPE_ETS_MODULE) { 210 // Breadth traversal is done through while and queues 211 const queue: Array<arkts.AstNode> = [node]; 212 while (queue.length > 0) { 213 const currentNode: arkts.AstNode = queue.shift() as arkts.AstNode; 214 // Filter and record the nodes of the tree 215 rememberStructName(currentNode, componentv2WithConsumer); 216 const children = currentNode.getChildren(); 217 for (const child of children) { 218 queue.push(child); 219 } 220 } 221 } 222} 223function validateStructDecoratorsAndMembers(node: arkts.AstNode, context: UISyntaxRuleContext): void { 224 if (arkts.isStructDeclaration(node)) { 225 const hasComponentV2 = getAnnotationUsage(node, PresetDecorators.COMPONENT_V2); 226 const hasComponent = getAnnotationUsage(node, PresetDecorators.COMPONENT_V1); 227 node.definition.body.forEach(member => { 228 if (arkts.isMethodDefinition(member)) { 229 validateConsumerOnMethod(member, context); 230 } 231 if (arkts.isClassProperty(member)) { 232 validateMemberDecorators(member, hasComponentV2, hasComponent, node, context); 233 } 234 }); 235 } 236} 237function validateProviderInClasses(node: arkts.AstNode, context: UISyntaxRuleContext): void { 238 if (arkts.isClassDeclaration(node)) { 239 node.definition?.body.forEach(member => { 240 if (arkts.isClassProperty(member)) { 241 validateProviderInClass(member, context); 242 } 243 }); 244 } 245} 246const rule: UISyntaxRule = { 247 name: 'consumer-provider-decorator-check', 248 messages: { 249 consumerOnlyOnMember: `'@Consumer' can only decorate member property.`, 250 multipleBuiltInDecorators: `The struct member variable can not be decorated by multiple built-in decorators.`, 251 providerRequiresComponentV2: `The '@Provider' decorator can only be used in a 'struct' decorated with '@ComponentV2'.`, 252 providerOnlyInStruct: `The '@Provider' decorator can only be used with 'struct'.`, 253 forbiddenInitialization: `Property '{{value}}' in the custom component '{{structName}}' cannot be initialized here (forbidden to specify).`, 254 }, 255 setup(context) { 256 // Used to record the names of the corresponding structs and member variables that are @consumer modified 257 let componentv2WithConsumer: MultiMap<string, string> = new MultiMap(); 258 return { 259 parsed: (node): void => { 260 collectStructsWithConsumer(node, componentv2WithConsumer); 261 validateStructDecoratorsAndMembers(node, context); 262 validateProviderInClasses(node, context); 263 if (arkts.isCallExpression(node)) { 264 validateConsumerInitialization(node, componentv2WithConsumer, context); 265 } 266 }, 267 }; 268 }, 269}; 270 271export default rule; 272