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