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}