• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
16/* eslint prefer-named-capture-group: 0 */
17
18import * as ts from 'typescript';
19import ArrayUtils from './ArrayUtils';
20
21/*
22 * Current approach relates on error code and error message matching and it is quite fragile,
23 * so this place should be checked thoroughly in the case of typescript upgrade
24 */
25export const TYPE_0_IS_NOT_ASSIGNABLE_TO_TYPE_1_ERROR_CODE = 2322;
26export const TYPE_UNKNOWN_IS_NOT_ASSIGNABLE_TO_TYPE_1_RE =
27  /^Type '(.*)\bunknown\b(.*)' is not assignable to type '.*'\.$/;
28export const TYPE_NULL_IS_NOT_ASSIGNABLE_TO_TYPE_1_RE = /^Type '(.*)\bnull\b(.*)' is not assignable to type '.*'\.$/;
29export const TYPE_UNDEFINED_IS_NOT_ASSIGNABLE_TO_TYPE_1_RE =
30  /^Type '(.*)\bundefined\b(.*)' is not assignable to type '.*'\.$/;
31
32export const ARGUMENT_OF_TYPE_0_IS_NOT_ASSIGNABLE_TO_PARAMETER_OF_TYPE_1_ERROR_CODE = 2345;
33export const OBJECT_IS_POSSIBLY_UNDEFINED_ERROR_CODE = 2532;
34export const NO_OVERLOAD_MATCHES_THIS_CALL_ERROR_CODE = 2769;
35export const ARGUMENT_OF_TYPE_NULL_IS_NOT_ASSIGNABLE_TO_PARAMETER_OF_TYPE_1_RE =
36  /^Argument of type '(.*)\bnull\b(.*)' is not assignable to parameter of type '.*'\.$/;
37export const ARGUMENT_OF_TYPE_UNDEFINED_IS_NOT_ASSIGNABLE_TO_PARAMETER_OF_TYPE_1_RE =
38  /^Argument of type '(.*)\bundefined\b(.*)' is not assignable to parameter of type '.*'\.$/;
39export const TYPE = 'Type';
40export const IS_NOT_ASSIGNABLE_TO_TYPE = 'is not assignable to type';
41export const ARGUMENT_OF_TYPE = 'Argument of type';
42export const IS_NOT_ASSIGNABLE_TO_PARAMETER_OF_TYPE = 'is not assignable to parameter of type';
43
44export enum ErrorType {
45  NO_ERROR,
46  UNKNOW,
47  NULL,
48  POSSIBLY_UNDEFINED
49}
50
51interface CheckRange {
52  begin: number;
53  end: number;
54}
55
56export class LibraryTypeCallDiagnosticChecker {
57  private static _instance: LibraryTypeCallDiagnosticChecker;
58
59  static get instance(): LibraryTypeCallDiagnosticChecker {
60    if (!LibraryTypeCallDiagnosticChecker._instance) {
61      LibraryTypeCallDiagnosticChecker._instance = new LibraryTypeCallDiagnosticChecker();
62    }
63    return LibraryTypeCallDiagnosticChecker._instance;
64  }
65
66  private _diagnosticErrorTypeMap: Map<ts.Diagnostic, ErrorType> = new Map();
67
68  private constructor() {}
69
70  clear(): void {
71    this._diagnosticErrorTypeMap = new Map();
72  }
73
74  // eslint-disable-next-line max-lines-per-function
75  rebuildTscDiagnostics(tscStrictDiagnostics: Map<string, ts.Diagnostic[]>): void {
76    this.clear();
77    if (tscStrictDiagnostics.size === 0) {
78      return;
79    }
80
81    const diagnosticMessageChainArr: ts.Diagnostic[] = [];
82    const strictArr: ts.Diagnostic[] = [];
83    tscStrictDiagnostics.forEach((strict) => {
84      if (strict.length === 0) {
85        return;
86      }
87
88      for (let i = 0; i < strict.length; i++) {
89        if (typeof strict[i].messageText === 'string') {
90          strictArr.push(strict[i]);
91        } else {
92          diagnosticMessageChainArr.push(strict[i]);
93        }
94      }
95    });
96
97    /**
98     * When there are multiple errors with the same origin,
99     * only the first error message will retain the complete error reason,
100     * while the rest will only have a single line of error information.
101     *
102     * So need to check all the complete error messages first,
103     * and then check the single-line error messages by matching the first line of text.
104     */
105    const nullSet: Set<string> = new Set<string>();
106    const unknownSet: Set<string> = new Set<string>();
107    diagnosticMessageChainArr.forEach((item) => {
108      const diagnosticMessageChain = item.messageText as ts.DiagnosticMessageChain;
109      const errorType: ErrorType = MessageUtils.checkMessageChainErrorType(diagnosticMessageChain);
110      if (errorType === ErrorType.UNKNOW) {
111        MessageUtils.collectDiagnosticMessage(diagnosticMessageChain, unknownSet);
112        this._diagnosticErrorTypeMap.set(item, ErrorType.UNKNOW);
113      } else if (errorType === ErrorType.NULL) {
114        MessageUtils.collectDiagnosticMessage(diagnosticMessageChain, nullSet);
115        this._diagnosticErrorTypeMap.set(item, ErrorType.NULL);
116      }
117    });
118    strictArr.forEach((item) => {
119      const messageText = item.messageText as string;
120      let errorType: ErrorType;
121      if (unknownSet.has(messageText)) {
122        errorType = ErrorType.UNKNOW;
123      } else if (nullSet.has(messageText)) {
124        errorType = ErrorType.NULL;
125      } else {
126        errorType = MessageUtils.checkMessageErrorType(item.code, messageText);
127      }
128
129      if (errorType === ErrorType.NO_ERROR) {
130        return;
131      }
132      this._diagnosticErrorTypeMap.set(item, errorType);
133    });
134  }
135
136  filterDiagnostics(
137    tscDiagnostics: readonly ts.Diagnostic[],
138    expr: ts.CallExpression | ts.NewExpression,
139    isLibCall: boolean,
140    filterHandle: (diagnositc: ts.Diagnostic, errorType: ErrorType) => boolean
141  ): void {
142    const exprRange: CheckRange = { begin: expr.getStart(), end: expr.getEnd() };
143    let validArgsRanges: CheckRange[];
144
145    /*
146     * When the "Object is possibly 'undefined'." error may be caused by another filterable error in the current callExpression,
147     * only then is it necessary to filter the "Object is possibly 'undefined'." error.
148     */
149    let hasFiltered: boolean = false;
150    ArrayUtils.forEachWithDefer(
151      tscDiagnostics,
152      (diagnostic) => {
153        return this.getErrorType(diagnostic) === ErrorType.POSSIBLY_UNDEFINED;
154      },
155      (diagnostic) => {
156        const errorType = this.getErrorType(diagnostic);
157        if (
158          errorType === ErrorType.NO_ERROR ||
159          diagnostic.category === ts.DiagnosticCategory.Warning ||
160          !LibraryTypeCallDiagnosticChecker.isValidErrorType(errorType, isLibCall, hasFiltered) ||
161          !LibraryTypeCallDiagnosticChecker.isValidDiagnosticRange(
162            diagnostic,
163            exprRange,
164            validArgsRanges || (validArgsRanges = RangeUtils.getValidArgsRanges(expr.arguments))
165          )
166        ) {
167          return;
168        }
169
170        hasFiltered = filterHandle(diagnostic, errorType);
171      }
172    );
173  }
174
175  private getErrorType(diagnostic: ts.Diagnostic): ErrorType {
176    return this._diagnosticErrorTypeMap.get(diagnostic) ?? ErrorType.NO_ERROR;
177  }
178
179  private static isValidErrorType(errorType: ErrorType, isLibCall: boolean, hasFiltered: boolean): boolean {
180    switch (errorType) {
181      case ErrorType.UNKNOW:
182        return true;
183      case ErrorType.NULL:
184        return isLibCall;
185      case ErrorType.POSSIBLY_UNDEFINED:
186        return isLibCall && hasFiltered;
187      default:
188        return false;
189    }
190  }
191
192  private static isValidDiagnosticRange(
193    diagnostic: ts.Diagnostic,
194    exprRange: CheckRange,
195    validArgsRanges: CheckRange[]
196  ): boolean {
197    if (diagnostic.start === undefined) {
198      return false;
199    }
200    // Some strict mode errors caused by actual parameters. The error message will be mounted on the entire function call node.
201    const isFullCall = !!diagnostic.length && exprRange.end === diagnostic.start + diagnostic.length;
202    switch (diagnostic.code) {
203      case ARGUMENT_OF_TYPE_0_IS_NOT_ASSIGNABLE_TO_PARAMETER_OF_TYPE_1_ERROR_CODE:
204        return RangeUtils.isInRanges(diagnostic.start, validArgsRanges);
205      case NO_OVERLOAD_MATCHES_THIS_CALL_ERROR_CODE:
206        return isFullCall || RangeUtils.isInRanges(diagnostic.start, validArgsRanges);
207      case TYPE_0_IS_NOT_ASSIGNABLE_TO_TYPE_1_ERROR_CODE:
208        return RangeUtils.isInRanges(diagnostic.start, validArgsRanges);
209      case OBJECT_IS_POSSIBLY_UNDEFINED_ERROR_CODE:
210        return isFullCall;
211      default:
212        return false;
213    }
214  }
215}
216
217class MessageUtils {
218  static collectDiagnosticMessage(diagnosticMessageChain: ts.DiagnosticMessageChain, textSet: Set<string>): void {
219    const isTypeError = diagnosticMessageChain.code === TYPE_0_IS_NOT_ASSIGNABLE_TO_TYPE_1_ERROR_CODE;
220    const typeText = isTypeError ?
221      diagnosticMessageChain.messageText :
222      diagnosticMessageChain.messageText.
223        replace(ARGUMENT_OF_TYPE, TYPE).
224        replace(IS_NOT_ASSIGNABLE_TO_PARAMETER_OF_TYPE, IS_NOT_ASSIGNABLE_TO_TYPE);
225    const argumentText = isTypeError ?
226      diagnosticMessageChain.messageText.
227        replace(TYPE, ARGUMENT_OF_TYPE).
228        replace(IS_NOT_ASSIGNABLE_TO_TYPE, IS_NOT_ASSIGNABLE_TO_PARAMETER_OF_TYPE) :
229      diagnosticMessageChain.messageText;
230    textSet.add(typeText);
231    textSet.add(argumentText);
232  }
233
234  static checkMessageErrorType(code: number, messageText: string): ErrorType {
235    if (code === TYPE_0_IS_NOT_ASSIGNABLE_TO_TYPE_1_ERROR_CODE) {
236      if (messageText.match(TYPE_UNKNOWN_IS_NOT_ASSIGNABLE_TO_TYPE_1_RE)) {
237        return ErrorType.UNKNOW;
238      }
239      if (messageText.match(TYPE_UNDEFINED_IS_NOT_ASSIGNABLE_TO_TYPE_1_RE)) {
240        return ErrorType.NULL;
241      }
242      if (messageText.match(TYPE_NULL_IS_NOT_ASSIGNABLE_TO_TYPE_1_RE)) {
243        return ErrorType.NULL;
244      }
245    }
246    if (code === ARGUMENT_OF_TYPE_0_IS_NOT_ASSIGNABLE_TO_PARAMETER_OF_TYPE_1_ERROR_CODE) {
247      if (messageText.match(ARGUMENT_OF_TYPE_UNDEFINED_IS_NOT_ASSIGNABLE_TO_PARAMETER_OF_TYPE_1_RE)) {
248        return ErrorType.NULL;
249      }
250      if (messageText.match(ARGUMENT_OF_TYPE_NULL_IS_NOT_ASSIGNABLE_TO_PARAMETER_OF_TYPE_1_RE)) {
251        return ErrorType.NULL;
252      }
253    }
254    if (code === OBJECT_IS_POSSIBLY_UNDEFINED_ERROR_CODE) {
255      return ErrorType.POSSIBLY_UNDEFINED;
256    }
257    return ErrorType.NO_ERROR;
258  }
259
260  static checkMessageChainErrorType(chain: ts.DiagnosticMessageChain): ErrorType {
261    let errorType = MessageUtils.checkMessageErrorType(chain.code, chain.messageText);
262    if (errorType !== ErrorType.NO_ERROR || !chain.next?.length) {
263      return errorType;
264    }
265    // 'No_overrload... 'Errors need to check each sub-error message, others only check the first one
266    if (chain.code !== NO_OVERLOAD_MATCHES_THIS_CALL_ERROR_CODE) {
267      return MessageUtils.checkMessageChainErrorType(chain.next[0]);
268    }
269
270    for (const child of chain.next) {
271      errorType = MessageUtils.checkMessageChainErrorType(child);
272      if (errorType !== ErrorType.NO_ERROR) {
273        break;
274      }
275    }
276    return errorType;
277  }
278}
279
280class RangeUtils {
281  static isInRanges(pos: number, ranges: CheckRange[]): boolean {
282    for (let i = 0; i < ranges.length; i++) {
283      if (pos >= ranges[i].begin && pos < ranges[i].end) {
284        return true;
285      }
286    }
287    return false;
288  }
289
290  static getValidArgsRanges(args: ts.NodeArray<ts.Expression> | undefined): CheckRange[] {
291    if (!args) {
292      return [];
293    }
294    const nonFilteringRanges = RangeUtils.findNonFilteringRangesFunctionCalls(args);
295    const rangesToFilter: CheckRange[] = [];
296    if (nonFilteringRanges.length !== 0) {
297      const rangesSize = nonFilteringRanges.length;
298      rangesToFilter.push({ begin: args.pos, end: nonFilteringRanges[0].begin });
299      rangesToFilter.push({ begin: nonFilteringRanges[rangesSize - 1].end, end: args.end });
300      for (let i = 0; i < rangesSize - 1; i++) {
301        rangesToFilter.push({ begin: nonFilteringRanges[i].end, end: nonFilteringRanges[i + 1].begin });
302      }
303    } else {
304      rangesToFilter.push({ begin: args.pos, end: args.end });
305    }
306    return rangesToFilter;
307  }
308
309  private static findNonFilteringRangesFunctionCalls(args: ts.NodeArray<ts.Expression>): CheckRange[] {
310    const result: CheckRange[] = [];
311    for (const arg of args) {
312      if (ts.isArrowFunction(arg)) {
313        const arrowFuncExpr = arg;
314        result.push({ begin: arrowFuncExpr.body.pos, end: arrowFuncExpr.body.end });
315      } else if (ts.isCallExpression(arg)) {
316        result.push({ begin: arg.arguments.pos, end: arg.arguments.end });
317      }
318      // there may be other cases
319    }
320    return result;
321  }
322}
323