1/* 2 * Copyright (c) 2023-2024 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 ts from 'typescript'; 17import type { TsUtils } from '../TsUtils'; 18 19type CheckStdCallApi = (callExpr: ts.CallExpression) => boolean; 20type StdCallApiEntry = Map<string, CheckStdCallApi>; 21 22export class SupportedStdCallApiChecker { 23 tsUtils: TsUtils; 24 typeChecker: ts.TypeChecker; 25 26 constructor(tsUtils: TsUtils, typeChecker: ts.TypeChecker) { 27 this.tsUtils = tsUtils; 28 this.typeChecker = typeChecker; 29 } 30 31 stdObjectEntry = new Map<string, CheckStdCallApi>([['assign', this.checkObjectAssignCall]]); 32 33 StdCallApi = new Map<string | undefined, StdCallApiEntry>([ 34 ['Object', this.stdObjectEntry], 35 ['ObjectConstructor', this.stdObjectEntry] 36 ]); 37 38 private static getCallExprNode(node: ts.Identifier): ts.CallExpression | undefined { 39 let callExpr: ts.CallExpression | undefined; 40 if (ts.isCallExpression(node.parent)) { 41 callExpr = node.parent; 42 } else if (ts.isPropertyAccessExpression(node.parent) && ts.isCallExpression(node.parent.parent)) { 43 callExpr = node.parent.parent; 44 } 45 return callExpr; 46 } 47 48 isSupportedStdCallAPI( 49 node: ts.Identifier | ts.CallExpression, 50 parentSymName: string | undefined, 51 symName: string 52 ): boolean { 53 const callExpr = ts.isIdentifier(node) ? SupportedStdCallApiChecker.getCallExprNode(node) : node; 54 if (!callExpr) { 55 return false; 56 } 57 58 const entry = this.StdCallApi.get(parentSymName); 59 if (entry) { 60 const stdCallApiCheckCb = entry.get(symName); 61 return !!stdCallApiCheckCb && stdCallApiCheckCb.call(this, callExpr); 62 } 63 64 return false; 65 } 66 67 private checkObjectAssignCall(callExpr: ts.CallExpression): boolean { 68 69 /* 70 * 'Object.assign' is allowed only with signature like following: 71 * assign(target: Record<string, V>, ...source: Object[]>): Record<String, V> 72 * 73 * Note: For 'return' type, check the contextual type of call expression, as the 74 * return type of actual call signature will be deduced as an intersection of all 75 * types of the 'target' and 'source' arguments. 76 */ 77 78 if (callExpr.typeArguments || callExpr.arguments.length === 0) { 79 return false; 80 } 81 const targetArgType = this.typeChecker.getTypeAtLocation(callExpr.arguments[0]); 82 if (!this.isValidObjectAssignRecordType(targetArgType)) { 83 return false; 84 } 85 const contextualType = this.typeChecker.getContextualType(callExpr); 86 if (!contextualType || !this.isValidObjectAssignRecordType(contextualType)) { 87 return false; 88 } 89 90 return true; 91 } 92 93 private isValidObjectAssignRecordType(type: ts.Type): boolean { 94 if (this.tsUtils.isStdRecordType(type) && type.aliasTypeArguments?.length) { 95 const typeArg = type.aliasTypeArguments[0]; 96 if (typeArg.getFlags() & (ts.TypeFlags.String | ts.TypeFlags.StringLiteral)) { 97 return true; 98 } 99 } 100 return false; 101 } 102} 103