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