Skip to content

Add TypeScript import path resolver with tsconfig paths support - #642

Open
alex4o wants to merge 4 commits into
CodeGraphContext:mainfrom
alex4o:ts-import-resolver
Open

Add TypeScript import path resolver with tsconfig paths support#642
alex4o wants to merge 4 commits into
CodeGraphContext:mainfrom
alex4o:ts-import-resolver

Conversation

@alex4o

@alex4o alex4o commented Feb 15, 2026

Copy link
Copy Markdown

What

Index <JSXComponent /> tags as call-sites so component usage shows up in the code graph.
Resolve TS/TSX import paths (aliases from tsconfig.json and relative imports) to absolute file paths so two files importing the same module share one Module node instead of creating duplicates.

Why

Without this:

  • <Layout> or <Button /> in .tsx files are invisible to the graph — no CALLS edge is created.
  • import { capitalize } from "@utils/string-helpers" and import { capitalize } from "./utils/string-helpers" produce two separate Module nodes for the same file, fragmenting the graph.

How

JSX call-site indexing

  • Added TypescriptJSXTreeSitterParser (extends the TS parser) with a _find_calls override that runs a tree-sitter query for jsx_opening_element / jsx_self_closing_element.
  • PascalCase names are treated as component references; lowercase (<div>) is ignored.
  • .tsx files now use the new typescriptjsx parser instead of reusing typescript.

