• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2022 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 ts from 'typescript';
17
18import { LogInfo, LogType } from './utils';
19import { IMPORT_FILE_ASTCACHE, generateSourceFileAST, getFileFullPath } from './process_import'
20
21const FILE_TYPE_EXPORT_NAMES: Map<string, Set<string>> = new Map();
22
23interface ImportName {
24  name: string,
25  node: ts.Node,
26  source: string
27}
28
29function collectNonTypeMarkedReExportName(node: ts.SourceFile, pagesDir: string): Map<string, Map<string, ts.Node>> {
30 /* those cases need be validated
31  * case 1: re-export
32  *   export { externalName as localName } from './xxx'
33  *
34  * case 2: indirect re-export nameBindings
35  *   import { externalName as localName } from './xxx'
36  *   export [type] { localName as re-exportName }
37  *
38  * case 3: indirect re-export default
39  *   import defaultLocalName from './xxx'
40  *   export [type] { defaultLocalName as re-exportName }
41  */
42  const RE_EXPORT_NAME: Map<string, Map<string, ts.Node>> = new Map();
43  const IMPORT_AS: Map<string, ImportName> = new Map();
44  const EXPORT_LOCAL: Set<string> = new Set();
45
46  node.statements.forEach(stmt => {
47    if (ts.isImportDeclaration(stmt) && stmt.importClause && !stmt.importClause.isTypeOnly) {
48      let fileFullPath: string = getFileFullPath(stmt.moduleSpecifier.getText().replace(/'|"/g, ''), pagesDir);
49      if (fileFullPath.endsWith('.ets') || fileFullPath.endsWith('.ts')) {
50        const importClause: ts.ImportClause = stmt.importClause;
51        if (importClause.name) {
52          let localName: string = importClause.name.escapedText.toString();
53          let importName: ImportName = {name: 'default', node: stmt, source: fileFullPath};
54          IMPORT_AS.set(localName, importName);
55        }
56        if (importClause.namedBindings && ts.isNamedImports(importClause.namedBindings)) {
57          importClause.namedBindings.elements.forEach(elem => {
58            let localName: string = elem.name.escapedText.toString();
59            let importName: string = elem.propertyName ? elem.propertyName.escapedText.toString() : localName;
60            IMPORT_AS.set(localName, <ImportName>{name: importName, node: stmt, source: fileFullPath})
61          });
62        }
63      }
64    }
65
66    if (ts.isExportDeclaration(stmt)) {
67      // TD: Check `export * from ...` when tsc supports `export type * from ...`.
68      if (stmt.moduleSpecifier && !stmt.isTypeOnly && stmt.exportClause && ts.isNamedExports(stmt.exportClause)) {
69        let fileFullPath: string = getFileFullPath(stmt.moduleSpecifier.getText().replace(/'|"/g, ''), pagesDir);
70        if (fileFullPath.endsWith('.ets') || fileFullPath.endsWith('.ts')) {
71          stmt.exportClause.elements.forEach(elem => {
72            let importName: string = elem.propertyName ? elem.propertyName.escapedText.toString() :
73                                      elem.name.escapedText.toString();
74            if (RE_EXPORT_NAME.has(fileFullPath)) {
75              RE_EXPORT_NAME.get(fileFullPath).set(importName, stmt);
76            } else {
77              RE_EXPORT_NAME.set(fileFullPath, (new Map<string, ts.Node>()).set(importName, stmt));
78            }
79          });
80        }
81      }
82      if (!stmt.moduleSpecifier && stmt.exportClause && ts.isNamedExports(stmt.exportClause)) {
83        stmt.exportClause.elements.forEach(elem => {
84          let localName: string = elem.propertyName ? elem.propertyName.escapedText.toString() :
85                                  elem.name.escapedText.toString();
86          EXPORT_LOCAL.add(localName);
87        });
88      }
89    }
90  });
91
92  EXPORT_LOCAL.forEach(local => {
93    if (IMPORT_AS.has(local)) {
94      let importName: ImportName = IMPORT_AS.get(local);
95      if (RE_EXPORT_NAME.has(importName.source)) {
96        RE_EXPORT_NAME.get(importName.source).set(importName.name, importName.node);
97      } else {
98        RE_EXPORT_NAME.set(importName.source, (new Map<string, ts.Node>()).set(importName.name, importName.node));
99      }
100    }
101  });
102
103  return RE_EXPORT_NAME;
104}
105
106function processTypeImportDecl(node: ts.ImportDeclaration, localTypeNames: Set<string>): void {
107  if (node.importClause && node.importClause.isTypeOnly) {
108    // import type T from ...
109    if (node.importClause.name) {
110      localTypeNames.add(node.importClause.name.escapedText.toString());
111    }
112    // import type * as T from ...
113    if (node.importClause.namedBindings && ts.isNamespaceImport(node.importClause.namedBindings)) {
114      localTypeNames.add(node.importClause.namedBindings.name.escapedText.toString());
115    }
116    // import type { e_T as T } from ...
117    if (node.importClause.namedBindings && ts.isNamedImports(node.importClause.namedBindings)) {
118      node.importClause.namedBindings.elements.forEach((elem: any) => {
119        localTypeNames.add(elem.name.escapedText.toString());
120      });
121    }
122  }
123}
124
125function processExportDecl(node: ts.ExportDeclaration, typeExportNames: Set<string>,
126                           exportAs: Map<string, string>): void {
127  if (node.isTypeOnly) {
128    if (node.moduleSpecifier && node.exportClause) {
129      // export type * as T from ...
130      if (ts.isNamespaceExport(node.exportClause)) {
131        typeExportNames.add(node.exportClause.name.escapedText.toString());
132      }
133      // export type { e_T as T } from ...
134      if (ts.isNamedExports(node.exportClause)) {
135        node.exportClause.elements.forEach((elem: any) => {
136          typeExportNames.add(elem.name.escapedText.toString());
137        })
138      }
139    }
140    // export type { e_T as T }
141    if (!node.moduleSpecifier && node.exportClause && ts.isNamedExports(node.exportClause)) {
142      node.exportClause.elements.forEach((elem: any) => {
143        typeExportNames.add(elem.name.escapedText.toString());
144      });
145    }
146  } else {
147    // export { e_T as T }
148    if (!node.moduleSpecifier && node.exportClause && ts.isNamedExports(node.exportClause)) {
149      node.exportClause.elements.forEach((elem: any) => {
150        let exportName: string = elem.name.escapedText.toString();
151        let localName: string = elem.propertyName ? elem.propertyName.escapedText.toString() : exportName;
152        exportAs.set(localName, exportName);
153      });
154    }
155  }
156}
157
158function processInterfaceAndTypeAlias(node: ts.InterfaceDeclaration | ts.TypeAliasDeclaration,
159                                      localTypeNames: Set<string>, typeExportNames: Set<string>): void {
160  let hasDefault: boolean = false, hasExport: boolean = false;
161  node.modifiers && node.modifiers.forEach(m => {
162    if (m.kind == ts.SyntaxKind.DefaultKeyword) {
163      hasDefault = true;
164    }
165    if (m.kind == ts.SyntaxKind.ExportKeyword) {
166      hasExport = true;
167    }
168  });
169  localTypeNames.add(node.name.escapedText.toString());
170
171  if (hasExport) {
172    let exportName = hasDefault ? 'default' : node.name.escapedText.toString();
173    typeExportNames.add(exportName);
174  }
175}
176
177function checkTypeModuleDeclIsType(node: ts.ModuleDeclaration): boolean {
178  if (ts.isIdentifier(node.name) && node.body && ts.isModuleBlock(node.body)) {
179    for (let idx = 0; idx < node.body.statements.length; idx++) {
180      let stmt: ts.Statement = node.body.statements[idx];
181      if (ts.isModuleDeclaration(stmt) && !checkTypeModuleDeclIsType(<ts.ModuleDeclaration>stmt)) {
182        return false;
183      } else if (ts.isImportEqualsDeclaration(stmt)) {
184        let hasExport: boolean = false;
185        stmt.modifiers && stmt.modifiers.forEach(m => {
186          if (m.kind == ts.SyntaxKind.ExportKeyword) {
187            hasExport = true;
188          }
189        });
190        if (hasExport) {
191          return false;
192        }
193      } else if (!ts.isInterfaceDeclaration(stmt) && !ts.isTypeAliasDeclaration(stmt)) {
194        return false;
195      }
196    }
197  }
198  return true;
199}
200
201function processNamespace(node: ts.ModuleDeclaration, localTypeNames: Set<string>, typeExportNames: Set<string>): void {
202  if (ts.isIdentifier(node.name) && node.body && ts.isModuleBlock(node.body)) {
203    if (!checkTypeModuleDeclIsType(<ts.ModuleDeclaration>node)) {
204      return;
205    }
206
207    let hasExport: boolean = false;
208    node.modifiers && node.modifiers.forEach(m => {
209      if (m.kind == ts.SyntaxKind.ExportKeyword) {
210        hasExport = true;
211      }
212    });
213    if (hasExport) {
214      typeExportNames.add(node.name.escapedText.toString());
215    }
216    localTypeNames.add(node.name.escapedText.toString());
217  }
218}
219
220function addErrorLogIfReExportType(sourceFile: ts.SourceFile, log: LogInfo[], typeExportNames: Set<string>,
221                                   exportNames: Map<string, ts.Node>): void {
222  let reExportNamesArray: Array<string> = Array.from(exportNames.keys());
223  let typeExportNamesArray: Array<string> = Array.from(typeExportNames);
224  const needWarningNames: Array<string> = reExportNamesArray.filter(name => typeExportNamesArray.includes(name));
225  needWarningNames.forEach(name => {
226    const moduleNode: ts.Node = exportNames.get(name)!;
227    let typeIdentifier: string = name;
228    if (name === 'default' && ts.isImportDeclaration(moduleNode) && moduleNode.importClause) {
229      typeIdentifier = moduleNode.importClause.name!.escapedText.toString();
230    }
231    const posOfNode: ts.LineAndCharacter = sourceFile.getLineAndCharacterOfPosition(moduleNode.getStart());
232    let warningMessage: string = `The re-export name '${typeIdentifier}' need to be marked as type, `;
233    warningMessage += ts.isImportDeclaration(moduleNode) ? "please use 'import type'." : "please use 'export type'.";
234    const warning: LogInfo = {
235      type: LogType.WARN,
236      message: warningMessage,
237      pos: moduleNode.getStart(),
238      fileName: sourceFile.fileName,
239      line: posOfNode.line + 1,
240      column: posOfNode.character + 1
241    }
242    log.push(warning);
243  });
244}
245
246function collectTypeExportNames(source: string): Set<string> {
247  let importFileAst: ts.SourceFile;
248  if (IMPORT_FILE_ASTCACHE.has(source)) {
249    importFileAst = IMPORT_FILE_ASTCACHE.get(source);
250  } else {
251    importFileAst = generateSourceFileAST(source, source);
252    IMPORT_FILE_ASTCACHE[source] = importFileAst;
253  }
254  const EXPORT_AS: Map<string, string> = new Map();
255  const LOCAL_TYPE_NAMES: Set<string> = new Set();
256  const TYPE_EXPORT_NAMES: Set<string> = new Set();
257  importFileAst.statements.forEach(stmt => {
258    switch(stmt.kind) {
259      case ts.SyntaxKind.ImportDeclaration: {
260        processTypeImportDecl(<ts.ImportDeclaration>stmt, LOCAL_TYPE_NAMES);
261        break;
262      }
263      case ts.SyntaxKind.ExportDeclaration: {
264        processExportDecl(<ts.ExportDeclaration>stmt, TYPE_EXPORT_NAMES, EXPORT_AS);
265        break;
266      }
267      case ts.SyntaxKind.ExportAssignment: {
268        if (ts.isIdentifier((<ts.ExportAssignment>stmt).expression)) {
269          EXPORT_AS.set((<ts.Identifier>(<ts.ExportAssignment>stmt).expression).escapedText.toString(), "default");
270        }
271        break;
272      }
273      case ts.SyntaxKind.ModuleDeclaration: {
274        processNamespace(<ts.ModuleDeclaration>stmt, LOCAL_TYPE_NAMES, TYPE_EXPORT_NAMES);
275        break;
276      }
277      case ts.SyntaxKind.InterfaceDeclaration:
278      case ts.SyntaxKind.TypeAliasDeclaration: {
279        processInterfaceAndTypeAlias(<ts.InterfaceDeclaration|ts.TypeAliasDeclaration>stmt,
280                                      LOCAL_TYPE_NAMES, TYPE_EXPORT_NAMES);
281        break;
282      }
283      default:
284        break;
285    }
286  });
287  LOCAL_TYPE_NAMES.forEach(localName => {
288    if (EXPORT_AS.has(localName)) {
289      TYPE_EXPORT_NAMES.add(EXPORT_AS.get(localName));
290    }
291  });
292  FILE_TYPE_EXPORT_NAMES.set(source, TYPE_EXPORT_NAMES);
293  return TYPE_EXPORT_NAMES;
294}
295
296/*
297 * Validate re-export names from ets/ts file whether is a type by compiling with [TranspileOnly].
298 * Currently, there are three scenarios as following can not be validated correctly:
299 * case 1 export some specify type Identifier from one module's export * from ...:
300 * // A
301 * export { xx } from 'B'
302 * // B
303 * export * from 'C'
304 * // C
305 * export interface xx{}
306 * case 2 export some type Identifier from indirect .d.ts module:
307 * // A(ts)
308 * export { xx } from 'B'
309 * // B(.d.ts)
310 * export { xx } from 'C'
311 * // C(.d.ts)
312 * export interface xx {}
313 * case 3 export some type Identifier from '/// .d.ts'
314 * // A(ts)
315 * export { xx } from 'B'
316 * // B(.d.ts)
317 * ///C // extend B with C by using '///'
318 * // C(.d.ts)
319 * export interface xx {}
320 */
321export default function validateReExportType(node: ts.SourceFile, pagesDir: string, log: LogInfo[]): void {
322  /*
323   * those cases' name should be treat as Type
324   * case1:
325   *   import type {T} from ...
326   *   import type T from ...
327   *   import type * as T from ...
328   * case2:
329   *   export type {T} from ...
330   *   export type * as T from ...
331   * case3:
332   *   export interface T {}
333   *   export type T = {}
334   * case4:
335   *   export default interface {}
336   * case5:
337   *   interface T {}
338   *   export {T}
339   */
340  const RE_EXPORT_NAME: Map<string, Map<string, ts.Node>> = collectNonTypeMarkedReExportName(node, pagesDir);
341  RE_EXPORT_NAME.forEach((exportNames: Map<string, ts.Node>, source: string) => {
342    let typeExportNames: Set<string> = FILE_TYPE_EXPORT_NAMES.has(source) ?
343                                        FILE_TYPE_EXPORT_NAMES.get(source) : collectTypeExportNames(source);
344    addErrorLogIfReExportType(node, log, typeExportNames, exportNames);
345  });
346}
347