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