TS import resolution

  • New ts_import_resolver.py module with:
    • parse_tsconfig_paths(project_root) — reads tsconfig.json (handles // comments, trailing commas, one level of extends), returns baseUrl + paths map.
    • resolve_ts_import(specifier, importing_file, ...) — resolves in order: relative → alias → baseUrl → bare (returns None for npm packages).
    • _try_resolve_file(base) — tries exact path → .ts/.tsx/.js/.jsxindex.{ts,tsx,js,jsx}.
  • graph_builder.py calls parse_tsconfig_paths once per indexing run, then passes the config to add_file_to_graph where each import is resolved before MERGE (m:Module {name: $resolved_path}).
  • Module nodes now also store raw_specifier (the original import string) for debugging.

Example

// tsconfig.json: { "paths": { "@utils/*": ["src/utils/*"] } }

// src/App.tsx
import { capitalize } from "@utils/string-helpers";  // → resolves to /abs/src/utils/string-helpers.ts
import Layout from "./Layout";                        // → resolves to /abs/src/Layout.tsx

const App = () => (
  <Layout>              // ← CALLS edge to Layout component
    <Button />          // ← CALLS edge to Button component
  </Layout>
);

Testing

  • Unit tests (test_ts_import_resolver.py, 25 tests): _try_resolve_file, relative imports, bare specifiers, alias imports, tsconfig.json parsing (comments, trailing commas, extends).
  • Integration tests (test_ts_import_resolver_integration.py, 14 tests): parse the real fixture app-service.ts with tree-sitter, then resolve every import against the fixture's tsconfig.json.
  • E2E tests (test_ts_indexing.py): index the full TS fixture into the DB and verify via Cypher that alias imports resolve to absolute paths, bare specifiers stay raw, JSX component calls are detected, and no duplicate Module nodes exist.
  • Fixture: expanded sample_project_typescript with tsconfig.json paths, .tsx components (App.tsx, Layout.tsx, Button.tsx), alias-imported modules (string-helpers, math-helpers, user-model, constants, logger), and app-service.ts exercising all import patterns.

alex4o and others added 3 commits February 15, 2026 01:07
Use the tsx tree-sitter grammar for .tsx files (the typescript grammar
misparses JSX as type assertions). Override _find_calls() in
TypescriptJSXTreeSitterParser to capture jsx_opening_element and
jsx_self_closing_element nodes, filtering to PascalCase names only.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Resolve TS/JS import specifiers to absolute file paths during indexing so
that Module nodes use resolved paths instead of raw specifiers. This makes
File->Module->File traversal work correctly for relative and alias imports.

- Create ts_import_resolver.py handling relative, alias (tsconfig paths),
  and bare specifier imports with extension/index file resolution
- Fix lang routing bug: include 'typescript' and 'typescriptjsx' alongside
  'javascript' in the import handling branch of graph_builder
- Parse tsconfig.json once per indexing run (supports baseUrl, paths,
  extends, comments, trailing commas)
- Store raw_specifier on Module nodes for debugging
- Add test fixture files with tsconfig paths aliases (@utils/*, @models/*,
  @shared/*, @app/*) and corresponding source files
- Add 50 unit + integration tests covering resolver, tsconfig parsing,
  and end-to-end resolution with real TS parser output

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add ts_import_resolver for resolving TypeScript path aliases from tsconfig.json
- Track JSX component usage as CALLS relationships in graph builder
- Add TSX test fixtures and e2e test for TypeScript indexing
- Update sample TypeScript project with TSX components

Amp-Thread-ID: https://ampcode.com/threads/T-019c6303-d1c9-763f-b9ab-08be8da73f8a
Co-authored-by: Amp <amp@ampcode.com>
@vercel

vercel Bot commented Feb 15, 2026

Copy link
Copy Markdown

@alex4o is attempting to deploy a commit to the shashankss1205's projects Team on Vercel.

A member of the Team first needs to authorize it.

…icity

- Resolve baseUrl-only imports (e.g. "utils/foo") before dismissing as bare specifiers
- Replace naive regex comment stripping with state-aware parser to avoid corrupting strings containing "//"
- Sort tsconfig path patterns by specificity (longest first) to match TypeScript behavior
- Remove debug log from graph_builder.py
- Improve E2E test assertions to parse JSON values instead of fragile string checks
- Restore .cgcignore file

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@Shashankss1205

Copy link
Copy Markdown
Collaborator

👋 Thanks for contributing to CodeGraphContext! Since this PR was opened, main has moved forward significantly (we just shipped v0.5.2 and merged ~50 PRs), so this one now has merge conflicts. Could you please rebase onto the latest main and resolve the conflicts? Once it is conflict-free and CI is green, we will review and merge it. Really appreciate your work — thank you! 🙏

@Shashankss1205

Copy link
Copy Markdown
Collaborator

Triage update: this tsconfig-paths resolver is still something we want — TS monorepos with path aliases currently lose those import edges. The branch has drifted a long way from main (the indexing pipeline was restructured into tools/indexing/), so it needs a rebase before it can be reviewed. @alex4o are you still interested in bringing it up to date? If not, we'll treat this as a design reference and reimplement against the current pipeline. Thanks for the substantial work either way! 🙏

@alex4o

alex4o commented Jul 31, 2026

Copy link
Copy Markdown
Author

Sry I don't have the time to contribute to this anymore

@Shashankss1205

Copy link
Copy Markdown
Collaborator

I spent a solid session trying to land this and did not get it over the line. Recording everything so the effort is not lost — my working attempt is pushed to port/642-ts-import-resolver on this repo if you want to pick it up from there.

This is a port, not a rebase

main has restructured underneath the PR. Two of your six conflict hunks in graph_builder.py target code that no longer exists:

  • Indexing loop — your inline for file in files: loop plus the ts_config setup is now run_tree_sitter_index_async(...) in tools/indexing/pipeline.py.
  • Import handling — the add_file_to_graph body you modify is now GraphWriter.add_file_to_graph in indexing/persistence/writer.py.

So ts_config cannot be threaded the way the PR does it. The other four hunks are the old inline TreeSitterParser class, which moved to tools/tree_sitter_parser.py as a registry — and that registry already contains "tsx": TypescriptJSXTreeSitterParser, so the JSX half of this PR has effectively landed already via another route.

What is definitely good

ts_import_resolver.py is sound and lands cleanly. It applies to current main with no conflicts and its 35 unit tests pass unmodified:

$ pytest tests/unit/parsers/test_ts_import_resolver.py -q
35 passed

Driven directly it does exactly what it claims:

@utils/string-helpers   -> .../src/utils/string-helpers.ts   SAME FILE
./utils/string-helpers  -> .../src/utils/string-helpers.ts   SAME FILE
react                   -> unresolved (bare package, correct)

What I could not finish

I wired the resolver into GraphWriter.add_file_to_graph (cached per project root, so tsconfig is parsed once rather than per import). Confirmed by instrumenting a real add_file_to_graph call that it is invoked and does resolve. But end to end on the TS fixture:

RESOLVED to a file path : 19
STILL an alias (failed) :  5   @models/user-model, @shared/constants,
                               @shared/logger, @utils/Button, @utils/string-helpers
bare package names (ok) :  2   react, reflect-metadata

Relative specifiers resolve; tsconfig aliases do not, even though the same aliases resolve when the resolver is called directly. My hypothesis was that tsconfig.json was being looked up from the indexed repo root rather than the TS project directory — I changed the lookup to walk up from the importing file, and the count did not move. So the cause is elsewhere and I have not found it.

Partial resolution is arguably worse than none: it leaves Module names inconsistent, some file paths and some specifiers, which is a different kind of fragmentation from the one this PR set out to fix. That is why I have not merged it.

One thing worth knowing, since it looks alarming

Regenerating the TS golden with your fixtures shows CALLS 389 → 262 and HEURISTIC_CALLS 0 → 141. I chased that specifically, because that exact signature is why I abandoned a different import fix in #1526.

It is not caused by the resolver. I isolated it by regenerating with the fixtures present but the wiring removed — every edge count was identical:

fixtures-only  ->  fixtures+resolver
CALLS              262 -> 262   (+0)
HEURISTIC_CALLS    141 -> 141   (+0)

So the shift comes from the new fixture files themselves. Most likely they introduce same-named symbols across files, making previously-unambiguous calls ambiguous — which may be correct behaviour, but is worth a look, since it is a large swing for a fixture addition.

Suggested path

  1. Rebase onto main yourself; take main's side for all six graph_builder.py hunks (the JSX registration is already there).
  2. Wire the resolver in GraphWriter.add_file_to_graphport/642-ts-import-resolver has a working starting point.
  3. Work out why aliases fail in the indexing path but not standalone. A focused test that calls add_file_to_graph on app-service.ts and asserts the Module name is a path would pin it.
  4. Consider splitting: ts_import_resolver.py plus its 35 tests could merge on its own today, with the wiring following separately.

Happy to review whenever you pick it up.

@Shashankss1205

Copy link
Copy Markdown
Collaborator

Thanks for this, and sorry it's sat so long — the tsconfig path resolution is genuinely useful and ts_import_resolver.py (+247 lines, with 600 lines of tests) is well built. Unfortunately main has restructured the indexing pipeline underneath this branch since February, and the conflicts are no longer mechanical, so I can't resolve them for you without guessing at your intent.

What changed on main:

  1. TreeSitterParser moved out of graph_builder.py into its own tools/tree_sitter_parser.py, and the if/elif language dispatch became a table lookup. Your branch still carries the old inline class, so it shows up as a ~70-line conflict that's really just a stale copy — safe to drop entirely.

  2. add_file_to_graph was rewritten and relocated. It now lives in tools/indexing/persistence/writer.py and uses batched UNWIND queries (one round-trip per node type) with as_posix() paths and a repo_path_str parameter. Your branch adds ts_config: Optional[Dict] to the old signature. These are competing rewrites of the same function rather than additive changes — the ts_config threading needs re-applying against the new batched implementation, and probably in writer.py rather than graph_builder.py.

  3. .cgcignore was removed from the repo (in fix: buffer full request body before SSE JSON-RPC parsing (#1113) #1319). Your branch modifies it; that hunk can just be dropped.

  4. Same story for update_file_in_graph, which main also rewrote.

The good news is that the actual substance of your PR — ts_import_resolver.py, the typescriptjsx.py changes, the fixtures and all the tests — merges cleanly. It's only the ts_config plumbing through the (now rewritten) indexing path that needs redoing.

If you rebase onto current main and re-thread ts_config through writer.py's add_file_to_graph, I'll review and merge promptly. Happy to answer questions about the new pipeline shape if that helps — just reply here.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants