Skip to content

[api] createSnapshot never returns when the opened tsconfig.json has a circular extends #64754

Open

Description

🔎 Search Terms

createSnapshot, openProjects, extends, circular extends, TS18000, Circularity detected while resolving configuration, ExtendedSourceFiles, configFileRegistryBuilder, hang, deadlock

🕗 Version & Regression Information

  • This is the behavior in every version I tried: typescript@7.1.0-dev.20261010.1 (native binary @typescript/typescript-linux-x64@7.1.0-dev.20261010.1, built from main at aa81492) and a build of main at 96fffa9, with the synchronous API (typescript/sync) on linux-x64, Node 26.11.1.

⏯ 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.20261010.1" } }

repro.mjs writes a tsconfig.json that extends a.json, which extends tsconfig.json again, then opens the project:

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

const dir = join(process.cwd(), "proj");
rmSync(dir, { recursive: true, force: true });
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, "tsconfig.json"), JSON.stringify({ extends: "./a.json" }));
writeFileSync(join(dir, "a.json"), JSON.stringify({ extends: "./tsconfig.json" }));
writeFileSync(join(dir, "main.ts"), "export const one = 1;\n");
const configFileName = join(dir, "tsconfig.json");
const api = new API({ cwd: dir });
console.log("parseConfigFile:", api.parseConfigFile(configFileName).errors.map((d) => `TS${d.code}: ${d.text}`));
const snapshot = api.createSnapshot({ openProjects: [configFileName] });
console.log("createSnapshot:", snapshot.getProjects()[0].program.getConfigFileParsingDiagnostics().map((d) => `TS${d.code}: ${d.text}`));
snapshot.dispose();
api.close();

🙁 Actual behavior

parseConfigFile reports the circularity, and then createSnapshot never returns. The process has to be killed:

$ node repro.mjs
parseConfigFile: [ 'TS18000: Circularity detected while resolving configuration: {0}' ]
^C
$ npx tsc -p proj
error TS18000: Circularity detected while resolving configuration: {0}
$ echo $?
2

🙂 Expected behavior

createSnapshot returns, and the project's config file parsing diagnostics contain TS18000, as parseConfigFile and tsc -p proj already report.

Additional information about the issue

Impact: one circular extends in a repository makes createSnapshot hang with no error, where tsc -p reports TS18000 and exits 2 at once, so an analyzer on the API stalls on that repository instead of reporting what is wrong with it. The same happens when the cycle runs through a base config shared with a project that was opened first.

acquireConfigForProject parses the config inside entry.ChangeIf, which holds that config's entry lock. reloadIfNeeded then calls updateExtendingConfigs, which runs LoadOrStore on every path in the parsed config's ExtendedSourceFiles, and LoadOrStore locks an entry it finds. parseConfig builds that list from each extended config and that config's own list, with nothing removed. When the chain cycles back to the opened config, the opened config is in its own list, so the goroutine waits on the entry lock it already holds. tsc -p does not walk the list under a lock, which is why it returns. The language server goes through the same acquireConfigForProject, but I reproduced only the API.

The {0} left in the TS18000 text, on the CLI too, has a separate cause and is not what this report is about.

I met this while building deadset-ts, the TypeScript analyzer of deadset, on the TypeScript 7 API. A pull request with the 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