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