microsoft / microsoft/TypeScript

JSDoc `@import` causes `getCompletionEntryDetails` to crash

Abierto
#62,027 1 comentario 1 reacción 1 asignado Ver en GitHub

Nadie ha tomado este issue todavía.

Bug Domain: JSDoc Help Wanted
Lenguaje dominante
Go
Estrellas
111k
Forks
14.3k
Merge medio
2 d 4 h
PR fusionados (30 d)
132

Descripción

🔎 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

Guía de contribución

Abrir la guía de contribución

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Evaluación

Este issue todavía no se ha evaluado.

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.