• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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