1/* 2 * Copyright (c) 2021 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 ts from 'typescript'; 17 18import { 19 COMPONENT_NON_DECORATOR, 20 COMPONENT_STATE_DECORATOR, 21 COMPONENT_PROP_DECORATOR, 22 COMPONENT_LINK_DECORATOR, 23 COMPONENT_STORAGE_LINK_DECORATOR, 24 COMPONENT_PROVIDE_DECORATOR, 25 COMPONENT_OBJECT_LINK_DECORATOR, 26 COMPONENT_CREATE_FUNCTION, 27 BASE_COMPONENT_NAME, 28 CUSTOM_COMPONENT_EARLIER_CREATE_CHILD, 29 COMPONENT_CONSTRUCTOR_UPDATE_PARAMS, 30 CUSTOM_COMPONENT_FUNCTION_FIND_CHILD_BY_ID, 31 COMPONENT_CONSTRUCTOR_UNDEFINED, 32 CUSTOM_COMPONENT_NEEDS_UPDATE_FUNCTION, 33 CUSTOM_COMPONENT_MARK_STATIC_FUNCTION 34} from './pre_define'; 35import { 36 propertyCollection, 37 stateCollection, 38 linkCollection, 39 propCollection, 40 regularCollection, 41 storagePropCollection, 42 storageLinkCollection, 43 provideCollection, 44 consumeCollection, 45 objectLinkCollection, 46 isStaticViewCollection 47} from './validate_ui_syntax'; 48import { 49 propAndLinkDecorators, 50 curPropMap, 51 observedPropertyDecorators, 52 createViewCreate, 53 createCustomComponentNewExpression 54} from './process_component_member'; 55import { builderParamObjectCollection } from './process_component_member'; 56import { 57 LogType, 58 LogInfo, 59 componentInfo 60} from './utils'; 61 62const localArray: string[] = [...observedPropertyDecorators, COMPONENT_NON_DECORATOR, 63 COMPONENT_PROP_DECORATOR, COMPONENT_OBJECT_LINK_DECORATOR]; 64 65const decoractorMap: Map<string, Map<string, Set<string>>> = new Map( 66 [[COMPONENT_STATE_DECORATOR, stateCollection], 67 [COMPONENT_LINK_DECORATOR, linkCollection], 68 [COMPONENT_PROP_DECORATOR, propCollection], 69 [COMPONENT_NON_DECORATOR, regularCollection], 70 [COMPONENT_PROVIDE_DECORATOR, provideCollection], 71 [COMPONENT_OBJECT_LINK_DECORATOR, objectLinkCollection]]); 72 73export function processCustomComponent(node: ts.ExpressionStatement, newStatements: ts.Statement[], 74 log: LogInfo[]): void { 75 if (ts.isCallExpression(node.expression)) { 76 let ischangeNode: boolean = false; 77 let customComponentNewExpression: ts.NewExpression = createCustomComponentNewExpression( 78 node.expression); 79 let argumentsArray: ts.PropertyAssignment[]; 80 if (isHasChild(node.expression)) { 81 // @ts-ignore 82 argumentsArray = node.expression.arguments[0].properties.slice(); 83 argumentsArray.forEach((item: ts.PropertyAssignment, index: number) => { 84 if (isToChange(item, node.expression as ts.CallExpression)) { 85 ischangeNode = true; 86 const propertyAssignmentNode: ts.PropertyAssignment = ts.factory.updatePropertyAssignment( 87 item, item.name, changeNodeFromCallToArrow(item.initializer as ts.CallExpression)); 88 argumentsArray.splice(index, 1, propertyAssignmentNode); 89 } 90 }); 91 if (ischangeNode) { 92 const newNode: ts.ExpressionStatement = ts.factory.updateExpressionStatement(node, 93 ts.factory.createNewExpression(node.expression.expression, node.expression.typeArguments, 94 [ts.factory.createObjectLiteralExpression(argumentsArray, true)])); 95 customComponentNewExpression = createCustomComponentNewExpression( 96 newNode.expression as ts.CallExpression); 97 } 98 } 99 addCustomComponent(node, newStatements, customComponentNewExpression, log); 100 } 101} 102 103function isHasChild(node: ts.CallExpression): boolean { 104 return node.arguments && node.arguments[0] && ts.isObjectLiteralExpression(node.arguments[0]) && 105 node.arguments[0].properties && node.arguments[0].properties.length; 106} 107 108function isToChange(item: ts.PropertyAssignment, node: ts.CallExpression): boolean { 109 const builderParamName: Set<string> = builderParamObjectCollection.get(node.expression.getText()); 110 if (item.initializer && ts.isCallExpression(item.initializer) && builderParamName && 111 builderParamName.has(item.name.getText()) && 112 !/\.(bind|call|apply)/.test(item.initializer.getText())) { 113 return true; 114 } 115 return false; 116} 117 118function changeNodeFromCallToArrow(node: ts.CallExpression): ts.ArrowFunction { 119 return ts.factory.createArrowFunction(undefined, undefined, [], undefined, 120 ts.factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), 121 ts.factory.createBlock([ts.factory.createExpressionStatement(node)], true)); 122} 123 124function addCustomComponent(node: ts.ExpressionStatement, newStatements: ts.Statement[], 125 newNode: ts.NewExpression, log: LogInfo[]): void { 126 if (ts.isNewExpression(newNode)) { 127 const customComponentName: string = getCustomComponentName(newNode); 128 const propertyArray: ts.ObjectLiteralElementLike[] = []; 129 validateCustomComponentPrams(node, customComponentName, propertyArray, log); 130 addCustomComponentStatements(node, newStatements, newNode, customComponentName, propertyArray); 131 } 132} 133 134function addCustomComponentStatements(node: ts.ExpressionStatement, newStatements: ts.Statement[], 135 newNode: ts.NewExpression, name: string, props: ts.ObjectLiteralElementLike[]): void { 136 const id: string = componentInfo.id.toString(); 137 newStatements.push(createFindChildById(id, name), createCustomComponentIfStatement(id, 138 ts.factory.updateExpressionStatement(node, createViewCreate(newNode)), 139 ts.factory.createObjectLiteralExpression(props, true), name)); 140} 141 142function validateCustomComponentPrams(node: ts.ExpressionStatement, name: string, 143 props: ts.ObjectLiteralElementLike[], log: LogInfo[]): void { 144 const curChildProps: Set<string> = new Set([]); 145 const nodeExpression: ts.CallExpression = node.expression as ts.CallExpression; 146 const nodeArguments: ts.NodeArray<ts.Expression> = nodeExpression.arguments; 147 const propertySet: Set<string> = getCollectionSet(name, propertyCollection); 148 const linkSet: Set<string> = getCollectionSet(name, linkCollection); 149 if (nodeArguments && nodeArguments.length === 1 && 150 ts.isObjectLiteralExpression(nodeArguments[0])) { 151 const nodeArgument: ts.ObjectLiteralExpression = nodeArguments[0] as ts.ObjectLiteralExpression; 152 nodeArgument.properties.forEach(item => { 153 if (item.name && ts.isIdentifier(item.name)) { 154 curChildProps.add(item.name.escapedText.toString()); 155 } 156 validateStateManagement(item, name, log); 157 if (isNonThisProperty(item, linkSet)) { 158 if (isToChange(item as ts.PropertyAssignment, node.expression as ts.CallExpression)) { 159 item = ts.factory.updatePropertyAssignment(item as ts.PropertyAssignment, 160 item.name, changeNodeFromCallToArrow(item.initializer)); 161 } 162 props.push(item); 163 } 164 }); 165 } 166 validateMandatoryToAssignmentViaParam(node, name, curChildProps, log); 167} 168 169function getCustomComponentName(newNode: ts.NewExpression): string { 170 let customComponentName: string; 171 if (ts.isIdentifier(newNode.expression)) { 172 customComponentName = newNode.expression.escapedText.toString(); 173 } else if (ts.isPropertyAccessExpression(newNode.expression)) { 174 customComponentName = newNode.expression.name.escapedText.toString(); 175 } 176 return customComponentName; 177} 178 179function getCollectionSet(name: string, collection: Map<string, Set<string>>): Set<string> { 180 if (!collection) { 181 return new Set([]); 182 } 183 return collection.get(name) || new Set([]); 184} 185 186function isThisProperty(node: ts.ObjectLiteralElementLike, propertySet: Set<string>): boolean { 187 if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name) && 188 propertySet.has(node.name.escapedText.toString())) { 189 return true; 190 } 191 return false; 192} 193 194function isNonThisProperty(node: ts.ObjectLiteralElementLike, propertySet: Set<string>): boolean { 195 if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name) && 196 (node.initializer.escapedText && node.initializer.escapedText.includes('$') || 197 ts.isPropertyAccessExpression(node.initializer) && node.initializer.expression && 198 node.initializer.expression.kind === ts.SyntaxKind.ThisKeyword && 199 ts.isIdentifier(node.initializer.name) && node.initializer.name.escapedText.toString().includes('$'))) { 200 return false; 201 } 202 if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name) && 203 !propertySet.has(node.name.escapedText.toString())) { 204 return true; 205 } 206 return false; 207} 208 209function validateStateManagement(node: ts.ObjectLiteralElementLike, customComponentName: string, 210 log: LogInfo[]): void { 211 validateForbiddenToInitViaParam(node, customComponentName, log); 212 checkFromParentToChild(node, customComponentName, log); 213} 214 215function checkFromParentToChild(node: ts.ObjectLiteralElementLike, customComponentName: string, 216 log: LogInfo[]): void { 217 let propertyName: string; 218 if (node.name && node.name.escapedText) { 219 // @ts-ignore 220 propertyName = node.name.escapedText.toString(); 221 } 222 const curPropertyKind: string = getPropertyDecoratorKind(propertyName, customComponentName); 223 if (curPropertyKind) { 224 if (isInitFromParent(node)) { 225 const parentPropertyName: string = 226 getParentPropertyName(node as ts.PropertyAssignment, curPropertyKind, log); 227 if (!parentPropertyName) { 228 return; 229 } 230 const parentPropertyKind: string = curPropMap.get(parentPropertyName); 231 if (parentPropertyKind && !isCorrectInitFormParent(parentPropertyKind, curPropertyKind)) { 232 validateIllegalInitFromParent( 233 node, propertyName, curPropertyKind, parentPropertyName, parentPropertyKind, log); 234 } 235 } else if (isInitFromLocal(node) && ts.isPropertyAssignment(node)) { 236 if (!localArray.includes(curPropertyKind)) { 237 validateIllegalInitFromParent(node, propertyName, curPropertyKind, 238 node.initializer.getText(), COMPONENT_NON_DECORATOR, log); 239 } 240 } 241 } 242} 243 244function isInitFromParent(node: ts.ObjectLiteralElementLike): boolean { 245 if (ts.isPropertyAssignment(node) && node.initializer) { 246 if (ts.isPropertyAccessExpression(node.initializer) && node.initializer.expression && 247 node.initializer.expression.kind === ts.SyntaxKind.ThisKeyword && 248 ts.isIdentifier(node.initializer.name)) { 249 return true; 250 } else if (ts.isIdentifier(node.initializer) && 251 matchStartWithDollar(node.initializer.getText())) { 252 return true; 253 } 254 } 255} 256 257function isInitFromLocal(node: ts.ObjectLiteralElementLike): boolean { 258 if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.initializer) && 259 !matchStartWithDollar(node.initializer.getText())) { 260 return true; 261 } 262} 263 264function getParentPropertyName(node: ts.PropertyAssignment, curPropertyKind: string, 265 log: LogInfo[]): string { 266 let parentPropertyName: string; 267 const initExpression: ts.Expression = node.initializer; 268 if (curPropertyKind === COMPONENT_LINK_DECORATOR) { 269 if (hasDollar(initExpression)) { 270 // @ts-ignore 271 const initName: ts.Identifier = initExpression.name || initExpression; 272 parentPropertyName = initName.getText().replace(/^\$/, ''); 273 } else { 274 validateLinkWithoutDollar(node, log); 275 } 276 } else { 277 if (hasDollar(initExpression)) { 278 validateNonLinkWithDollar(node, log); 279 } else { 280 // @ts-ignore 281 parentPropertyName = node.initializer.name.getText(); 282 } 283 } 284 return parentPropertyName; 285} 286 287function isCorrectInitFormParent(parent: string, child: string): boolean { 288 switch (child) { 289 case COMPONENT_STATE_DECORATOR: 290 case COMPONENT_PROVIDE_DECORATOR: 291 if (parent === COMPONENT_NON_DECORATOR) { 292 return true; 293 } 294 break; 295 case COMPONENT_LINK_DECORATOR: 296 if ([COMPONENT_STATE_DECORATOR, COMPONENT_LINK_DECORATOR, 297 COMPONENT_STORAGE_LINK_DECORATOR].includes(parent)) { 298 return true; 299 } 300 break; 301 case COMPONENT_PROP_DECORATOR: 302 if ([COMPONENT_STATE_DECORATOR, ...propAndLinkDecorators].includes(parent)) { 303 return true; 304 } 305 break; 306 case COMPONENT_NON_DECORATOR: 307 if ([COMPONENT_STATE_DECORATOR, ...propAndLinkDecorators, COMPONENT_NON_DECORATOR, 308 COMPONENT_OBJECT_LINK_DECORATOR, COMPONENT_STORAGE_LINK_DECORATOR].includes(parent)) { 309 return true; 310 } 311 break; 312 case COMPONENT_OBJECT_LINK_DECORATOR: 313 if (parent === COMPONENT_STATE_DECORATOR) { 314 return true; 315 } 316 break; 317 } 318 return false; 319} 320 321function getPropertyDecoratorKind(propertyName: string, customComponentName: string): string { 322 for (const item of decoractorMap.entries()) { 323 if (getCollectionSet(customComponentName, item[1]).has(propertyName)) { 324 return item[0]; 325 } 326 } 327} 328 329function createFindChildById(id: string, name: string): ts.VariableStatement { 330 return ts.factory.createVariableStatement(undefined, ts.factory.createVariableDeclarationList( 331 [ts.factory.createVariableDeclaration(ts.factory.createIdentifier( 332 `${CUSTOM_COMPONENT_EARLIER_CREATE_CHILD}${id}`), undefined, ts.factory.createTypeReferenceNode( 333 ts.factory.createIdentifier(name)), ts.factory.createAsExpression(ts.factory.createCallExpression( 334 ts.factory.createPropertyAccessExpression(ts.factory.createThis(), ts.factory.createIdentifier( 335 `${CUSTOM_COMPONENT_FUNCTION_FIND_CHILD_BY_ID}`)), undefined, [ts.factory.createStringLiteral(id)]), 336 ts.factory.createTypeReferenceNode(ts.factory.createIdentifier(name))))], ts.NodeFlags.Let)); 337} 338 339function createCustomComponentIfStatement(id: string, node: ts.ExpressionStatement, 340 newObjectLiteralExpression: ts.ObjectLiteralExpression, parentName: string): ts.IfStatement { 341 const viewName: string = `${CUSTOM_COMPONENT_EARLIER_CREATE_CHILD}${id}`; 342 return ts.factory.createIfStatement(ts.factory.createBinaryExpression( 343 ts.factory.createIdentifier(viewName), 344 ts.factory.createToken(ts.SyntaxKind.EqualsEqualsToken), 345 ts.factory.createIdentifier(`${COMPONENT_CONSTRUCTOR_UNDEFINED}`)), 346 ts.factory.createBlock([node], true), 347 ts.factory.createBlock([ts.factory.createExpressionStatement(ts.factory.createCallExpression( 348 ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier( 349 viewName), ts.factory.createIdentifier( 350 `${COMPONENT_CONSTRUCTOR_UPDATE_PARAMS}`)), undefined, [newObjectLiteralExpression])), 351 isStaticViewCollection.get(parentName) ? createStaticIf(viewName) : undefined, 352 ts.factory.createExpressionStatement(ts.factory.createCallExpression( 353 ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier(`${BASE_COMPONENT_NAME}`), 354 ts.factory.createIdentifier(`${COMPONENT_CREATE_FUNCTION}`)), undefined, 355 [ts.factory.createIdentifier(viewName)]))], true)); 356} 357 358function createStaticIf(name: string): ts.IfStatement { 359 return ts.factory.createIfStatement(ts.factory.createPrefixUnaryExpression( 360 ts.SyntaxKind.ExclamationToken, ts.factory.createCallExpression( 361 ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier(name), 362 ts.factory.createIdentifier(CUSTOM_COMPONENT_NEEDS_UPDATE_FUNCTION)), undefined, [])), 363 ts.factory.createBlock([ts.factory.createExpressionStatement(ts.factory.createCallExpression( 364 ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier(name), 365 ts.factory.createIdentifier(CUSTOM_COMPONENT_MARK_STATIC_FUNCTION)), 366 undefined, []))], true), undefined); 367} 368 369function hasDollar(initExpression: ts.Expression): boolean { 370 if (ts.isPropertyAccessExpression(initExpression) && 371 matchStartWithDollar(initExpression.name.getText())) { 372 return true; 373 } else if (ts.isIdentifier(initExpression) && matchStartWithDollar(initExpression.getText())) { 374 return true; 375 } else { 376 return false; 377 } 378} 379 380function matchStartWithDollar(name: string): boolean { 381 return /^\$/.test(name); 382} 383 384function validateForbiddenToInitViaParam(node: ts.ObjectLiteralElementLike, 385 customComponentName: string, log: LogInfo[]): void { 386 const forbiddenToInitViaParamSet: Set<string> = new Set([ 387 ...getCollectionSet(customComponentName, storageLinkCollection), 388 ...getCollectionSet(customComponentName, storagePropCollection), 389 ...getCollectionSet(customComponentName, consumeCollection)]); 390 if (isThisProperty(node, forbiddenToInitViaParamSet)) { 391 log.push({ 392 type: LogType.ERROR, 393 message: `Property '${node.name.getText()}' in the custom component '${customComponentName}'` + 394 ` cannot initialize here (forbidden to specify).`, 395 pos: node.name.getStart() 396 }); 397 } 398} 399 400function validateNonExistentProperty(node: ts.ObjectLiteralElementLike, 401 customComponentName: string, log: LogInfo[]): void { 402 log.push({ 403 type: LogType.ERROR, 404 message: `Property '${node.name.escapedText.toString()}' does not exist on type '${customComponentName}'.`, 405 pos: node.name.getStart() 406 }); 407} 408 409function validateMandatoryToAssignmentViaParam(node: ts.CallExpression, customComponentName: string, 410 curChildProps: Set<string>, log: LogInfo[]): void { 411 if (builderParamObjectCollection.get(customComponentName) && 412 builderParamObjectCollection.get(customComponentName).size) { 413 builderParamObjectCollection.get(customComponentName).forEach((item) => { 414 if (!curChildProps.has(item)) { 415 log.push({ 416 type: LogType.ERROR, 417 message: `The property decorated with @BuilderParam '${item}' must be assigned a value .`, 418 pos: node.getStart() 419 }); 420 } 421 }); 422 } 423} 424 425function validateMandatoryToInitViaParam(node: ts.ExpressionStatement, customComponentName: string, 426 curChildProps: Set<string>, log: LogInfo[]): void { 427 const mandatoryToInitViaParamSet: Set<string> = new Set([ 428 ...getCollectionSet(customComponentName, propCollection), 429 ...getCollectionSet(customComponentName, linkCollection), 430 ...getCollectionSet(customComponentName, objectLinkCollection)]); 431 mandatoryToInitViaParamSet.forEach(item => { 432 if (!curChildProps.has(item)) { 433 log.push({ 434 type: LogType.ERROR, 435 message: `Property '${item}' in the custom component '${customComponentName}'` + 436 ` is missing (mandatory to specify).`, 437 pos: node.getStart() 438 }); 439 } 440 }); 441} 442 443function validateIllegalInitFromParent(node: ts.ObjectLiteralElementLike, propertyName: string, 444 curPropertyKind: string, parentPropertyName: string, parentPropertyKind: string, 445 log: LogInfo[]): void { 446 log.push({ 447 type: LogType.ERROR, 448 message: `The ${parentPropertyKind} property '${parentPropertyName}' cannot be assigned to ` + 449 `the ${curPropertyKind} property '${propertyName}'.`, 450 // @ts-ignore 451 pos: node.initializer.getStart() 452 }); 453} 454 455function validateLinkWithoutDollar(node: ts.PropertyAssignment, log: LogInfo[]): void { 456 log.push({ 457 type: LogType.ERROR, 458 message: `The @Link property '${node.name.getText()}' should initialize` + 459 ` using '$' to create a reference to a @State or @Link variable.`, 460 pos: node.initializer.getStart() 461 }); 462} 463 464function validateNonLinkWithDollar(node: ts.PropertyAssignment, log: LogInfo[]): void { 465 log.push({ 466 type: LogType.ERROR, 467 message: `Property '${node.name.getText()}' cannot initialize` + 468 ` using '$' to create a reference to a variable.`, 469 pos: node.initializer.getStart() 470 }); 471} 472