Skip to content

TypeScript imports naming the emitted .js extension never resolve through the import resolver (wrapper methods become self-edges) #1705

Description

@bompus

Summary

A TypeScript relative import that names the emitted extension (import { x } from './util.js' for util.ts, the form moduleResolution: node16 | nodenext | bundler requires) never resolves through the import resolver. resolveRelativeImport appends each candidate extension to the specifier (util.js.ts, util.js.tsx, …) and then tries the specifier as-is (util.js), and none of those files exist, so it returns null. Every name imported that way then falls through to bare-name matching.

Two visible effects on a real repo (Chrome MV3 extension, 582 files, 92 of its .ts files import with .js specifiers):

  1. A method that wraps the same-named function it imports resolves to itself. renderDockStyles() { return renderDockStyles(); } in a class, with renderDockStyles imported from ./template.js, gets a calls edge from the method to the method (resolvedBy: exact-match, confidence 0.4). Eight such false self-edges here: three dock-template wrappers, four static wrappers on a consensus engine, one ...args pass-through. This is the shape TypeScript: a call through this.<field> resolves to the ENCLOSING method when the two share a name — silent self-edge, 0% recall on that shape #1496 does not cover (that one is this.field.method()).
  2. Cross-module edges are name guesses. On this tree 4,002 calls/imports edges were resolvedBy: import and 15,419 exact-match; with the remap below it is 7,312 and 12,784. fuzzy drops 44 → 23.

Repro

// template.ts
export function renderDockStyles(): string { return ".dock {}"; }

// sidebar.ts
import { renderDockStyles } from "./template.js";
export class Sidebar {
  renderDockStyles(): string { return renderDockStyles(); }
}

codegraph init on those two files: the calls edge from Sidebar::renderDockStyles targets Sidebar::renderDockStyles, not template.ts::renderDockStyles. Same for .jsx.tsx, .mjs.mts, .cjs.cts, and for an aliased specifier (@/lib/util.js).

Fix

When the specifier ends in an emitted extension and no file with that exact name exists, retry with the source extensions TypeScript would have compiled from (.js.ts, .tsx, .d.ts; .jsx.tsx; .mjs.mts; .cjs.cts), for the TS/JS-family languages only. A real .js file still wins when both exist. PR incoming.

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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions