1/* 2 * Copyright (c) 2022-2025 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 { FaultID } from './Problems'; 19import { TypeScriptLinterConfig } from './TypeScriptLinterConfig'; 20import { TsUtils } from './utils/TsUtils'; 21import { ARKTS_COLLECTIONS_D_ETS, ARKTS_LANG_D_ETS } from './utils/consts/SupportedDetsIndexableTypes'; 22import { D_ETS, D_TS, ETS, KIT } from './utils/consts/TsSuffix'; 23import { forEachNodeInSubtree } from './utils/functions/ForEachNodeInSubtree'; 24import type { LinterOptions } from './LinterOptions'; 25import { BaseTypeScriptLinter } from './BaseTypeScriptLinter'; 26 27export interface KitSymbol { 28 source: string; 29 bindings: string; 30} 31 32export type KitSymbols = Record<string, KitSymbol>; 33 34export interface KitInfo { 35 symbols?: KitSymbols; 36} 37 38export class InteropTypescriptLinter extends BaseTypeScriptLinter { 39 private isInSdk?: boolean; 40 static kitInfos = new Map<string, KitInfo>(); 41 private static etsLoaderPath?: string; 42 private static sdkPath?: string; 43 44 static initGlobals(): void { 45 InteropTypescriptLinter.kitInfos = new Map<string, KitInfo>(); 46 } 47 48 constructor( 49 tsTypeChecker: ts.TypeChecker, 50 readonly compileOptions: ts.CompilerOptions, 51 options: LinterOptions, 52 sourceFile: ts.SourceFile 53 ) { 54 super(tsTypeChecker, options, sourceFile); 55 InteropTypescriptLinter.etsLoaderPath = options.etsLoaderPath; 56 InteropTypescriptLinter.sdkPath = options.etsLoaderPath ? path.resolve(options.etsLoaderPath, '../..') : undefined; 57 } 58 59 readonly handlersMap = new Map([ 60 [ts.SyntaxKind.ImportDeclaration, this.handleImportDeclaration], 61 [ts.SyntaxKind.InterfaceDeclaration, this.handleInterfaceDeclaration], 62 [ts.SyntaxKind.ClassDeclaration, this.handleClassDeclaration], 63 [ts.SyntaxKind.NewExpression, this.handleNewExpression], 64 [ts.SyntaxKind.ObjectLiteralExpression, this.handleObjectLiteralExpression], 65 [ts.SyntaxKind.ArrayLiteralExpression, this.handleArrayLiteralExpression], 66 [ts.SyntaxKind.AsExpression, this.handleAsExpression], 67 [ts.SyntaxKind.ExportDeclaration, this.handleExportDeclaration], 68 [ts.SyntaxKind.ExportAssignment, this.handleExportAssignment] 69 ]); 70 71 private visitSourceFile(sf: ts.SourceFile): void { 72 const callback = (node: ts.Node): void => { 73 this.fileStats.visitedNodes++; 74 const handler = this.handlersMap.get(node.kind); 75 if (handler !== undefined) { 76 77 /* 78 * possibly requested cancellation will be checked in a limited number of handlers 79 * checked nodes are selected as construct nodes, similar to how TSC does 80 */ 81 handler.call(this, node); 82 } 83 }; 84 const stopCondition = (node: ts.Node): boolean => { 85 if (!node) { 86 return true; 87 } 88 if (TypeScriptLinterConfig.terminalTokens.has(node.kind)) { 89 return true; 90 } 91 return false; 92 }; 93 forEachNodeInSubtree(sf, callback, stopCondition); 94 } 95 96 private handleImportDeclaration(node: ts.Node): void { 97 const importDeclaration = node as ts.ImportDeclaration; 98 this.checkSendableClassOrISendable(importDeclaration); 99 } 100 101 private checkSendableClassOrISendable(node: ts.ImportDeclaration): void { 102 const currentSourceFile = node.getSourceFile(); 103 const contextSpecifier = node.moduleSpecifier; 104 if (!ts.isStringLiteralLike(contextSpecifier)) { 105 return; 106 } 107 const resolvedModule = this.getResolveModule(contextSpecifier.text, currentSourceFile.fileName); 108 const importClause = node.importClause; 109 if (!resolvedModule) { 110 return; 111 } 112 // handle kit 113 const baseFileName = path.basename(resolvedModule.resolvedFileName); 114 if (baseFileName.startsWith(KIT) && baseFileName.endsWith(D_TS)) { 115 this.checkSendableClassOrISendableKit(baseFileName, importClause); 116 return; 117 } 118 119 if ( 120 resolvedModule?.extension !== ETS && resolvedModule?.extension !== D_ETS || 121 TsUtils.isInImportWhiteList(resolvedModule) 122 ) { 123 return; 124 } 125 126 if (!importClause) { 127 this.incrementCounters(node, FaultID.NoSideEffectImportEtsToTs); 128 return; 129 } 130 this.checkImportClause(importClause, resolvedModule); 131 } 132 133 private checkSendableClassOrISendableKit(baseFileName: string, importClause: ts.ImportClause | undefined): void { 134 if (!InteropTypescriptLinter.etsLoaderPath) { 135 return; 136 } 137 InteropTypescriptLinter.initKitInfos(baseFileName); 138 139 if (!importClause) { 140 return; 141 } 142 143 // skip default import 144 if (importClause.name) { 145 return; 146 } 147 148 if (importClause.namedBindings && ts.isNamedImports(importClause.namedBindings)) { 149 this.checkKitImportClause(importClause.namedBindings, baseFileName); 150 } 151 } 152 153 private getResolveModule(moduleSpecifier: string, fileName: string): ts.ResolvedModuleFull | undefined { 154 const resolveModuleName = ts.resolveModuleName(moduleSpecifier, fileName, this.compileOptions, ts.sys); 155 return resolveModuleName.resolvedModule; 156 } 157 158 private checkKitImportClause(node: ts.NamedImports | ts.NamedExports, kitFileName: string): void { 159 const length = node.elements.length; 160 for (let i = 0; i < length; i++) { 161 const fileName = InteropTypescriptLinter.getKitModuleFileNames(kitFileName, node, i); 162 if (fileName === '' || fileName.endsWith(D_TS)) { 163 continue; 164 } 165 166 const element = node.elements[i]; 167 const decl = this.tsUtils.getDeclarationNode(element.name); 168 if (!decl) { 169 continue; 170 } 171 if ( 172 ts.isModuleDeclaration(decl) && fileName !== ARKTS_COLLECTIONS_D_ETS && fileName !== ARKTS_LANG_D_ETS || 173 !this.tsUtils.isSendableClassOrInterfaceEntity(element.name) 174 ) { 175 this.incrementCounters(element, FaultID.NoTsImportEts); 176 } 177 } 178 } 179 180 private checkImportClause(node: ts.ImportClause, resolvedModule: ts.ResolvedModuleFull): void { 181 const checkAndIncrement = (identifier: ts.Identifier | undefined): void => { 182 if (identifier && !this.tsUtils.isSendableClassOrInterfaceEntity(identifier)) { 183 this.incrementCounters(identifier, FaultID.NoTsImportEts); 184 } 185 }; 186 if (node.name) { 187 if (this.allowInSdkImportSendable(resolvedModule)) { 188 return; 189 } 190 checkAndIncrement(node.name); 191 } 192 if (!node.namedBindings) { 193 return; 194 } 195 if (ts.isNamespaceImport(node.namedBindings)) { 196 this.incrementCounters(node.namedBindings, FaultID.NoNameSpaceImportEtsToTs); 197 } else if (ts.isNamedImports(node.namedBindings)) { 198 node.namedBindings.elements.forEach((element: ts.ImportSpecifier) => { 199 checkAndIncrement(element.name); 200 }); 201 } 202 } 203 204 private allowInSdkImportSendable(resolvedModule: ts.ResolvedModuleFull): boolean { 205 const resolvedModuleIsInSdk = InteropTypescriptLinter.sdkPath ? 206 path.normalize(resolvedModule.resolvedFileName).startsWith(InteropTypescriptLinter.sdkPath) : 207 false; 208 return ( 209 !!this.isInSdk && 210 resolvedModuleIsInSdk && 211 path.basename(resolvedModule.resolvedFileName).indexOf('sendable') !== -1 212 ); 213 } 214 215 private handleClassDeclaration(node: ts.Node): void { 216 const tsClassDecl = node as ts.ClassDeclaration; 217 if (!tsClassDecl.heritageClauses) { 218 return; 219 } 220 221 for (const hClause of tsClassDecl.heritageClauses) { 222 if (hClause) { 223 this.checkClassOrInterfaceDeclarationHeritageClause(hClause); 224 } 225 } 226 } 227 228 // In ts files, sendable classes and sendable interfaces can not be extended or implemented. 229 private checkClassOrInterfaceDeclarationHeritageClause(hClause: ts.HeritageClause): void { 230 for (const tsTypeExpr of hClause.types) { 231 232 /* 233 * Always resolve type from 'tsTypeExpr' node, not from 'tsTypeExpr.expression' node, 234 * as for the latter, type checker will return incorrect type result for classes in 235 * 'extends' clause. Additionally, reduce reference, as mostly type checker returns 236 * the TypeReference type objects for classes and interfaces. 237 */ 238 const tsExprType = TsUtils.reduceReference(this.tsTypeChecker.getTypeAtLocation(tsTypeExpr)); 239 const isSendableBaseType = this.tsUtils.isSendableClassOrInterface(tsExprType); 240 if (isSendableBaseType) { 241 this.incrementCounters(tsTypeExpr, FaultID.SendableTypeInheritance); 242 } 243 } 244 } 245 246 private handleInterfaceDeclaration(node: ts.Node): void { 247 const interfaceNode = node as ts.InterfaceDeclaration; 248 const iSymbol = this.tsUtils.trueSymbolAtLocation(interfaceNode.name); 249 const iDecls = iSymbol ? iSymbol.getDeclarations() : null; 250 if (!iDecls) { 251 return; 252 } 253 254 if (!interfaceNode.heritageClauses) { 255 return; 256 } 257 258 for (const hClause of interfaceNode.heritageClauses) { 259 if (hClause) { 260 this.checkClassOrInterfaceDeclarationHeritageClause(hClause); 261 } 262 } 263 } 264 265 private handleNewExpression(node: ts.Node): void { 266 const tsNewExpr = node as ts.NewExpression; 267 this.handleSendableGenericTypes(tsNewExpr); 268 } 269 270 private handleSendableGenericTypes(node: ts.NewExpression): void { 271 const type = this.tsTypeChecker.getTypeAtLocation(node); 272 if (!this.tsUtils.isSendableClassOrInterface(type)) { 273 return; 274 } 275 276 const typeArgs = node.typeArguments; 277 if (!typeArgs || typeArgs.length === 0) { 278 return; 279 } 280 281 for (const arg of typeArgs) { 282 if (!this.tsUtils.isSendableTypeNode(arg)) { 283 this.incrementCounters(arg, FaultID.SendableGenericTypes); 284 } 285 } 286 } 287 288 private handleObjectLiteralExpression(node: ts.Node): void { 289 const objectLiteralExpr = node as ts.ObjectLiteralExpression; 290 const objectLiteralType = this.tsTypeChecker.getContextualType(objectLiteralExpr); 291 if (objectLiteralType && this.tsUtils.typeContainsSendableClassOrInterface(objectLiteralType)) { 292 this.incrementCounters(node, FaultID.SendableObjectInitialization); 293 } 294 } 295 296 private handleArrayLiteralExpression(node: ts.Node): void { 297 const arrayLitNode = node as ts.ArrayLiteralExpression; 298 const arrayLitType = this.tsTypeChecker.getContextualType(arrayLitNode); 299 if (arrayLitType && this.tsUtils.typeContainsSendableClassOrInterface(arrayLitType)) { 300 this.incrementCounters(node, FaultID.SendableObjectInitialization); 301 } 302 } 303 304 private handleAsExpression(node: ts.Node): void { 305 const tsAsExpr = node as ts.AsExpression; 306 const targetType = this.tsTypeChecker.getTypeAtLocation(tsAsExpr.type).getNonNullableType(); 307 const exprType = this.tsTypeChecker.getTypeAtLocation(tsAsExpr.expression).getNonNullableType(); 308 309 if ( 310 !this.tsUtils.isSendableClassOrInterface(exprType) && 311 !this.tsUtils.isObject(exprType) && 312 !TsUtils.isAnyType(exprType) && 313 this.tsUtils.isSendableClassOrInterface(targetType) 314 ) { 315 this.incrementCounters(tsAsExpr, FaultID.SendableAsExpr); 316 } 317 } 318 319 private handleExportDeclaration(node: ts.Node): void { 320 const exportDecl = node as ts.ExportDeclaration; 321 const currentSourceFile = exportDecl.getSourceFile(); 322 const contextSpecifier = exportDecl.moduleSpecifier; 323 324 if (contextSpecifier && ts.isStringLiteralLike(contextSpecifier)) { 325 const resolvedModule = this.getResolveModule(contextSpecifier.text, currentSourceFile.fileName); 326 327 if (!resolvedModule) { 328 return; 329 } 330 331 if (this.isKitModule(resolvedModule.resolvedFileName, exportDecl)) { 332 return; 333 } 334 335 if (InteropTypescriptLinter.isEtsFile(resolvedModule.extension)) { 336 this.incrementCounters(contextSpecifier, FaultID.NoTsReExportEts); 337 } 338 return; 339 } 340 341 if (!this.isInSdk) { 342 return; 343 } 344 345 this.handleSdkExport(exportDecl); 346 } 347 348 private isKitModule(resolvedFileName: string, exportDecl: ts.ExportDeclaration): boolean { 349 const baseFileName = path.basename(resolvedFileName); 350 if (baseFileName.startsWith(KIT) && baseFileName.endsWith(D_TS)) { 351 if (!InteropTypescriptLinter.etsLoaderPath) { 352 return true; 353 } 354 InteropTypescriptLinter.initKitInfos(baseFileName); 355 const exportClause = exportDecl.exportClause; 356 357 if (exportClause && ts.isNamedExports(exportClause)) { 358 this.checkKitImportClause(exportClause, baseFileName); 359 } 360 return true; 361 } 362 return false; 363 } 364 365 private static isEtsFile(extension: string | undefined): boolean { 366 return extension === ETS || extension === D_ETS; 367 } 368 369 private handleSdkExport(exportDecl: ts.ExportDeclaration): void { 370 if (!exportDecl.exportClause || !ts.isNamedExports(exportDecl.exportClause)) { 371 return; 372 } 373 374 for (const exportSpecifier of exportDecl.exportClause.elements) { 375 if (this.tsUtils.isSendableClassOrInterfaceEntity(exportSpecifier.name)) { 376 this.incrementCounters(exportSpecifier.name, FaultID.SendableTypeExported); 377 } 378 } 379 } 380 381 private handleExportAssignment(node: ts.Node): void { 382 if (!this.isInSdk) { 383 return; 384 } 385 386 // In sdk .d.ts files, sendable classes and sendable interfaces can not be "default" exported. 387 const exportAssignment = node as ts.ExportAssignment; 388 389 if (this.tsUtils.isSendableClassOrInterfaceEntity(exportAssignment.expression)) { 390 this.incrementCounters(exportAssignment.expression, FaultID.SendableTypeExported); 391 } 392 } 393 394 private static initKitInfos(fileName: string): void { 395 if (InteropTypescriptLinter.kitInfos.has(fileName)) { 396 return; 397 } 398 399 const JSON_SUFFIX = '.json'; 400 const KIT_CONFIGS = '../ets-loader/kit_configs'; 401 const KIT_CONFIG_PATH = './build-tools/ets-loader/kit_configs'; 402 403 const kitConfigs: string[] = [path.resolve(InteropTypescriptLinter.etsLoaderPath as string, KIT_CONFIGS)]; 404 if (process.env.externalApiPaths) { 405 const externalApiPaths = process.env.externalApiPaths.split(path.delimiter); 406 externalApiPaths.forEach((sdkPath) => { 407 kitConfigs.push(path.resolve(sdkPath, KIT_CONFIG_PATH)); 408 }); 409 } 410 411 for (const kitConfig of kitConfigs) { 412 const kitModuleConfigJson = path.resolve(kitConfig, './' + fileName.replace(D_TS, JSON_SUFFIX)); 413 if (fs.existsSync(kitModuleConfigJson)) { 414 InteropTypescriptLinter.kitInfos.set(fileName, JSON.parse(fs.readFileSync(kitModuleConfigJson, 'utf-8'))); 415 } 416 } 417 } 418 419 private static getKitModuleFileNames( 420 fileName: string, 421 node: ts.NamedImports | ts.NamedExports, 422 index: number 423 ): string { 424 if (!InteropTypescriptLinter.kitInfos.has(fileName)) { 425 return ''; 426 } 427 428 const kitInfo = InteropTypescriptLinter.kitInfos.get(fileName); 429 if (!kitInfo?.symbols) { 430 return ''; 431 } 432 433 const element = node.elements[index]; 434 return element.propertyName ? 435 kitInfo.symbols[element.propertyName.text].source : 436 kitInfo.symbols[element.name.text].source; 437 } 438 439 lint(): void { 440 this.isInSdk = InteropTypescriptLinter.sdkPath ? 441 path.normalize(this.sourceFile.fileName).indexOf(InteropTypescriptLinter.sdkPath) === 0 : 442 false; 443 this.visitSourceFile(this.sourceFile); 444 } 445} 446