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