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