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