Skip to content

[API] getImmediateAliasedSymbol returns a different symbol than classic tsc #64532

Description

Acknowledgement

  • I acknowledge that issues using this template may be closed without further explanation at the maintainer's discretion.

Comment

What happens

getImmediateAliasedSymbol for a namespace import never gets back to the file target symbol if the module has a default export.

So for example for:

// mod.ts
export default class User {}
/// usage.ts
import * as X from "./mod"

If we have:

const moduleSymbol = checker.getSymbolOfSourceFile("./mod.ts")!;
let ns = /* X from `import * as X from "./mod"` */
const aliasSymbol = checker.getSymbolAtLocation(ns)!;
const resolvedModuleSymbol = checker.getImmediateAliasedSymbol(aliasSymbol);
console.log(moduleSymbol === resolvedModuleSymbol)

This will log true in TS6, false in TS7.
It will also log true in TS7 if mod.ts does not have a default export. (delete the default)

In TS7 when the module has a default export we seem to get a cloned symbol. This does internally have a target, but that is not exposed in the API so we can't get from the import symbol back to the same symbol as we would get by getting the symbol of the file.

Caused by microsoft/typescript-go#2168. which dropped the dontRecursivelyResolve flag that let classic tsc skip synthetic-default-interop cloning for this one-hop lookup.

Possible solutions:

  1. getImmediateAliasedSymbol returns the same symbol as it did in TS6
  2. Expose the target property on Symbol. Although I am not sure how this would interfere with other symbols that have a target.
  3. Expose some other way to reliably get the file symbol starting from the namespace import symbol.
Reproduction Code

TS6 (classic tsc):

import * as ts from "typescript";

const indexPath = "/repro/index.d.ts";
const withDefaultPath = "/repro/with-default.d.ts";

const files = new Map([
    [indexPath, `import * as X from "./with-default";`],
    [withDefaultPath, `export default class User {}`],
]);

const host = ts.createCompilerHost({});
host.readFile = fileName => files.get(fileName);
host.fileExists = fileName => files.has(fileName);
host.directoryExists = dirName => dirName === "/repro";

console.log(`=== TypeScript ${ts.version} (classic) ===`);

const program = ts.createProgram([indexPath, withDefaultPath], { strict: true }, host);
const checker = program.getTypeChecker();
const indexFile = program.getSourceFile(indexPath)!;
const withDefaultFile = program.getSourceFile(withDefaultPath)!;

const moduleSymbol = checker.getSymbolAtLocation(withDefaultFile)!;
let ns = ((indexFile.statements[0] as ts.ImportDeclaration).importClause?.namedBindings as ts.NamespaceImport)?.name
const aliasSymbol = checker.getSymbolAtLocation(ns!)!;
const resolvedModuleSymbol = checker.getImmediateAliasedSymbol(aliasSymbol);

console.log("moduleSymbol === getImmediateAliasedSymbol(X)?", moduleSymbol === resolvedModuleSymbol);

TS7 (tsgo):

import { createFileSystemLayer } from "@typescript/typescript/unstable/fs";
import { API } from "@typescript/typescript/unstable/sync";
import type { ImportDeclaration, NamespaceImport } from "@typescript/typescript/unstable/ast";

const cwd = "/repro";
const indexPath = "/repro/index.d.ts";
const withDefaultPath = "/repro/with-default.d.ts";
const configPath = "/repro/tsconfig.json";

const files = {
    [indexPath]: `import * as X from "./with-default";`,
    [withDefaultPath]:`export default class User {}`,
    [configPath]: JSON.stringify({ compilerOptions: { strict: true, noEmit: true }, files: [indexPath, withDefaultPath] }),
};


console.log("=== tsgo ===");
const api = new API({ cwd  });
const snapshot = api.createSnapshot({ openProjects: [configPath], fileSystem: createFileSystemLayer(Object.entries(files))});
const project = snapshot.getConfiguredProject(configPath)!;
const checker = project.checker;
const indexFile = project.program.getSourceFile(indexPath)!;


const moduleSymbol = checker.getSymbolOfSourceFile(withDefaultPath)!;
let ns = ((indexFile.statements[0] as ImportDeclaration).importClause?.namedBindings as NamespaceImport)?.name
const aliasSymbol = checker.getSymbolAtLocation(ns)!;
const resolvedModuleSymbol = checker.getImmediateAliasedSymbol(aliasSymbol);

console.log("moduleSymbol === getImmediateAliasedSymbol(X)?", moduleSymbol === resolvedModuleSymbol);

api.close();

No activity

Activity on this issue will appear here.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Needs InvestigationThis issue needs a team member to investigate its status.

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions