FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

[api] A nested request from a resolveModuleName callback gets another request's answer · Issue #64631 · microsoft/TypeScript · GitHub

[api] A nested request from a resolveModuleName callback gets another request's answer #64631

Description

🔎 Search Terms

createModuleResolver, resolveModuleName callback, nested request, inProgressSnapshot, SyncConn, wrong module

🕗 Version & Regression Information

  • This is the behavior in every version I tried: 7.1.0-dev.20261003.1 and 7.1.0-dev.20261004.1 (commit 50d70a3, the newest typescript@next), the synchronous API (typescript/unstable/sync), linux-x64, Node 26.8.2. Module resolver callbacks arrived in [api] Provide module resolution overrides #64299, so earlier versions do not have this API.

⏯ Playground Link

The API is not in the playground. The reproduction is one file.

💻 Code

package.json:

{ "private": true, "type": "module", "dependencies": { "typescript": "7.1.0-dev.20261004.1" } }

repro.mjs writes 300 files that each import three siblings, then loads them with a resolveModuleName callback that asks a default resolver for the answer with the in-progress snapshot, as the "module resolver callbacks can delegate to another resolver" API test does. The callback always returns the right file itself and only counts whether the nested answer was right.

import { mkdirSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { API } from "typescript/unstable/sync";

const N = 300;
const nested = process.argv[2] !== "local";
const dir = join(process.cwd(), "proj");
rmSync(dir, { recursive: true, force: true });
mkdirSync(dir, { recursive: true });
for (let i = 0; i < N; i++) {
  const next = [1, 2, 3].map((k) => (i + k) % N);
  writeFileSync(
    join(dir, `m${i}.ts`),
    next.map((j) => `import { v${j} } from "./m${j}.js";`).join("\n") +
      `\nexport const v${i}: number = 1;\nexport const use${i} = [${next.map((j) => `v${j}`).join(", ")}];\n`,
  );
}
const compilerOptions = { module: 199, moduleResolution: 99, strict: true, noEmit: true };
const rootFiles = Array.from({ length: N }, (_, i) => join(dir, `m${i}.ts`));

const api = new API({ cwd: dir });
const defaultResolver = api.createModuleResolver(compilerOptions);
let right = 0;
let wrong = 0;
const customResolver = api.createModuleResolver(compilerOptions, {
  resolveModuleName(moduleName, containingDirectory, resolutionMode, { snapshot }) {
    const expected = join(containingDirectory, moduleName.replace(/\.js$/u, ".ts"));
    if (nested) {
      const answer = defaultResolver.resolveModuleName(moduleName, containingDirectory, resolutionMode, { snapshot });
      if (answer.resolvedModule?.resolvedFileName === expected) right++;
      else wrong++;
    }
    return { resolvedFileName: expected };
  },
});
const snapshot = api.createSnapshot({
  createPrograms: [{ rootFiles, compilerOptions, options: { moduleResolver: customResolver } }],
});
const errors = snapshot.operation.createdPrograms[0].getSemanticDiagnostics();
console.log(
  nested ? `nested answers: ${right} right, ${wrong} for another request;` : "no nested request;",
  `program errors: ${errors.length}${errors[0] ? ` (first: ${errors[0].text})` : ""}`,
);
snapshot.dispose();
api.close();

🙁 Actual behavior

On 7.1.0-dev.20261004.1:

$ node repro.mjs
nested answers: 16 right, 884 for another request; program errors: 893 (first: Module '"./m1.js"' has no exported member 'v1'.)
$ node repro.mjs
nested answers: 27 right, 873 for another request; program errors: 889 (first: Module '"./m1.js"' has no exported member 'v1'.)
$ node repro.mjs local
no nested request; program errors: 0
$ GOMAXPROCS=1 node repro.mjs
nested answers: 900 right, 0 for another request; program errors: 0

Two answers go astray, and only when the callback makes a nested request. The nested resolveModuleName returns the answer to a different import, and the callback's own return value reaches the wrong import too: every callback returns the correct file, yet almost every import in the program is bound to another module.

🙂 Expected behavior

Each nested request gets its own answer and each callback answer reaches the import it was asked for, so the program has 0 errors with any number of threads.

Additional information about the issue

Impact: a resolver callback that delegates to another resolver, the pattern the API's own test shows, binds most imports to the wrong module once the loader uses more than one thread. Every type and diagnostic the tool reads from that program is then wrong, and no error says so. In the reproduction, 873 of 900 nested answers went to another request.

The program loader resolves imports from several goroutines, and each one calls the callback through SyncConn.Call. Call matches a response by method name, so it relies on c.mu to keep its request and its response together. When the client's callback makes a nested request, Call releases c.mu while it handles that request. Another goroutine's Call then writes its own callback request, which the synchronous client handles inside its pending nested call. When the first nested request finishes, its response reaches the client while the client waits for the second callback's nested answer, and the two callback responses then reach the wrong goroutines because they share a method name.

I met this while building deadset-ts, the TypeScript analyzer of deadset, on the TypeScript 7 API. A pull request with a fix and a test follows.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions


      Back | FazBrowse Home | New Git URL