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