• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 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
16import * as ts from 'typescript';
17import fs from 'fs';
18import path from 'path';
19
20import {
21  FileLog,
22  LogType
23} from './utils';
24import { projectConfig } from '../main';
25import { ModuleSourceFile } from './fast_build/ark_compiler/module/module_source_file';
26import { collectKitModules } from './fast_build/system_api/rollup-plugin-system-api';
27
28/*
29* basic implementation logic:
30* tsc -> transformer
31*           | -> iterate top-level static import/export declaration
32*                  | -> for each declaration
33*                        | -> collect KitInfo
34*                        | -> generate corresponding ohosImports for each ohos-source
35*                  | -> replace each origin declaration with corresponding ohosImports
36*/
37
38export const kitTransformLog: FileLog = new FileLog();
39export let hasTsNoCheckOrTsIgnoreFiles: string[] = [];
40export let compilingEtsOrTsFiles: string[] = [];
41
42const KIT_PREFIX = '@kit.';
43
44/*
45* This API is the TSC Transformer for transforming `KitImport` into `OhosImport`
46* e.g.
47*    ```
48*      import { ability, ErrorCode } from '@kit.AbilityKit'
49*      --->
50*      import ability from '@ohos.ability.ability'
51*      import ErrorCode from '@ohos.ability.errorCode'
52*    ```
53*/
54export function processKitImport(): Function {
55  return (context: ts.TransformationContext) => {
56    const visitor: ts.Visitor = node => {
57      // only transform static import/export declaration
58      if (ts.isImportDeclaration(node) || (ts.isExportDeclaration(node) && node.moduleSpecifier)) {
59        const moduleRequest: string = (node.moduleSpecifier as ts.StringLiteral).text.replace(/'|"/g, '');
60        if (moduleRequest.startsWith(KIT_PREFIX)) {
61          const kitDefs = getKitDefs(moduleRequest);
62          if (kitDefs && kitDefs.symbols) {
63            KitInfo.processKitInfo(moduleRequest, kitDefs.symbols as KitSymbols, node);
64            return [...KitInfo.getCurrentKitInfo().getOhosImportNodes()];
65          } else {
66            kitTransformLog.errors.push({
67              type: LogType.ERROR,
68              message: `Kit '${moduleRequest}' has no corresponding config file in ArkTS SDK, `+
69                       `please check the consistency of SDK.`,
70              pos: node.getStart()
71            });
72          }
73        }
74      }
75      return node;
76    }
77
78    return (node: ts.SourceFile) => {
79      compilingEtsOrTsFiles.push(path.normalize(node.fileName));
80
81      KitInfo.init(node, context);
82
83      if (projectConfig.processTs === true) {
84        if (ts.hasTsNoCheckOrTsIgnoreFlag(node)) {
85          hasTsNoCheckOrTsIgnoreFiles.push(path.normalize(node.fileName));
86          // process KitImport transforming
87          return ts.visitEachChild(node, visitor, context); // this node used for [writeFile]
88        }
89        // process [ConstEnum] + [TypeExportImport] + [KitImport] transforming
90        const processedNode: ts.SourceFile =
91          ts.visitEachChild(ts.getTypeExportImportAndConstEnumTransformer(context)(node), visitor, context);
92        ModuleSourceFile.newSourceFile(path.normalize(processedNode.fileName), processedNode);
93        return node; // this node not used for [writeFile]
94      }
95      // process KitImport transforming
96      return ts.visitEachChild(node, visitor, context);
97    };
98  }
99}
100
101/*
102*  Main implementation of Transforming
103*/
104const DEFAULT_BINDINGS = 'default';
105
106enum FileType {
107  ETS,
108  TS
109}
110
111interface KitSymbol {
112  source: string
113  bindings: string
114}
115
116declare type KitSymbols = Record<string, KitSymbol>;
117declare type TSspecifier = ts.ImportSpecifier | ts.ExportSpecifier;
118declare type TSModuleDeclaration = ts.ImportDeclaration | ts.ExportDeclaration;
119
120/*
121* class SpecificerInfo represents the corresponding info of each imported identifier which coming from Kit
122*/
123class SpecificerInfo {
124  private localName: string;
125  private importName: string;
126  private symbol: KitSymbol;
127  private renamed: boolean;
128
129  private originElement: TSspecifier | undefined;
130
131  constructor(localName: string, importName: string, symbol: KitSymbol, originElement: TSspecifier | undefined) {
132    this.localName = localName;
133    this.importName = importName;
134    this.symbol = symbol;
135    this.originElement = originElement;
136    this.renamed = (this.localName !== this.symbol.bindings);
137
138    this.validateImportingETSDeclarationSymbol();
139  }
140
141  getSource(): string {
142    return this.symbol.source;
143  }
144
145  getLocalName(): string {
146    return this.localName;
147  }
148
149  isRenamed(): boolean {
150    return this.renamed;
151  }
152
153  getBindings(): string {
154    return this.symbol.bindings;
155  }
156
157  isDefaultBinding(): boolean {
158    return this.symbol.bindings === DEFAULT_BINDINGS;
159  }
160
161  validateImportingETSDeclarationSymbol() {
162    if (KitInfo.isTSFile() && /.d.ets$/.test(this.symbol.source)) {
163      kitTransformLog.errors.push({
164        type: LogType.ERROR,
165        message: `Identifier '${this.importName}' comes from '${this.symbol.source}' ` +
166                 `which can not be imported in .ts file.`,
167        pos: this.getOriginElementNode().getStart()
168      });
169    }
170  }
171
172  setOriginElementNode(originElement: TSspecifier) {
173    this.originElement = originElement;
174  }
175
176  getOriginElementNode(): TSspecifier {
177    return this.originElement;
178  }
179}
180
181class KitInfo {
182  private static currentKitInfo: KitInfo = undefined;
183  private static currentFileType: FileType = FileType.ETS;
184  private static currentKitName: string = '';
185  private static currentSourcefile: string = '';
186  private static needSkipType: boolean = true;
187  private static tsEmitResolver: any;
188
189  private symbols: KitSymbols;
190  private kitNode: TSModuleDeclaration;
191  private kitNodeModifier: readonly ts.Modifier[] | undefined;
192  private specifiers: Map<string, SpecificerInfo[]> = new Map<string, SpecificerInfo[]>();
193
194  private ohosImportNodes: TSModuleDeclaration[] = [];
195
196  constructor(kitNode: TSModuleDeclaration, symbols: Record<string, KitSymbol>) {
197    this.kitNode = kitNode;
198    this.symbols = symbols;
199
200    this.kitNodeModifier = ts.canHaveDecorators(this.kitNode) ? ts.getModifiers(this.kitNode) : undefined;
201  }
202
203  static init(node: ts.SourceFile, context: ts.TransformationContext): void {
204    // @ts-ignore
205    this.tsEmitResolver = context.getEmitResolver();
206    this.currentSourcefile = node.fileName;
207    if (/\.ts$/.test(node.fileName)) {
208      this.setFileType(FileType.TS);
209    } else {
210      this.setFileType(FileType.ETS);
211    }
212
213    if (projectConfig.processTs === true && !ts.hasTsNoCheckOrTsIgnoreFlag(node)) {
214      this.needSkipType = false;
215    } else {
216      this.needSkipType = true;
217    }
218
219    kitTransformLog.sourceFile = node;
220  }
221
222  static getCurrentKitName(): string {
223    return this.currentKitName;
224  }
225
226  static getCurrentKitInfo(): KitInfo {
227    return this.currentKitInfo;
228  }
229
230  static setFileType(fileType: FileType): void {
231    this.currentFileType = fileType;
232  }
233
234  static isTSFile(): boolean {
235    return this.currentFileType === FileType.TS;
236  }
237
238  static getCurrentSourcefile(): string {
239    return this.currentSourcefile;
240  }
241
242  static needSkipTypeSymbolOfNode(node: ts.Node): boolean {
243    if (!this.needSkipType) {
244      return false;
245    }
246
247    // need to skip type symbol
248    const resolver = this.tsEmitResolver;
249    let isTypeSymbol: boolean = false;
250    switch(node.kind) {
251      case ts.SyntaxKind.ImportDeclaration:
252      case ts.SyntaxKind.ImportClause: {
253        const importClause = ts.isImportClause(node) ? node : (node as ts.ImportDeclaration).importClause;
254        if (importClause) {
255          isTypeSymbol = importClause.isTypeOnly;
256          if (importClause.name &&
257            !resolver.isReferencedAliasDeclaration(importClause) && resolver.isReferenced(node)) {
258              isTypeSymbol = true;
259          }
260        }
261        break;
262      }
263      case ts.SyntaxKind.ImportSpecifier: {
264        isTypeSymbol = (node as ts.ImportSpecifier).isTypeOnly;
265        if (!resolver.isReferencedAliasDeclaration(node) && resolver.isReferenced(node)) {
266          isTypeSymbol = true;
267        }
268        break;
269      }
270      case ts.SyntaxKind.ExportDeclaration: {
271        isTypeSymbol = (node as ts.ExportDeclaration).isTypeOnly;
272        break;
273      }
274      case ts.SyntaxKind.ExportSpecifier: {
275        isTypeSymbol = (node as ts.ExportSpecifier).isTypeOnly;
276        if (!resolver.isValueAliasDeclaration(node)) {
277          isTypeSymbol = true;
278        }
279        break;
280      }
281    }
282    return isTypeSymbol;
283  }
284
285  static processImportDecl(kitNode: ts.ImportDeclaration, symbols: Record<string, KitSymbol>) {
286    if (kitNode.importClause!.namedBindings) {
287      const namedBindings: ts.NamedImportBindings = kitNode.importClause.namedBindings;
288      if (ts.isNamespaceImport(namedBindings)) {
289        // e.g. import * as ns from "@kit.xxx"
290        this.currentKitInfo = new NameSpaceKitInfo(kitNode, symbols);
291      }
292      if (ts.isNamedImports(namedBindings) && namedBindings.elements.length !== 0) {
293        // e.g. import { ... } from "@kit.xxx"
294        this.currentKitInfo = new ImportSpecifierKitInfo(kitNode, symbols);
295        namedBindings.elements.forEach(element => { this.currentKitInfo.collectSpecifier(element) });
296      }
297    }
298
299    if (kitNode.importClause!.name && !this.needSkipTypeSymbolOfNode(kitNode.importClause)) {
300      // e.g. import default from "@kit.xxx"
301      const defaultName: string = kitNode.importClause.name.text;
302      if (!this.currentKitInfo) {
303        this.currentKitInfo = new ImportSpecifierKitInfo(kitNode, symbols);
304      }
305      this.currentKitInfo.newSpecificerInfo(defaultName, DEFAULT_BINDINGS, undefined);
306    }
307  }
308
309  static processExportDecl(kitNode: ts.ExportDeclaration, symbols: Record<string, KitSymbol>) {
310    if (kitNode.exportClause) {
311      const namedExportBindings: ts.NamedExportBindings = kitNode.exportClause;
312      if (ts.isNamespaceExport(namedExportBindings)) {
313        // e.g. export * as ns from "@kit.xxx"
314        this.currentKitInfo = new NameSpaceKitInfo(kitNode, symbols);
315      } else if (ts.isNamedExports(namedExportBindings) && namedExportBindings.elements.length !== 0) {
316        // e.g. export { ... } from "@kit.xxx"
317        this.currentKitInfo = new ExportSpecifierKitInfo(kitNode, symbols);
318        namedExportBindings.elements.forEach(element => { this.currentKitInfo.collectSpecifier(element) });
319      }
320    } else {
321      this.currentKitInfo = new ExportStarKitInfo(kitNode, symbols);
322    }
323  }
324
325  static processKitInfo(kitName: string, symbols: Record<string, KitSymbol>, kitNode: TSModuleDeclaration): void {
326    this.currentKitName = kitName;
327
328    // do not handle an empty import
329    if (ts.isImportDeclaration(kitNode) && kitNode.importClause) {
330      // case 1: import { ... } from '@kit.xxx'
331      // case 2: import * as ns from '@kit.xxx'
332      // case 3: import defalutValue from '@kit.xxx'
333      this.processImportDecl(kitNode, symbols);
334    }
335
336    if (ts.isExportDeclaration(kitNode) && kitNode.moduleSpecifier) {
337      // case 1: export { ... } from '@kit.xxx'
338      // case 2: export * from '@kit.xxx'
339      // case 3: export * as ns from '@kit.xxx' - considering forbidden
340      this.processExportDecl(kitNode, symbols);
341    }
342    // transform into ohos imports or exports
343    this.currentKitInfo && this.currentKitInfo.transform();
344  }
345
346  getSymbols(): KitSymbols {
347    return this.symbols;
348  }
349
350  getKitNode(): TSModuleDeclaration {
351    return this.kitNode;
352  }
353
354  getKitNodeModifier(): readonly ts.Modifier[] | undefined {
355      return this.kitNodeModifier;
356  }
357
358  getSpecifiers(): Map<string, SpecificerInfo[]> {
359    return this.specifiers;
360  }
361
362  getOhosImportNodes(): TSModuleDeclaration[] {
363    return this.ohosImportNodes;
364  }
365
366  newSpecificerInfo(localName: string, importName: string, originElement: TSspecifier | undefined): void {
367    const symbol: KitSymbol | undefined = this.symbols[importName];
368    if (symbol) {
369      const specifier: SpecificerInfo = new SpecificerInfo(localName, importName, symbol, originElement);
370      if (this.specifiers.has(symbol.source)) {
371        this.specifiers.get(symbol.source).push(specifier);
372      } else {
373        this.specifiers.set(symbol.source, [specifier]);
374      }
375    } else {
376      kitTransformLog.errors.push({
377        type: LogType.ERROR,
378        message: `Kit '${KitInfo.getCurrentKitName()} has not exported the Identifier '${importName}'.`,
379        pos: originElement ? originElement.getStart() : this.getKitNode().getStart()
380      });
381    }
382  }
383
384  collectSpecifier(element: TSspecifier) {
385    if (KitInfo.needSkipTypeSymbolOfNode(this.getKitNode()) || KitInfo.needSkipTypeSymbolOfNode(element)) {
386      // skip type symbol
387      return;
388    }
389
390    const localName: string = element.name.text;
391    const importName: string = element.propertyName ? element.propertyName.text : localName;
392    this.newSpecificerInfo(localName, importName, element);
393  }
394
395  // @ts-ignore
396  transform(): void {} //override api
397}
398
399class NameSpaceKitInfo extends KitInfo {
400  private namespaceName: string;
401  private localNameTable: string[] = [];
402
403  constructor(kitNode: ts.ImportDeclaration | ts.ExportDeclaration, symbols: Record<string, KitSymbol>) {
404    super(kitNode, symbols);
405
406    kitTransformLog.errors.push({
407      type: LogType.ERROR,
408      message: `Namespace import or export of Kit is not supported currently.`,
409      pos: kitNode.getStart()
410    });
411  }
412
413  transform(): void {
414  }
415}
416
417class ImportSpecifierKitInfo extends KitInfo {
418  private namedBindings: ts.ImportSpecifier[] = [];
419  private specifierDefaultName: ts.Identifier | undefined = undefined;
420
421  constructor(kitNode: ts.ImportDeclaration, symbols: Record<string, KitSymbol>) {
422    super(kitNode, symbols);
423  }
424
425  private hasNamedBindings(): boolean {
426    return this.namedBindings.length !== 0;
427  }
428
429  private clearSpecifierKitInfo(): void {
430    this.namedBindings = [];
431    this.specifierDefaultName = undefined;
432  }
433
434  transform() {
435    const node: ts.ImportDeclaration = this.getKitNode() as ts.ImportDeclaration;
436
437    this.getSpecifiers().forEach((specifiers: SpecificerInfo[], source: string) => {
438      collectKitModules(KitInfo.getCurrentSourcefile(), KitInfo.getCurrentKitName(), source);
439      specifiers.forEach((specifier: SpecificerInfo) => {
440        if (specifier.isDefaultBinding()) {
441          this.specifierDefaultName = ts.factory.createIdentifier(specifier.getLocalName());
442        } else {
443          this.namedBindings.push(
444            ts.factory.createImportSpecifier(
445              specifier.getOriginElementNode() ?
446                (specifier.getOriginElementNode() as ts.ImportSpecifier).isTypeOnly : node.importClause.isTypeOnly,
447              specifier.isRenamed() ? ts.factory.createIdentifier(specifier.getBindings()) : undefined,
448              ts.factory.createIdentifier(specifier.getLocalName())
449            )
450          );
451        }
452      });
453
454      this.getOhosImportNodes().push(ts.factory.createImportDeclaration(
455        this.getKitNodeModifier(),
456        ts.factory.createImportClause(
457          node.importClause!.isTypeOnly,
458          this.specifierDefaultName,
459          this.hasNamedBindings() ? ts.factory.createNamedImports(this.namedBindings) : undefined
460        ),
461        ts.factory.createStringLiteral(trimSourceSuffix(source))
462      ));
463
464      this.clearSpecifierKitInfo();
465    });
466  }
467}
468
469class ExportSpecifierKitInfo extends KitInfo {
470  private namedBindings: ts.ExportSpecifier[] = [];
471
472  constructor(kitNode: ts.ExportDeclaration, symbols: Record<string, KitSymbol>) {
473    super(kitNode, symbols);
474  }
475
476  private hasNamedBindings(): boolean {
477    return this.namedBindings.length !== 0;
478  }
479
480  private clearSpecifierKitInfo(): void {
481    this.namedBindings = [];
482  }
483
484  transform(): void {
485    const node: ts.ExportDeclaration = this.getKitNode() as ts.ExportDeclaration;
486
487    this.getSpecifiers().forEach((specifiers: SpecificerInfo[], source: string) => {
488      specifiers.forEach((specifier: SpecificerInfo) => {
489        this.namedBindings.push(
490          ts.factory.createExportSpecifier(
491            (specifier.getOriginElementNode() as ts.ExportSpecifier).isTypeOnly,
492            specifier.isRenamed() ? ts.factory.createIdentifier(specifier.getBindings()) : undefined,
493            ts.factory.createIdentifier(specifier.getLocalName())
494          )
495        );
496      });
497
498      this.getOhosImportNodes().push(ts.factory.createExportDeclaration(
499        this.getKitNodeModifier(),
500        node.isTypeOnly,
501        this.hasNamedBindings() ? ts.factory.createNamedExports(this.namedBindings) : undefined,
502        ts.factory.createStringLiteral(trimSourceSuffix(source)),
503        node.assertClause
504      ));
505
506      this.clearSpecifierKitInfo();
507    });
508  }
509}
510
511class ExportStarKitInfo extends KitInfo {
512  private sourceSet: Set<string> = new Set<string>();
513
514  constructor(kitNode: ts.ExportDeclaration, symbols: Record<string, KitSymbol>) {
515    super(kitNode, symbols);
516
517    for (const symbol in symbols) {
518      this.sourceSet.add(symbols[symbol].source);
519    }
520
521    kitTransformLog.errors.push({
522      type: LogType.WARN,
523      message: `Using 'export *' will load all the sub-module of Kit in runtime.`,
524      pos: this.getKitNode().getStart()
525    });
526  }
527
528  transform(): void {
529    const node: ts.ExportDeclaration = this.getKitNode() as ts.ExportDeclaration;
530
531    this.sourceSet.forEach((source: string) => {
532      this.getOhosImportNodes().push(ts.factory.createExportDeclaration(
533        this.getKitNodeModifier(),
534        node.isTypeOnly,
535        undefined,
536        ts.factory.createStringLiteral(trimSourceSuffix(source)),
537        node.assertClause
538      ));
539    });
540  }
541}
542
543/*
544* utils part
545*/
546const JSON_SUFFIX = '.json';
547const KIT_CONFIGS = 'kit_configs';
548const KIT_CONFIG_PATH = './build-tools/ets-loader/kit_configs';
549
550function getKitDefs(kitModuleRequest: string) {
551  const kitConfigs: string[] = [path.resolve(__dirname, `../${KIT_CONFIGS}`)];
552  if (process.env.externalApiPaths) {
553    const externalApiPaths = process.env.externalApiPaths.split(path.delimiter);
554    externalApiPaths.forEach(sdkPath => {
555      kitConfigs.push(path.resolve(sdkPath, KIT_CONFIG_PATH));
556    });
557  }
558
559  for (const kitConfig of kitConfigs) {
560    const kitModuleConfigJson = path.resolve(kitConfig, `./${kitModuleRequest}${JSON_SUFFIX}`);
561    if (fs.existsSync(kitModuleConfigJson)) {
562      return JSON.parse(fs.readFileSync(kitModuleConfigJson, 'utf-8'));
563    }
564  }
565  return undefined;
566}
567
568function trimSourceSuffix(source: string): string {
569  return source.replace(/\.d.[e]?ts$/, '');
570}
571
572export function cleanUpKitImportObjects(): void {
573  kitTransformLog.cleanUp();
574}