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