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