Skip to content
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

Closed
wants to merge 17 commits into from

Conversation

navya9singh
Copy link
Member

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.

@typescript-bot typescript-bot added Author: Team For Milestone Bug PRs that fix a bug with a specific milestone labels Jun 24, 2024
@navya9singh navya9singh changed the title Error for namespace import Error for namespace import debug failure Jun 24, 2024
@navya9singh navya9singh changed the title Error for namespace import debug failure Fixing namespace import debug failure Jun 24, 2024
Copy link
Member

@andrewbranch andrewbranch left a 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

  1. 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);
    }`
        }
    });
    
    

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);
Copy link
Member

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) {
Copy link
Member

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) {
Copy link
Member

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 },
Copy link
Member

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.

@@ -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() })) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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

@navya9singh navya9singh marked this pull request as ready for review July 19, 2024 21:31
Copy link
Member

@andrewbranch andrewbranch left a 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 } };
Copy link
Member

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.

Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

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,
Copy link
Member

@andrewbranch andrewbranch Jul 22, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Author: Team For Milestone Bug PRs that fix a bug with a specific milestone
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Debug failure on paste with imports
3 participants