-
Notifications
You must be signed in to change notification settings - Fork 12.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fixing namespace import debug failure #59004
Conversation
…rrorForNamespaceImport
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the identifier for the import did not have a parent
This isn’t quite right—the symbol the identifier for the import resolved to doesn’t have a parent, because the symbol is the module symbol, not an export of a module. That’s what namespace imports are—imports that bind an identifier to an entire module namespace record. So while your draft implementation looks pretty good, there are two problems: (1) addImportFromExportedSymbol
is not the right function to contain it, since the symbol being passed in is not an export, and (2) there are other ways an import/require can bind to a module symbol that are still broken. For example, I just changed in pasteEdits_namespaceImport.ts
- import * as test from "./a";
+ import test = require("./a");
and replicated the same crash. The same thing can also happen with a JavaScript require, as well as with a default import under certain conditions.1 The detection you need to determine if the symbol you have is for a module symbol is the isExternalModuleSymbol
function instead of relying on looking at the syntax of referenceImport
. However, I think you should maybe move your new code into a new ImportAdder function instead of expanding addImportForExportedSymbol
.
Footnotes
-
↩
Test case
/// <reference path="../fourslash.ts" /> // @Filename: /folder/c.ts //// [||] // @Filename: /a.ts //// const abc = 10; //// const def = 20; //// export interface testInterface { //// abc: number; //// def: number; //// } // @Filename: /b.mts //// import test from "./a.js"; //// //// [|function foo(abc: test.testInterface, def: test.testInterface) { //// console.log(abc); //// console.log(def); //// }|] //// // @Filename: /tsconfig.json ////{ "compilerOptions": { "module": "nodenext" }, "files": ["/folder/c.ts", "a.ts", "b.mts"] } const range = test.ranges(); verify.pasteEdits({ args: { pastedText: [`function foo(abc: test.abc, def: test.def) { console.log(abc); console.log(def); }`], pasteLocations: [range[0]], copiedFrom: { file: "b.mts", range: [range[1]] }, }, newFileContents: { "/folder/c.ts": `import * as test from "../a"; function foo(abc: test.abc, def: test.def) { console.log(abc); console.log(def); }` } });
src/services/refactors/moveToFile.ts
Outdated
for (const e of declaration.importClause.namedBindings.elements) { | ||
const symbol = checker.getSymbolAtLocation(e.propertyName || e.name); | ||
if (isNamespaceImport(declaration.importClause.namedBindings)) { | ||
const symbol = checker.getSymbolAtLocation(declaration.importClause.namedBindings.name); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there’s no need for checker.getSymbolAtLocation
anywhere in this function. Declarations always have a .symbol
; you can just do declaration.importClause.namedBindings.symbol
here, and .symbol
on each ImportSpecifier
below. Even the existing variable declaration / binding pattern stuff further down can lose the getSymbolAtLocation
.
@@ -356,6 +357,24 @@ function createImportAdderWorker(sourceFile: SourceFile | FutureSourceFile, prog | |||
} | |||
} | |||
|
|||
function addImportForExternalModuleSymbol(symbol: Symbol, originalSymbol: Symbol, isValidTypeOnlyUseSite: boolean) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
symbol
/originalSymbol
are confusing names here. It looks like originalSymbol
is the actual module symbol, and symbol
is just an existing alias for the module that’s only being used to see what name to use. Instead, try just passing in the module symbol and the name you want to use?
@@ -356,6 +357,24 @@ function createImportAdderWorker(sourceFile: SourceFile | FutureSourceFile, prog | |||
} | |||
} | |||
|
|||
function addImportForExternalModuleSymbol(symbol: Symbol, originalSymbol: Symbol, isValidTypeOnlyUseSite: boolean) { | |||
const useRequire = shouldUseRequire(sourceFile, program); | |||
if (originalSymbol.valueDeclaration) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this check for? When does an external module symbol not have a valueDeclaration?
createModuleSpecifierResolutionHost(program, host), | ||
); | ||
const info: FixInfo = { | ||
fix: { kind: ImportFixKind.AddNew, importKind: ImportKind.Namespace, addAsTypeOnly: isValidTypeOnlyUseSite ? AddAsTypeOnly.Allowed : AddAsTypeOnly.NotAllowed, useRequire, moduleSpecifierKind: undefined, moduleSpecifier }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since there may be multiple different forms of import that can reference a whole module, you were asking about whether it’s ok to unconditionally coerce them into a namespace import. That might be fine, but it seems like it would make sense for this function to take an optional referenceImport
like addImportFromExportedSymbol
, and copy its kind and type-only status where appropriate.
src/services/refactors/moveToFile.ts
Outdated
@@ -964,7 +964,7 @@ function inferNewFileName(importsFromNewFile: Map<Symbol, unknown>, movedSymbols | |||
function forEachReference(node: Node, checker: TypeChecker, enclosingRange: TextRange | undefined, onReference: (s: Symbol, isValidTypeOnlyUseSite: boolean) => void) { | |||
node.forEachChild(function cb(node) { | |||
if (isIdentifier(node) && !isDeclarationName(node)) { | |||
if (enclosingRange && !rangeContainsRange(enclosingRange, node)) { | |||
if (enclosingRange && !rangeContainsRange(enclosingRange, { pos: node.getStart(), end: node.getEnd() })) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if (enclosingRange && !rangeContainsRange(enclosingRange, { pos: node.getStart(), end: node.getEnd() })) { | |
if (enclosingRange && !rangeContainsRange(enclosingRange, createTextRangeFromNode(node, sourceFile))) { | |
And declare sourceFile
outside the recursive function so it doesn’t have to be retrieved with a parent walk each time
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good except for the one comment
errorIdentifierText: undefined, | ||
}; | ||
if (referenceImport && isImportClause(referenceImport)) { | ||
info = { ...info, fix: { ...info.fix, importKind: ImportKind.Default, addAsTypeOnly: isTypeOnlyImportDeclaration(referenceImport) ? AddAsTypeOnly.Required : AddAsTypeOnly.NotAllowed } }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think isValidTypeOnlyUseSite
should be consulted here in addition to the reference import. Also, what if the reference import is a type-only namespace import? It looks like we're only considering the type-onlyness of the import if it's a default import.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you explain what the rules are supposed to be for setting addAsTypeOnly
? I don't quite understand the last commit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe when isValidTypeOnlyUseSite
is true, then if isTypeOnlyImportDeclaration(referenceImport)
is true then it should be AddAsTypeOnly.Required
and if not, then it should be AddAsTypeOnly.Allowed
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, and if isValidTypeOnlyUseSite
is false, then it has to be AddAsTypeOnly.NotAllowed
. That makes sense to me!
...info, | ||
fix: { | ||
kind: ImportFixKind.AddNew, | ||
importKind: isNamespaceImport(referenceImport) ? ImportKind.Namespace : ImportKind.Default, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This condition can never be true based on the if statement it's in. Also, it looks like we don't use the referenceImport
to decide between default/namespace unless isValidTypeOnlyUseSite
is true, which is an unrelated piece of information. I think all this code can be reduced to a single assignment to info
with conditional properties, which would help untangle the two separate things you're trying to control.
Fixes #58740
This debug failure happened when trying to resolve a namespace import. As the identifier for the import did not have a parent, an error was thrown.