1/* 2 * Copyright (c) 2022-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 */ 15import * as fs from 'fs'; 16import * as path from 'node:path'; 17import * as ts from 'typescript'; 18import { cookBookTag } from './CookBookMsg'; 19import { faultsAttrs } from './FaultAttrs'; 20import { faultDesc } from './FaultDesc'; 21import { Logger } from './Logger'; 22import type { ProblemInfo } from './ProblemInfo'; 23import { ProblemSeverity } from './ProblemSeverity'; 24import { FaultID } from './Problems'; 25import { LinterConfig } from './TypeScriptLinterConfig'; 26import { cookBookRefToFixTitle } from './autofixes/AutofixTitles'; 27import type { Autofix } from './autofixes/Autofixer'; 28import { TsUtils } from './utils/TsUtils'; 29import { ARKTS_COLLECTIONS_D_ETS, ARKTS_LANG_D_ETS } from './utils/consts/SupportedDetsIndexableTypes'; 30import { D_ETS, D_TS, ETS, KIT } from './utils/consts/TsSuffix'; 31import { forEachNodeInSubtree } from './utils/functions/ForEachNodeInSubtree'; 32import type { LinterOptions } from './LinterOptions'; 33 34export interface KitSymbol { 35 source: string; 36 bindings: string; 37} 38 39export type KitSymbols = Record<string, KitSymbol>; 40 41export interface KitInfo { 42 symbols?: KitSymbols; 43} 44 45export class InteropTypescriptLinter { 46 totalVisitedNodes: number = 0; 47 nodeCounters: number[] = []; 48 lineCounters: number[] = []; 49 50 totalErrorLines: number = 0; 51 errorLineNumbersString: string = ''; 52 totalWarningLines: number = 0; 53 warningLineNumbersString: string = ''; 54 55 problemsInfos: ProblemInfo[] = []; 56 57 tsUtils: TsUtils; 58 59 currentErrorLine: number; 60 currentWarningLine: number; 61 62 private sourceFile?: ts.SourceFile; 63 private isInSdk?: boolean; 64 static kitInfos = new Map<string, KitInfo>(); 65 private static etsLoaderPath?: string; 66 private static sdkPath?: string; 67 68 static initGlobals(): void { 69 InteropTypescriptLinter.kitInfos = new Map<string, KitInfo>(); 70 } 71 72 private initCounters(): void { 73 for (let i = 0; i < FaultID.LAST_ID; i++) { 74 this.nodeCounters[i] = 0; 75 this.lineCounters[i] = 0; 76 } 77 } 78 79 constructor( 80 private readonly tsTypeChecker: ts.TypeChecker, 81 private readonly compileOptions: ts.CompilerOptions, 82 readonly options: LinterOptions, 83 etsLoaderPath: string | undefined 84 ) { 85 this.tsUtils = new TsUtils(this.tsTypeChecker, options); 86 this.currentErrorLine = 0; 87 this.currentWarningLine = 0; 88 InteropTypescriptLinter.etsLoaderPath = etsLoaderPath; 89 InteropTypescriptLinter.sdkPath = etsLoaderPath ? path.resolve(etsLoaderPath, '../..') : undefined; 90 this.initCounters(); 91 } 92 93 readonly handlersMap = new Map([ 94 [ts.SyntaxKind.ImportDeclaration, this.handleImportDeclaration], 95 [ts.SyntaxKind.InterfaceDeclaration, this.handleInterfaceDeclaration], 96 [ts.SyntaxKind.ClassDeclaration, this.handleClassDeclaration], 97 [ts.SyntaxKind.NewExpression, this.handleNewExpression], 98 [ts.SyntaxKind.ObjectLiteralExpression, this.handleObjectLiteralExpression], 99 [ts.SyntaxKind.ArrayLiteralExpression, this.handleArrayLiteralExpression], 100 [ts.SyntaxKind.AsExpression, this.handleAsExpression], 101 [ts.SyntaxKind.ExportDeclaration, this.handleExportDeclaration], 102 [ts.SyntaxKind.ExportAssignment, this.handleExportAssignment] 103 ]); 104 105 private getLineAndCharacterOfNode(node: ts.Node | ts.CommentRange): ts.LineAndCharacter { 106 const startPos = TsUtils.getStartPos(node); 107 const { line, character } = this.sourceFile!.getLineAndCharacterOfPosition(startPos); 108 // TSC counts lines and columns from zero 109 return { line: line + 1, character: character + 1 }; 110 } 111 112 incrementCounters(node: ts.Node | ts.CommentRange, faultId: number, autofix?: Autofix[]): void { 113 this.nodeCounters[faultId]++; 114 const { line, character } = this.getLineAndCharacterOfNode(node); 115 if (this.options.ideMode) { 116 this.incrementCountersIdeMode(node, faultId, autofix); 117 } else { 118 const faultDescr = faultDesc[faultId]; 119 const faultType = LinterConfig.tsSyntaxKindNames[node.kind]; 120 Logger.info( 121 `Warning: ${this.sourceFile!.fileName} (${line}, ${character}): ${faultDescr ? faultDescr : faultType}` 122 ); 123 } 124 this.lineCounters[faultId]++; 125 switch (faultsAttrs[faultId].severity) { 126 case ProblemSeverity.ERROR: { 127 this.currentErrorLine = line; 128 ++this.totalErrorLines; 129 this.errorLineNumbersString += line + ', '; 130 break; 131 } 132 case ProblemSeverity.WARNING: { 133 if (line === this.currentWarningLine) { 134 break; 135 } 136 this.currentWarningLine = line; 137 ++this.totalWarningLines; 138 this.warningLineNumbersString += line + ', '; 139 break; 140 } 141 default: 142 } 143 } 144 145 private incrementCountersIdeMode(node: ts.Node | ts.CommentRange, faultId: number, autofix?: Autofix[]): void { 146 if (!this.options.ideMode) { 147 return; 148 } 149 const [startOffset, endOffset] = TsUtils.getHighlightRange(node, faultId); 150 const startPos = this.sourceFile!.getLineAndCharacterOfPosition(startOffset); 151 const endPos = this.sourceFile!.getLineAndCharacterOfPosition(endOffset); 152 153 const faultDescr = faultDesc[faultId]; 154 const faultType = LinterConfig.tsSyntaxKindNames[node.kind]; 155 156 const cookBookMsgNum = faultsAttrs[faultId] ? faultsAttrs[faultId].cookBookRef : 0; 157 const cookBookTg = cookBookTag[cookBookMsgNum]; 158 const severity = faultsAttrs[faultId]?.severity ?? ProblemSeverity.ERROR; 159 const isMsgNumValid = cookBookMsgNum > 0; 160 const badNodeInfo: ProblemInfo = { 161 line: startPos.line + 1, 162 column: startPos.character + 1, 163 endLine: endPos.line + 1, 164 endColumn: endPos.character + 1, 165 start: startOffset, 166 end: endOffset, 167 type: faultType, 168 severity: severity, 169 problem: FaultID[faultId], 170 suggest: '', 171 // eslint-disable-next-line no-nested-ternary 172 rule: isMsgNumValid && cookBookTg !== '' ? cookBookTg : faultDescr ? faultDescr : faultType, 173 ruleTag: cookBookMsgNum, 174 autofix: autofix, 175 autofixTitle: isMsgNumValid && autofix !== undefined ? cookBookRefToFixTitle.get(cookBookMsgNum) : undefined 176 }; 177 this.problemsInfos.push(badNodeInfo); 178 } 179 180 private visitSourceFile(sf: ts.SourceFile): void { 181 const callback = (node: ts.Node): void => { 182 this.totalVisitedNodes++; 183 const handler = this.handlersMap.get(node.kind); 184 if (handler !== undefined) { 185 186 /* 187 * possibly requested cancellation will be checked in a limited number of handlers 188 * checked nodes are selected as construct nodes, similar to how TSC does 189 */ 190 handler.call(this, node); 191 } 192 }; 193 const stopCondition = (node: ts.Node): boolean => { 194 if (!node) { 195 return true; 196 } 197 if (LinterConfig.terminalTokens.has(node.kind)) { 198 return true; 199 } 200 return false; 201 }; 202 forEachNodeInSubtree(sf, callback, stopCondition); 203 } 204 205 private handleImportDeclaration(node: ts.Node): void { 206 const importDeclaration = node as ts.ImportDeclaration; 207 this.checkSendableClassOrISendable(importDeclaration); 208 } 209 210 private checkSendableClassOrISendable(node: ts.ImportDeclaration): void { 211 const currentSourceFile = node.getSourceFile(); 212 const contextSpecifier = node.moduleSpecifier; 213 if (!ts.isStringLiteralLike(contextSpecifier)) { 214 return; 215 } 216 const resolvedModule = this.getResolveModule(contextSpecifier.text, currentSourceFile.fileName); 217 const importClause = node.importClause; 218 if (!resolvedModule) { 219 return; 220 } 221 // handle kit 222 const baseFileName = path.basename(resolvedModule.resolvedFileName); 223 if (baseFileName.startsWith(KIT) && baseFileName.endsWith(D_TS)) { 224 this.checkSendableClassOrISendableKit(baseFileName, importClause); 225 return; 226 } 227 228 if ( 229 resolvedModule?.extension !== ETS && resolvedModule?.extension !== D_ETS || 230 TsUtils.isInImportWhiteList(resolvedModule) 231 ) { 232 return; 233 } 234 235 if (!importClause) { 236 this.incrementCounters(node, FaultID.NoSideEffectImportEtsToTs); 237 return; 238 } 239 this.checkImportClause(importClause, resolvedModule); 240 } 241 242 private checkSendableClassOrISendableKit(baseFileName: string, importClause: ts.ImportClause | undefined): void { 243 if (!InteropTypescriptLinter.etsLoaderPath) { 244 return; 245 } 246 InteropTypescriptLinter.initKitInfos(baseFileName); 247 248 if (!importClause) { 249 return; 250 } 251 252 // skip default import 253 if (importClause.name) { 254 return; 255 } 256 257 if (importClause.namedBindings && ts.isNamedImports(importClause.namedBindings)) { 258 this.checkKitImportClause(importClause.namedBindings, baseFileName); 259 } 260 } 261 262 private getResolveModule(moduleSpecifier: string, fileName: string): ts.ResolvedModuleFull | undefined { 263 const resolveModuleName = ts.resolveModuleName(moduleSpecifier, fileName, this.compileOptions, ts.sys); 264 return resolveModuleName.resolvedModule; 265 } 266 267 private checkKitImportClause(node: ts.NamedImports | ts.NamedExports, kitFileName: string): void { 268 const length = node.elements.length; 269 for (let i = 0; i < length; i++) { 270 const fileName = InteropTypescriptLinter.getKitModuleFileNames(kitFileName, node, i); 271 if (fileName === '' || fileName.endsWith(D_TS)) { 272 continue; 273 } 274 275 const element = node.elements[i]; 276 const decl = this.tsUtils.getDeclarationNode(element.name); 277 if (!decl) { 278 continue; 279 } 280 if ( 281 ts.isModuleDeclaration(decl) && fileName !== ARKTS_COLLECTIONS_D_ETS && fileName !== ARKTS_LANG_D_ETS || 282 !this.tsUtils.isSendableClassOrInterfaceEntity(element.name) 283 ) { 284 this.incrementCounters(element, FaultID.NoTsImportEts); 285 } 286 } 287 } 288 289 private checkImportClause(node: ts.ImportClause, resolvedModule: ts.ResolvedModuleFull): void { 290 const checkAndIncrement = (identifier: ts.Identifier | undefined): void => { 291 if (identifier && !this.tsUtils.isSendableClassOrInterfaceEntity(identifier)) { 292 this.incrementCounters(identifier, FaultID.NoTsImportEts); 293 } 294 }; 295 if (node.name) { 296 if (this.allowInSdkImportSendable(resolvedModule)) { 297 return; 298 } 299 checkAndIncrement(node.name); 300 } 301 if (!node.namedBindings) { 302 return; 303 } 304 if (ts.isNamespaceImport(node.namedBindings)) { 305 this.incrementCounters(node.namedBindings, FaultID.NoNameSpaceImportEtsToTs); 306 } else if (ts.isNamedImports(node.namedBindings)) { 307 node.namedBindings.elements.forEach((element: ts.ImportSpecifier) => { 308 checkAndIncrement(element.name); 309 }); 310 } 311 } 312 313 private allowInSdkImportSendable(resolvedModule: ts.ResolvedModuleFull): boolean { 314 const resolvedModuleIsInSdk = InteropTypescriptLinter.sdkPath ? 315 path.normalize(resolvedModule.resolvedFileName).startsWith(InteropTypescriptLinter.sdkPath) : 316 false; 317 return ( 318 !!this.isInSdk && 319 resolvedModuleIsInSdk && 320 path.basename(resolvedModule.resolvedFileName).indexOf('sendable') !== -1 321 ); 322 } 323 324 private handleClassDeclaration(node: ts.Node): void { 325 const tsClassDecl = node as ts.ClassDeclaration; 326 if (!tsClassDecl.heritageClauses) { 327 return; 328 } 329 330 for (const hClause of tsClassDecl.heritageClauses) { 331 if (hClause) { 332 this.checkClassOrInterfaceDeclarationHeritageClause(hClause); 333 } 334 } 335 } 336 337 // In ts files, sendable classes and sendable interfaces can not be extended or implemented. 338 private checkClassOrInterfaceDeclarationHeritageClause(hClause: ts.HeritageClause): void { 339 for (const tsTypeExpr of hClause.types) { 340 341 /* 342 * Always resolve type from 'tsTypeExpr' node, not from 'tsTypeExpr.expression' node, 343 * as for the latter, type checker will return incorrect type result for classes in 344 * 'extends' clause. Additionally, reduce reference, as mostly type checker returns 345 * the TypeReference type objects for classes and interfaces. 346 */ 347 const tsExprType = TsUtils.reduceReference(this.tsTypeChecker.getTypeAtLocation(tsTypeExpr)); 348 const isSendableBaseType = this.tsUtils.isSendableClassOrInterface(tsExprType); 349 if (isSendableBaseType) { 350 this.incrementCounters(tsTypeExpr, FaultID.SendableTypeInheritance); 351 } 352 } 353 } 354 355 private handleInterfaceDeclaration(node: ts.Node): void { 356 const interfaceNode = node as ts.InterfaceDeclaration; 357 const iSymbol = this.tsUtils.trueSymbolAtLocation(interfaceNode.name); 358 const iDecls = iSymbol ? iSymbol.getDeclarations() : null; 359 if (!iDecls) { 360 return; 361 } 362 363 if (!interfaceNode.heritageClauses) { 364 return; 365 } 366 367 for (const hClause of interfaceNode.heritageClauses) { 368 if (hClause) { 369 this.checkClassOrInterfaceDeclarationHeritageClause(hClause); 370 } 371 } 372 } 373 374 private handleNewExpression(node: ts.Node): void { 375 const tsNewExpr = node as ts.NewExpression; 376 this.handleSendableGenericTypes(tsNewExpr); 377 } 378 379 private handleSendableGenericTypes(node: ts.NewExpression): void { 380 const type = this.tsTypeChecker.getTypeAtLocation(node); 381 if (!this.tsUtils.isSendableClassOrInterface(type)) { 382 return; 383 } 384 385 const typeArgs = node.typeArguments; 386 if (!typeArgs || typeArgs.length === 0) { 387 return; 388 } 389 390 for (const arg of typeArgs) { 391 if (!this.tsUtils.isSendableTypeNode(arg)) { 392 this.incrementCounters(arg, FaultID.SendableGenericTypes); 393 } 394 } 395 } 396 397 private handleObjectLiteralExpression(node: ts.Node): void { 398 const objectLiteralExpr = node as ts.ObjectLiteralExpression; 399 const objectLiteralType = this.tsTypeChecker.getContextualType(objectLiteralExpr); 400 if (objectLiteralType && this.tsUtils.typeContainsSendableClassOrInterface(objectLiteralType)) { 401 this.incrementCounters(node, FaultID.SendableObjectInitialization); 402 } 403 } 404 405 private handleArrayLiteralExpression(node: ts.Node): void { 406 const arrayLitNode = node as ts.ArrayLiteralExpression; 407 const arrayLitType = this.tsTypeChecker.getContextualType(arrayLitNode); 408 if (arrayLitType && this.tsUtils.typeContainsSendableClassOrInterface(arrayLitType)) { 409 this.incrementCounters(node, FaultID.SendableObjectInitialization); 410 } 411 } 412 413 private handleAsExpression(node: ts.Node): void { 414 const tsAsExpr = node as ts.AsExpression; 415 const targetType = this.tsTypeChecker.getTypeAtLocation(tsAsExpr.type).getNonNullableType(); 416 const exprType = this.tsTypeChecker.getTypeAtLocation(tsAsExpr.expression).getNonNullableType(); 417 418 if ( 419 !this.tsUtils.isSendableClassOrInterface(exprType) && 420 !this.tsUtils.isObject(exprType) && 421 !TsUtils.isAnyType(exprType) && 422 this.tsUtils.isSendableClassOrInterface(targetType) 423 ) { 424 this.incrementCounters(tsAsExpr, FaultID.SendableAsExpr); 425 } 426 } 427 428 private handleExportDeclaration(node: ts.Node): void { 429 const exportDecl = node as ts.ExportDeclaration; 430 const currentSourceFile = exportDecl.getSourceFile(); 431 const contextSpecifier = exportDecl.moduleSpecifier; 432 433 if (contextSpecifier && ts.isStringLiteralLike(contextSpecifier)) { 434 const resolvedModule = this.getResolveModule(contextSpecifier.text, currentSourceFile.fileName); 435 436 if (!resolvedModule) { 437 return; 438 } 439 440 if (this.isKitModule(resolvedModule.resolvedFileName, exportDecl)) { 441 return; 442 } 443 444 if (InteropTypescriptLinter.isEtsFile(resolvedModule.extension)) { 445 this.incrementCounters(contextSpecifier, FaultID.NoTsReExportEts); 446 } 447 return; 448 } 449 450 if (!this.isInSdk) { 451 return; 452 } 453 454 this.handleSdkExport(exportDecl); 455 } 456 457 private isKitModule(resolvedFileName: string, exportDecl: ts.ExportDeclaration): boolean { 458 const baseFileName = path.basename(resolvedFileName); 459 if (baseFileName.startsWith(KIT) && baseFileName.endsWith(D_TS)) { 460 if (!InteropTypescriptLinter.etsLoaderPath) { 461 return true; 462 } 463 InteropTypescriptLinter.initKitInfos(baseFileName); 464 const exportClause = exportDecl.exportClause; 465 466 if (exportClause && ts.isNamedExports(exportClause)) { 467 this.checkKitImportClause(exportClause, baseFileName); 468 } 469 return true; 470 } 471 return false; 472 } 473 474 private static isEtsFile(extension: string | undefined): boolean { 475 return extension === ETS || extension === D_ETS; 476 } 477 478 private handleSdkExport(exportDecl: ts.ExportDeclaration): void { 479 if (!exportDecl.exportClause || !ts.isNamedExports(exportDecl.exportClause)) { 480 return; 481 } 482 483 for (const exportSpecifier of exportDecl.exportClause.elements) { 484 if (this.tsUtils.isSendableClassOrInterfaceEntity(exportSpecifier.name)) { 485 this.incrementCounters(exportSpecifier.name, FaultID.SendableTypeExported); 486 } 487 } 488 } 489 490 private handleExportAssignment(node: ts.Node): void { 491 if (!this.isInSdk) { 492 return; 493 } 494 495 // In sdk .d.ts files, sendable classes and sendable interfaces can not be "default" exported. 496 const exportAssignment = node as ts.ExportAssignment; 497 498 if (this.tsUtils.isSendableClassOrInterfaceEntity(exportAssignment.expression)) { 499 this.incrementCounters(exportAssignment.expression, FaultID.SendableTypeExported); 500 } 501 } 502 503 private static initKitInfos(fileName: string): void { 504 if (InteropTypescriptLinter.kitInfos.has(fileName)) { 505 return; 506 } 507 508 const JSON_SUFFIX = '.json'; 509 const KIT_CONFIGS = '../ets-loader/kit_configs'; 510 const KIT_CONFIG_PATH = './build-tools/ets-loader/kit_configs'; 511 512 const kitConfigs: string[] = [path.resolve(InteropTypescriptLinter.etsLoaderPath as string, KIT_CONFIGS)]; 513 if (process.env.externalApiPaths) { 514 const externalApiPaths = process.env.externalApiPaths.split(path.delimiter); 515 externalApiPaths.forEach((sdkPath) => { 516 kitConfigs.push(path.resolve(sdkPath, KIT_CONFIG_PATH)); 517 }); 518 } 519 520 for (const kitConfig of kitConfigs) { 521 const kitModuleConfigJson = path.resolve(kitConfig, './' + fileName.replace(D_TS, JSON_SUFFIX)); 522 if (fs.existsSync(kitModuleConfigJson)) { 523 InteropTypescriptLinter.kitInfos.set(fileName, JSON.parse(fs.readFileSync(kitModuleConfigJson, 'utf-8'))); 524 } 525 } 526 } 527 528 private static getKitModuleFileNames( 529 fileName: string, 530 node: ts.NamedImports | ts.NamedExports, 531 index: number 532 ): string { 533 if (!InteropTypescriptLinter.kitInfos.has(fileName)) { 534 return ''; 535 } 536 537 const kitInfo = InteropTypescriptLinter.kitInfos.get(fileName); 538 if (!kitInfo?.symbols) { 539 return ''; 540 } 541 542 const element = node.elements[index]; 543 return element.propertyName ? 544 kitInfo.symbols[element.propertyName.text].source : 545 kitInfo.symbols[element.name.text].source; 546 } 547 548 lint(sourceFile: ts.SourceFile): void { 549 this.sourceFile = sourceFile; 550 this.isInSdk = InteropTypescriptLinter.sdkPath ? 551 path.normalize(this.sourceFile.fileName).indexOf(InteropTypescriptLinter.sdkPath) === 0 : 552 false; 553 this.visitSourceFile(this.sourceFile); 554 } 555} 556