• 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  // eslint-disable-next-line max-lines-per-function
137  filterDiagnostics(
138    tscDiagnostics: readonly ts.Diagnostic[],
139    expr: ts.CallExpression | ts.NewExpression,
140    isLibCall: boolean,
141    filterHandle: (diagnositc: ts.Diagnostic, errorType: ErrorType) => void
142  ): void {
143    const exprRange: CheckRange = { begin: expr.getStart(), end: expr.getEnd() };
144    let validArgsRanges: CheckRange[];
145
146    /*
147     * When the "Object is possibly 'undefined'." error may be caused by another filterable error in the current callExpression,
148     * only then is it necessary to filter the "Object is possibly 'undefined'." error.
149     */
150    let hasFiltered: boolean = false;
151    ArrayUtils.forEachWithDefer(
152      tscDiagnostics,
153      (diagnostic) => {
154        return this.getErrorType(diagnostic) === ErrorType.POSSIBLY_UNDEFINED;
155      },
156      (diagnostic) => {
157        const errorType = this.getErrorType(diagnostic);
158        if (
159          errorType === ErrorType.NO_ERROR ||
160          diagnostic.category === ts.DiagnosticCategory.Warning ||
161          !LibraryTypeCallDiagnosticChecker.isValidErrorType(errorType, isLibCall, hasFiltered) ||
162          !LibraryTypeCallDiagnosticChecker.isValidDiagnosticRange(
163            diagnostic,
164            exprRange,
165            validArgsRanges || (validArgsRanges = RangeUtils.getValidArgsRanges(expr.arguments))
166          )
167        ) {
168          return;
169        }
170
171        hasFiltered = false;
172        filterHandle(diagnostic, errorType);
173      }
174    );
175  }
176
177  private getErrorType(diagnostic: ts.Diagnostic): ErrorType {
178    return this._diagnosticErrorTypeMap.get(diagnostic) ?? ErrorType.NO_ERROR;
179  }
180
181  private static isValidErrorType(errorType: ErrorType, isLibCall: boolean, hasFiltered: boolean): boolean {
182    switch (errorType) {
183      case ErrorType.UNKNOW:
184        return true;
185      case ErrorType.NULL:
186        return isLibCall;
187      case ErrorType.POSSIBLY_UNDEFINED:
188        return isLibCall && hasFiltered;
189      default:
190        return false;
191    }
192  }
193
194  private static isValidDiagnosticRange(
195    diagnostic: ts.Diagnostic,
196    exprRange: CheckRange,
197    validArgsRanges: CheckRange[]
198  ): boolean {
199    if (diagnostic.start === undefined) {
200      return false;
201    }
202    // Some strict mode errors caused by actual parameters. The error message will be mounted on the entire function call node.
203    const isFullCall = !!diagnostic.length && exprRange.end === diagnostic.start + diagnostic.length;
204    switch (diagnostic.code) {
205      case ARGUMENT_OF_TYPE_0_IS_NOT_ASSIGNABLE_TO_PARAMETER_OF_TYPE_1_ERROR_CODE:
206        return RangeUtils.isInRanges(diagnostic.start, validArgsRanges);
207      case NO_OVERLOAD_MATCHES_THIS_CALL_ERROR_CODE:
208        return isFullCall || RangeUtils.isInRanges(diagnostic.start, validArgsRanges);
209      case TYPE_0_IS_NOT_ASSIGNABLE_TO_TYPE_1_ERROR_CODE:
210        return RangeUtils.isInRanges(diagnostic.start, validArgsRanges);
211      case OBJECT_IS_POSSIBLY_UNDEFINED_ERROR_CODE:
212        return isFullCall;
213      default:
214        return false;
215    }
216  }
217}
218
219class MessageUtils {
220  static collectDiagnosticMessage(diagnosticMessageChain: ts.DiagnosticMessageChain, textSet: Set<string>): void {
221    const isTypeError = diagnosticMessageChain.code === TYPE_0_IS_NOT_ASSIGNABLE_TO_TYPE_1_ERROR_CODE;
222    const typeText = isTypeError ?
223      diagnosticMessageChain.messageText :
224      diagnosticMessageChain.messageText.
225        replace(ARGUMENT_OF_TYPE, TYPE).
226        replace(IS_NOT_ASSIGNABLE_TO_PARAMETER_OF_TYPE, IS_NOT_ASSIGNABLE_TO_TYPE);
227    const argumentText = isTypeError ?
228      diagnosticMessageChain.messageText.
229        replace(TYPE, ARGUMENT_OF_TYPE).
230        replace(IS_NOT_ASSIGNABLE_TO_TYPE, IS_NOT_ASSIGNABLE_TO_PARAMETER_OF_TYPE) :
231      diagnosticMessageChain.messageText;
232    textSet.add(typeText);
233    textSet.add(argumentText);
234  }
235
236  static checkMessageErrorType(code: number, messageText: string): ErrorType {
237    if (code === TYPE_0_IS_NOT_ASSIGNABLE_TO_TYPE_1_ERROR_CODE) {
238      if (messageText.match(TYPE_UNKNOWN_IS_NOT_ASSIGNABLE_TO_TYPE_1_RE)) {
239        return ErrorType.UNKNOW;
240      }
241      if (messageText.match(TYPE_UNDEFINED_IS_NOT_ASSIGNABLE_TO_TYPE_1_RE)) {
242        return ErrorType.NULL;
243      }
244      if (messageText.match(TYPE_NULL_IS_NOT_ASSIGNABLE_TO_TYPE_1_RE)) {
245        return ErrorType.NULL;
246      }
247    }
248    if (code === ARGUMENT_OF_TYPE_0_IS_NOT_ASSIGNABLE_TO_PARAMETER_OF_TYPE_1_ERROR_CODE) {
249      if (messageText.match(ARGUMENT_OF_TYPE_UNDEFINED_IS_NOT_ASSIGNABLE_TO_PARAMETER_OF_TYPE_1_RE)) {
250        return ErrorType.NULL;
251      }
252      if (messageText.match(ARGUMENT_OF_TYPE_NULL_IS_NOT_ASSIGNABLE_TO_PARAMETER_OF_TYPE_1_RE)) {
253        return ErrorType.NULL;
254      }
255    }
256    if (code === OBJECT_IS_POSSIBLY_UNDEFINED_ERROR_CODE) {
257      return ErrorType.POSSIBLY_UNDEFINED;
258    }
259    return ErrorType.NO_ERROR;
260  }
261
262  static checkMessageChainErrorType(chain: ts.DiagnosticMessageChain): ErrorType {
263    let errorType = MessageUtils.checkMessageErrorType(chain.code, chain.messageText);
264    if (errorType !== ErrorType.NO_ERROR || !chain.next?.length) {
265      return errorType;
266    }
267    // 'No_overrload... 'Errors need to check each sub-error message, others only check the first one
268    if (chain.code !== NO_OVERLOAD_MATCHES_THIS_CALL_ERROR_CODE) {
269      return MessageUtils.checkMessageChainErrorType(chain.next[0]);
270    }
271
272    for (const child of chain.next) {
273      errorType = MessageUtils.checkMessageChainErrorType(child);
274      if (errorType !== ErrorType.NO_ERROR) {
275        break;
276      }
277    }
278    return errorType;
279  }
280}
281
282class RangeUtils {
283  static isInRanges(pos: number, ranges: CheckRange[]): boolean {
284    for (let i = 0; i < ranges.length; i++) {
285      if (pos >= ranges[i].begin && pos < ranges[i].end) {
286        return true;
287      }
288    }
289    return false;
290  }
291
292  static getValidArgsRanges(args: ts.NodeArray<ts.Expression> | undefined): CheckRange[] {
293    if (!args) {
294      return [];
295    }
296    const nonFilteringRanges = RangeUtils.findNonFilteringRangesFunctionCalls(args);
297    const rangesToFilter: CheckRange[] = [];
298    if (nonFilteringRanges.length !== 0) {
299      const rangesSize = nonFilteringRanges.length;
300      rangesToFilter.push({ begin: args.pos, end: nonFilteringRanges[0].begin });
301      rangesToFilter.push({ begin: nonFilteringRanges[rangesSize - 1].end, end: args.end });
302      for (let i = 0; i < rangesSize - 1; i++) {
303        rangesToFilter.push({ begin: nonFilteringRanges[i].end, end: nonFilteringRanges[i + 1].begin });
304      }
305    } else {
306      rangesToFilter.push({ begin: args.pos, end: args.end });
307    }
308    return rangesToFilter;
309  }
310
311  private static findNonFilteringRangesFunctionCalls(args: ts.NodeArray<ts.Expression>): CheckRange[] {
312    const result: CheckRange[] = [];
313    for (const arg of args) {
314      if (ts.isArrowFunction(arg)) {
315        const arrowFuncExpr = arg;
316        result.push({ begin: arrowFuncExpr.body.pos, end: arrowFuncExpr.body.end });
317      } else if (ts.isCallExpression(arg)) {
318        result.push({ begin: arg.arguments.pos, end: arg.arguments.end });
319      }
320      // there may be other cases
321    }
322    return result;
323  }
324}
325