|
| 1 | +/* @internal */ |
| 2 | +namespace 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 | +} |
0 commit comments