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