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