Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(37440): Provide a quick-fix for non-exported types (#51038)
* feat(37440): add QF to handle missing exports * change diagnostic message * add type modifier only if isolatedModules is set or if the export declaration already uses type modifiers
- Loading branch information
1 parent
a24201c
commit 2bcfed0
Showing
27 changed files
with
725 additions
and
35 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
/* @internal */ | ||
namespace ts.codefix { | ||
const fixId = "fixImportNonExportedMember"; | ||
const errorCodes = [ | ||
Diagnostics.Module_0_declares_1_locally_but_it_is_not_exported.code, | ||
]; | ||
|
||
registerCodeFix({ | ||
errorCodes, | ||
fixIds: [fixId], | ||
getCodeActions(context) { | ||
const { sourceFile, span, program } = context; | ||
const info = getInfo(sourceFile, span.start, program); | ||
if (info === undefined) return undefined; | ||
|
||
const changes = textChanges.ChangeTracker.with(context, t => doChange(t, program, info)); | ||
return [createCodeFixAction(fixId, changes, [Diagnostics.Export_0_from_module_1, info.exportName.node.text, info.moduleSpecifier], fixId, Diagnostics.Export_all_referenced_locals)]; | ||
}, | ||
getAllCodeActions(context) { | ||
const { program } = context; | ||
return createCombinedCodeActions(textChanges.ChangeTracker.with(context, changes => { | ||
const exports = new Map<SourceFile, ModuleExports>(); | ||
|
||
eachDiagnostic(context, errorCodes, diag => { | ||
const info = getInfo(diag.file, diag.start, program); | ||
if (info === undefined) return undefined; | ||
|
||
const { exportName, node, moduleSourceFile } = info; | ||
if (tryGetExportDeclaration(moduleSourceFile, exportName.isTypeOnly) === undefined && canHaveExportModifier(node)) { | ||
changes.insertExportModifier(moduleSourceFile, node); | ||
} | ||
else { | ||
const moduleExports = exports.get(moduleSourceFile) || { typeOnlyExports: [], exports: [] }; | ||
if (exportName.isTypeOnly) { | ||
moduleExports.typeOnlyExports.push(exportName); | ||
} | ||
else { | ||
moduleExports.exports.push(exportName); | ||
} | ||
exports.set(moduleSourceFile, moduleExports); | ||
} | ||
}); | ||
|
||
exports.forEach((moduleExports, moduleSourceFile) => { | ||
const exportDeclaration = tryGetExportDeclaration(moduleSourceFile, /*isTypeOnly*/ true); | ||
if (exportDeclaration && exportDeclaration.isTypeOnly) { | ||
doChanges(changes, program, moduleSourceFile, moduleExports.typeOnlyExports, exportDeclaration); | ||
doChanges(changes, program, moduleSourceFile, moduleExports.exports, tryGetExportDeclaration(moduleSourceFile, /*isTypeOnly*/ false)); | ||
} | ||
else { | ||
doChanges(changes, program, moduleSourceFile, [...moduleExports.exports, ...moduleExports.typeOnlyExports], exportDeclaration); | ||
} | ||
}); | ||
})); | ||
} | ||
}); | ||
|
||
interface ModuleExports { | ||
typeOnlyExports: ExportName[]; | ||
exports: ExportName[]; | ||
} | ||
|
||
interface ExportName { | ||
node: Identifier; | ||
isTypeOnly: boolean; | ||
} | ||
|
||
interface Info { | ||
exportName: ExportName; | ||
node: Declaration | VariableStatement; | ||
moduleSourceFile: SourceFile; | ||
moduleSpecifier: string; | ||
} | ||
|
||
function getInfo(sourceFile: SourceFile, pos: number, program: Program): Info | undefined { | ||
const token = getTokenAtPosition(sourceFile, pos); | ||
if (isIdentifier(token)) { | ||
const importDeclaration = findAncestor(token, isImportDeclaration); | ||
if (importDeclaration === undefined) return undefined; | ||
|
||
const moduleSpecifier = isStringLiteral(importDeclaration.moduleSpecifier) ? importDeclaration.moduleSpecifier.text : undefined; | ||
if (moduleSpecifier === undefined) return undefined; | ||
|
||
const resolvedModule = getResolvedModule(sourceFile, moduleSpecifier, /*mode*/ undefined); | ||
if (resolvedModule === undefined) return undefined; | ||
|
||
const moduleSourceFile = program.getSourceFile(resolvedModule.resolvedFileName); | ||
if (moduleSourceFile === undefined || isSourceFileFromLibrary(program, moduleSourceFile)) return undefined; | ||
|
||
const moduleSymbol = moduleSourceFile.symbol; | ||
const locals = moduleSymbol.valueDeclaration?.locals; | ||
if (locals === undefined) return undefined; | ||
|
||
const localSymbol = locals.get(token.escapedText); | ||
if (localSymbol === undefined) return undefined; | ||
|
||
const node = getNodeOfSymbol(localSymbol); | ||
if (node === undefined) return undefined; | ||
|
||
const exportName = { node: token, isTypeOnly: isTypeDeclaration(node) }; | ||
return { exportName, node, moduleSourceFile, moduleSpecifier }; | ||
} | ||
return undefined; | ||
} | ||
|
||
function doChange(changes: textChanges.ChangeTracker, program: Program, { exportName, node, moduleSourceFile }: Info) { | ||
const exportDeclaration = tryGetExportDeclaration(moduleSourceFile, exportName.isTypeOnly); | ||
if (exportDeclaration) { | ||
updateExport(changes, program, moduleSourceFile, exportDeclaration, [exportName]); | ||
} | ||
else if (canHaveExportModifier(node)) { | ||
changes.insertExportModifier(moduleSourceFile, node); | ||
} | ||
else { | ||
createExport(changes, program, moduleSourceFile, [exportName]); | ||
} | ||
} | ||
|
||
function doChanges(changes: textChanges.ChangeTracker, program: Program, sourceFile: SourceFile, moduleExports: ExportName[], node: ExportDeclaration | undefined) { | ||
if (length(moduleExports)) { | ||
if (node) { | ||
updateExport(changes, program, sourceFile, node, moduleExports); | ||
} | ||
else { | ||
createExport(changes, program, sourceFile, moduleExports); | ||
} | ||
} | ||
} | ||
|
||
function tryGetExportDeclaration(sourceFile: SourceFile, isTypeOnly: boolean) { | ||
const predicate = (node: Node): node is ExportDeclaration => | ||
isExportDeclaration(node) && (isTypeOnly && node.isTypeOnly || !node.isTypeOnly); | ||
return findLast(sourceFile.statements, predicate); | ||
} | ||
|
||
function updateExport(changes: textChanges.ChangeTracker, program: Program, sourceFile: SourceFile, node: ExportDeclaration, names: ExportName[]) { | ||
const namedExports = node.exportClause && isNamedExports(node.exportClause) ? node.exportClause.elements : factory.createNodeArray([]); | ||
const allowTypeModifier = !node.isTypeOnly && !!(program.getCompilerOptions().isolatedModules || find(namedExports, e => e.isTypeOnly)); | ||
changes.replaceNode(sourceFile, node, | ||
factory.updateExportDeclaration(node, node.modifiers, node.isTypeOnly, | ||
factory.createNamedExports( | ||
factory.createNodeArray([...namedExports, ...createExportSpecifiers(names, allowTypeModifier)], /*hasTrailingComma*/ namedExports.hasTrailingComma)), node.moduleSpecifier, node.assertClause)); | ||
} | ||
|
||
function createExport(changes: textChanges.ChangeTracker, program: Program, sourceFile: SourceFile, names: ExportName[]) { | ||
changes.insertNodeAtEndOfScope(sourceFile, sourceFile, | ||
factory.createExportDeclaration(/*modifiers*/ undefined, /*isTypeOnly*/ false, | ||
factory.createNamedExports(createExportSpecifiers(names, /*allowTypeModifier*/ !!program.getCompilerOptions().isolatedModules)), /*moduleSpecifier*/ undefined, /*assertClause*/ undefined)); | ||
} | ||
|
||
function createExportSpecifiers(names: ExportName[], allowTypeModifier: boolean) { | ||
return factory.createNodeArray(map(names, n => factory.createExportSpecifier(allowTypeModifier && n.isTypeOnly, /*propertyName*/ undefined, n.node))); | ||
} | ||
|
||
function getNodeOfSymbol(symbol: Symbol) { | ||
if (symbol.valueDeclaration === undefined) { | ||
return firstOrUndefined(symbol.declarations); | ||
} | ||
const declaration = symbol.valueDeclaration; | ||
const variableStatement = isVariableDeclaration(declaration) ? tryCast(declaration.parent.parent, isVariableStatement) : undefined; | ||
return variableStatement && length(variableStatement.declarationList.declarations) === 1 ? variableStatement : declaration; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
/// <reference path="fourslash.ts" /> | ||
|
||
// @module: esnext | ||
// @filename: /a.ts | ||
////declare function foo(): any | ||
////declare function bar(): any; | ||
////export { foo }; | ||
|
||
// @filename: /b.ts | ||
////import { bar } from "./a"; | ||
|
||
goTo.file("/b.ts"); | ||
verify.codeFix({ | ||
description: [ts.Diagnostics.Export_0_from_module_1.message, "bar", "./a"], | ||
index: 0, | ||
newFileContent: { | ||
"/a.ts": | ||
`declare function foo(): any | ||
declare function bar(): any; | ||
export { foo, bar };`, | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
/// <reference path="fourslash.ts" /> | ||
|
||
// @module: esnext | ||
// @filename: /a.ts | ||
/////** | ||
//// * foo | ||
//// */ | ||
////function foo() {} | ||
////export const bar = 1; | ||
|
||
// @filename: /b.ts | ||
////import { foo } from "./a"; | ||
|
||
goTo.file("/b.ts"); | ||
verify.codeFix({ | ||
description: [ts.Diagnostics.Export_0_from_module_1.message, "foo", "./a"], | ||
index: 0, | ||
newFileContent: { | ||
"/a.ts": | ||
`/** | ||
* foo | ||
*/ | ||
export function foo() {} | ||
export const bar = 1;`, | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
/// <reference path="fourslash.ts" /> | ||
|
||
// @module: esnext | ||
// @isolatedModules: true | ||
// @filename: /a.ts | ||
////type T = {}; | ||
////export {}; | ||
|
||
// @filename: /b.ts | ||
////import { T } from "./a"; | ||
|
||
goTo.file("/b.ts"); | ||
verify.codeFix({ | ||
description: [ts.Diagnostics.Export_0_from_module_1.message, "T", "./a"], | ||
index: 0, | ||
newFileContent: { | ||
"/a.ts": | ||
`type T = {}; | ||
export { type T };`, | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
/// <reference path="fourslash.ts" /> | ||
|
||
// @module: esnext | ||
// @filename: /a.ts | ||
////type T1 = {}; | ||
////type T2 = {}; | ||
////export { type T1 }; | ||
|
||
// @filename: /b.ts | ||
////import { T2 } from "./a"; | ||
|
||
goTo.file("/b.ts"); | ||
verify.codeFix({ | ||
description: [ts.Diagnostics.Export_0_from_module_1.message, "T2", "./a"], | ||
index: 0, | ||
newFileContent: { | ||
"/a.ts": | ||
`type T1 = {}; | ||
type T2 = {}; | ||
export { type T1, type T2 };`, | ||
} | ||
}); |
Oops, something went wrong.