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