Skip to content

JSDoc @import causes getCompletionEntryDetails to crash #62027

Open
@louwers

Description

@louwers

🔎 Search Terms

jsdoc

The supplied value [object Object] did not pass the test 'isTypeKeywordToken'

🕗 Version & Regression Information

At least since TypeScript 5.5.0.

Since @import has been introduced in TypeScript 5.5.0 it does not make sense to look further back than that.

⏯ Playground Link

No response

💻 Code

tl;dr

try to run getCompletionsAtPosition on this file main.js:

/** @import * as t from '/types.ts' */

import { preventDefault } from "/other-file.js";

p

jsconfig.json

{
  "compilerOptions": {
    "checkJs": true,
    "strict": true,
    "paths": {
      "/*": ["./*"],
    },
    "allowImportingTsExtensions": true
  },
    "include": ["**/*.js", "**/*.ts"],
}

other-file.js

export function preventDefault() {
  
}

types.ts

export {};

And it will crash.

Here is a ChatGPT generated reproduction that should be self-contained:

#!/usr/bin/env ts-node

import fs from "fs";
import os from "os";
import path from "path";
import * as ts from "typescript";

// -------------------------------------------------------------------------------------
// Default jsconfig.json content to embed
// -------------------------------------------------------------------------------------
const defaultJsconfig = {
  compilerOptions: {
    checkJs: true,
    strict: true,
    paths: {
      "/*": ["./*"]
    },
    allowImportingTsExtensions: true
  },
  include: ["**/*.js", "**/*.ts"]
};

// -------------------------------------------------------------------------------------
// Create synthetic project in a temp folder
// -------------------------------------------------------------------------------------
function createTempProject(): string {
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "ts-crash-"));

  // Write the embedded jsconfig.json
  fs.writeFileSync(
    path.join(tempDir, "jsconfig.json"),
    JSON.stringify(defaultJsconfig, null, 2),
    "utf8"
  );

  // Dummy supporting files
  fs.writeFileSync(
    path.join(tempDir, "types.ts"),
    "export interface Foo { bar: string }\nexport const baz = 42;\n",
  );
  fs.writeFileSync(
    path.join(tempDir, "other-file.js"),
    "export function preventDefault() {}\n"
  );

  // main.js with your exact snippet (absolute imports!)
  fs.writeFileSync(
    path.join(tempDir, "main.js"),
    `/** @import * as t from '/types.ts' */\n\nimport { preventDefault } from "/other-file.js";\n\np`
  );

  return tempDir;
}

// -------------------------------------------------------------------------------------
// Build a Language-Service that *pretends* absolute paths exist but empty
// -------------------------------------------------------------------------------------
function createLanguageService(root: string): import("typescript").LanguageService {
  const files = new Map<string, { text: string; version: number }>();

  const addFile = (filePath: string) => {
    const full = path.resolve(root, filePath);
    files.set(full, { text: fs.readFileSync(full, "utf8"), version: 0 });
  };
  ["main.js", "types.ts", "other-file.js"].forEach(addFile);

  const host: import("typescript").LanguageServiceHost = {
    getCompilationSettings: () => ({
      allowJs: true,
      checkJs: true,
      ...defaultJsconfig.compilerOptions,
      target: ts.ScriptTarget.ES2022
    }),
    getScriptFileNames: () => Array.from(files.keys()),
    getScriptVersion: (f) => `${files.get(f)?.version ?? 0}`,
    getScriptSnapshot: (f) => {
      const entry = files.get(f);
      if (entry) return ts.ScriptSnapshot.fromString(entry.text);
      // Pretend absolute‑path imports exist but are empty strings
      if (f.startsWith(path.sep)) return ts.ScriptSnapshot.fromString("");
      return undefined;
    },
    getCurrentDirectory: () => root,
    getDefaultLibFileName: (o) => ts.getDefaultLibFilePath(o),
    fileExists: (f) => files.has(f) || f.startsWith(path.sep),
    readFile: (f) => files.get(f)?.text ?? "",
    readDirectory: ts.sys.readDirectory,
    useCaseSensitiveFileNames: () => ts.sys.useCaseSensitiveFileNames,
  };

  return ts.createLanguageService(host, ts.createDocumentRegistry());
}

