1/* @internal */ 2namespace ts.codefix { 3 const fixId = "fixImportNonExportedMember"; 4 const errorCodes = [ 5 Diagnostics.Module_0_declares_1_locally_but_it_is_not_exported.code, 6 ]; 7 8 registerCodeFix({ 9 errorCodes, 10 fixIds: [fixId], 11 getCodeActions(context) { 12 const { sourceFile, span, program } = context; 13 const info = getInfo(sourceFile, span.start, program); 14 if (info === undefined) return undefined; 15 16 const changes = textChanges.ChangeTracker.with(context, t => doChange(t, program, info)); 17 return [createCodeFixAction(fixId, changes, [Diagnostics.Export_0_from_module_1, info.exportName.node.text, info.moduleSpecifier], fixId, Diagnostics.Export_all_referenced_locals)]; 18 }, 19 getAllCodeActions(context) { 20 const { program } = context; 21 return createCombinedCodeActions(textChanges.ChangeTracker.with(context, changes => { 22 const exports = new Map<SourceFile, ModuleExports>(); 23 24 eachDiagnostic(context, errorCodes, diag => { 25 const info = getInfo(diag.file, diag.start, program); 26 if (info === undefined) return undefined; 27 28 const { exportName, node, moduleSourceFile } = info; 29 if (tryGetExportDeclaration(moduleSourceFile, exportName.isTypeOnly) === undefined && canHaveExportModifier(node)) { 30 changes.insertExportModifier(moduleSourceFile, node); 31 } 32 else { 33 const moduleExports = exports.get(moduleSourceFile) || { typeOnlyExports: [], exports: [] }; 34 if (exportName.isTypeOnly) { 35 moduleExports.typeOnlyExports.push(exportName); 36 } 37 else { 38 moduleExports.exports.push(exportName); 39 } 40 exports.set(moduleSourceFile, moduleExports); 41 } 42 }); 43 44 exports.forEach((moduleExports, moduleSourceFile) => { 45 const exportDeclaration = tryGetExportDeclaration(moduleSourceFile, /*isTypeOnly*/ true); 46 if (exportDeclaration && exportDeclaration.isTypeOnly) { 47 doChanges(changes, program, moduleSourceFile, moduleExports.typeOnlyExports, exportDeclaration); 48 doChanges(changes, program, moduleSourceFile, moduleExports.exports, tryGetExportDeclaration(moduleSourceFile, /*isTypeOnly*/ false)); 49 } 50 else { 51 doChanges(changes, program, moduleSourceFile, [...moduleExports.exports, ...moduleExports.typeOnlyExports], exportDeclaration); 52 } 53 }); 54 })); 55 } 56 }); 57 58 interface ModuleExports { 59 typeOnlyExports: ExportName[]; 60 exports: ExportName[]; 61 } 62 63 interface ExportName { 64 node: Identifier; 65 isTypeOnly: boolean; 66 } 67 68 interface Info { 69 exportName: ExportName; 70 node: Declaration | VariableStatement; 71 moduleSourceFile: SourceFile; 72 moduleSpecifier: string; 73 } 74 75 function getInfo(sourceFile: SourceFile, pos: number, program: Program): Info | undefined { 76 const token = getTokenAtPosition(sourceFile, pos); 77 if (isIdentifier(token)) { 78 const importDeclaration = findAncestor(token, isImportDeclaration); 79 if (importDeclaration === undefined) return undefined; 80 81 const moduleSpecifier = isStringLiteral(importDeclaration.moduleSpecifier) ? importDeclaration.moduleSpecifier.text : undefined; 82 if (moduleSpecifier === undefined) return undefined; 83 84 const resolvedModule = getResolvedModule(sourceFile, moduleSpecifier, /*mode*/ undefined); 85 if (resolvedModule === undefined) return undefined; 86 87 const moduleSourceFile = program.getSourceFile(resolvedModule.resolvedFileName); 88 if (moduleSourceFile === undefined || isSourceFileFromLibrary(program, moduleSourceFile)) return undefined; 89 90 const moduleSymbol = moduleSourceFile.symbol; 91 const locals = moduleSymbol.valueDeclaration?.locals; 92 if (locals === undefined) return undefined; 93 94 const localSymbol = locals.get(token.escapedText); 95 if (localSymbol === undefined) return undefined; 96 97 const node = getNodeOfSymbol(localSymbol); 98 if (node === undefined) return undefined; 99 100 const exportName = { node: token, isTypeOnly: isTypeDeclaration(node) }; 101 return { exportName, node, moduleSourceFile, moduleSpecifier }; 102 } 103 return undefined; 104 } 105 106 function doChange(changes: textChanges.ChangeTracker, program: Program, { exportName, node, moduleSourceFile }: Info) { 107 const exportDeclaration = tryGetExportDeclaration(moduleSourceFile, exportName.isTypeOnly); 108 if (exportDeclaration) { 109 updateExport(changes, program, moduleSourceFile, exportDeclaration, [exportName]); 110 } 111 else if (canHaveExportModifier(node)) { 112 changes.insertExportModifier(moduleSourceFile, node); 113 } 114 else { 115 createExport(changes, program, moduleSourceFile, [exportName]); 116 } 117 } 118 119 function doChanges(changes: textChanges.ChangeTracker, program: Program, sourceFile: SourceFile, moduleExports: ExportName[], node: ExportDeclaration | undefined) { 120 if (length(moduleExports)) { 121 if (node) { 122 updateExport(changes, program, sourceFile, node, moduleExports); 123 } 124 else { 125 createExport(changes, program, sourceFile, moduleExports); 126 } 127 } 128 } 129 130 function tryGetExportDeclaration(sourceFile: SourceFile, isTypeOnly: boolean) { 131 const predicate = (node: Node): node is ExportDeclaration => 132 isExportDeclaration(node) && (isTypeOnly && node.isTypeOnly || !node.isTypeOnly); 133 return findLast(sourceFile.statements, predicate); 134 } 135 136 function updateExport(changes: textChanges.ChangeTracker, program: Program, sourceFile: SourceFile, node: ExportDeclaration, names: ExportName[]) { 137 const namedExports = node.exportClause && isNamedExports(node.exportClause) ? node.exportClause.elements : factory.createNodeArray([]); 138 const allowTypeModifier = !node.isTypeOnly && !!(program.getCompilerOptions().isolatedModules || find(namedExports, e => e.isTypeOnly)); 139 changes.replaceNode(sourceFile, node, 140 factory.updateExportDeclaration(node, node.modifiers, node.isTypeOnly, 141 factory.createNamedExports( 142 factory.createNodeArray([...namedExports, ...createExportSpecifiers(names, allowTypeModifier)], /*hasTrailingComma*/ namedExports.hasTrailingComma)), node.moduleSpecifier, node.assertClause)); 143 } 144 145 function createExport(changes: textChanges.ChangeTracker, program: Program, sourceFile: SourceFile, names: ExportName[]) { 146 changes.insertNodeAtEndOfScope(sourceFile, sourceFile, 147 factory.createExportDeclaration(/*modifiers*/ undefined, /*isTypeOnly*/ false, 148 factory.createNamedExports(createExportSpecifiers(names, /*allowTypeModifier*/ !!program.getCompilerOptions().isolatedModules)), /*moduleSpecifier*/ undefined, /*assertClause*/ undefined)); 149 } 150 151 function createExportSpecifiers(names: ExportName[], allowTypeModifier: boolean) { 152 return factory.createNodeArray(map(names, n => factory.createExportSpecifier(allowTypeModifier && n.isTypeOnly, /*propertyName*/ undefined, n.node))); 153 } 154 155 function getNodeOfSymbol(symbol: Symbol) { 156 if (symbol.valueDeclaration === undefined) { 157 return firstOrUndefined(symbol.declarations); 158 } 159 const declaration = symbol.valueDeclaration; 160 const variableStatement = isVariableDeclaration(declaration) ? tryCast(declaration.parent.parent, isVariableStatement) : undefined; 161 return variableStatement && length(variableStatement.declarationList.declarations) === 1 ? variableStatement : declaration; 162 } 163} 164