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}