// -------------------------------------------------------------------------------------
// Attempt the crash reproduction
// -------------------------------------------------------------------------------------
function main() {
  console.log(`Using TypeScript ${ts.version}`);

  const projectDir = createTempProject();
  const ls = createLanguageService(projectDir);
  const targetFile = path.join(projectDir, "main.js");
  const src = fs.readFileSync(targetFile, "utf8");
  const pos = src.lastIndexOf("p") + 1;

  const completionInfo = ls.getCompletionsAtPosition(targetFile, pos, {
    includeExternalModuleExports: true,
    includeInsertTextCompletions: true,
  });

  if (!completionInfo) {
    console.error("No completions returned – cannot proceed.");
    return;
  }

  console.log(`Entries: ${completionInfo.entries.length}\n`);
  console.log("Running two passes (with & without entry.data)…\n");

  const run = (withData: boolean) => {
    console.log(withData ? "— WITH data —" : "— data = undefined —");
    completionInfo.entries.forEach((entry) => {
      try {
        const details = ls.getCompletionEntryDetails(
          targetFile,
          pos,
          entry.name,
          {},
          entry.source,
          {},
          withData ? entry.data : undefined,
        );
      } catch (e) {
        console.error(`✗ ${entry.name}${(e as Error).message}`);
        console.error((e as Error).stack);
      }
    });
    console.log();
  };

  run(true);
  run(false);

  console.log(`Temp project retained at: ${projectDir}`);
}

main();

🙁 Actual behavior

Crash!

Running two passes (with & without entry.data)…

— WITH data —
✗ t → Debug Failure. Invalid cast. The supplied value [object Object] did not pass the test 'isTypeKeywordToken'.
Error: Debug Failure. Invalid cast. The supplied value [object Object] did not pass the test 'isTypeKeywordToken'.
at cast (/Users/bart/src/sqlife/repro/node_modules/typescript/lib/typescript.js:3350:16)
at getTypeKeywordOfTypeOnlyImport (/Users/bart/src/sqlife/repro/node_modules/typescript/lib/typescript.js:139958:10)
at promoteImportClause (/Users/bart/src/sqlife/repro/node_modules/typescript/lib/typescript.js:158612:32)
at promoteFromTypeOnly (/Users/bart/src/sqlife/repro/node_modules/typescript/lib/typescript.js:158602:7)
at codeActionForFixWorker (/Users/bart/src/sqlife/repro/node_modules/typescript/lib/typescript.js:158558:35)
at /Users/bart/src/sqlife/repro/node_modules/typescript/lib/typescript.js:158501:13
at _ChangeTracker.with (/Users/bart/src/sqlife/repro/node_modules/typescript/lib/typescript.js:178070:5)
at codeActionForFix (/Users/bart/src/sqlife/repro/node_modules/typescript/lib/typescript.js:158500:60)
at Object.getPromoteTypeOnlyCompletionAction (/Users/bart/src/sqlife/repro/node_modules/typescript/lib/typescript.js:157905:43)
at getCompletionEntryCodeActionsAndSourceDisplay (/Users/bart/src/sqlife/repro/node_modules/typescript/lib/typescript.js:167744:44)

— data = undefined —
✗ t → Debug Failure. Invalid cast. The supplied value [object Object] did not pass the test 'isTypeKeywordToken'.
Error: Debug Failure. Invalid cast. The supplied value [object Object] did not pass the test 'isTypeKeywordToken'.
at cast (/Users/bart/src/sqlife/repro/node_modules/typescript/lib/typescript.js:3350:16)
at getTypeKeywordOfTypeOnlyImport (/Users/bart/src/sqlife/repro/node_modules/typescript/lib/typescript.js:139958:10)
at promoteImportClause (/Users/bart/src/sqlife/repro/node_modules/typescript/lib/typescript.js:158612:32)
at promoteFromTypeOnly (/Users/bart/src/sqlife/repro/node_modules/typescript/lib/typescript.js:158602:7)
at codeActionForFixWorker (/Users/bart/src/sqlife/repro/node_modules/typescript/lib/typescript.js:158558:35)
at /Users/bart/src/sqlife/repro/node_modules/typescript/lib/typescript.js:158501:13
at _ChangeTracker.with (/Users/bart/src/sqlife/repro/node_modules/typescript/lib/typescript.js:178070:5)
at codeActionForFix (/Users/bart/src/sqlife/repro/node_modules/typescript/lib/typescript.js:158500:60)
at Object.getPromoteTypeOnlyCompletionAction (/Users/bart/src/sqlife/repro/node_modules/typescript/lib/typescript.js:157905:43)
at getCompletionEntryCodeActionsAndSourceDisplay (/Users/bart/src/sqlife/repro/node_modules/typescript/lib/typescript.js:167744:44)

🙂 Expected behavior

No crash

Additional information about the issue

No response

Metadata

Metadata

Assignees

Labels

BugA bug in TypeScriptFix AvailableA PR has been opened for this issueHelp WantedYou can do this

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions