Skip to content

bug(indexer): every JS/TS ES import collapses to a bare module row — named, default and namespace bindings are all lost #1526

Closed

Description

@Shashankss1205

tools/languages/javascript.py:436 and tools/languages/typescript.py:413

Import handling starts with:

node.child_by_field_name('import')

import_clause is an unnamed child of import_statement, not a field, so this always returns None and every ES import falls into the bare-module fallback (js :437-440, ts :414-417).

The entire named-import block below it (js :456-465, ts :428-437) is therefore dead code — and would still not work if the lookup were fixed, because it tests the clause against identifier / named_imports / namespace_import, which are children of import_clause, not the clause itself.

Reproduction

$ .venv/bin/python -c "
from codegraphcontext.tools.tree_sitter_parser import TreeSitterParser
from codegraphcontext.tools.languages.javascript import JavascriptTreeSitterParser
import tempfile, pathlib
p = pathlib.Path(tempfile.mkdtemp())/'a.js'
p.write_text(\"import { useState as us, useEffect } from 'react';\n\")
print(JavascriptTreeSitterParser(TreeSitterParser('javascript')).parse(str(p))['imports'])
"
[{'name': 'react', 'source': 'react', 'alias': None, ...}]

One row for the module. useState, the alias us, and useEffect are all lost. Default and namespace (import * as X) imports collapse the same way.

Impact

Beyond the missing binding data, this actively corrupts call resolution. resolution/calls.py:2173-2177 builds:

local_imports = {
    imp.get("alias") or (imp.get("name") or "").split(".")[-1]: imp.get("name")
    ...
}

With alias=None and a relative specifier like ./components/Button, the key becomes "/components/Button" — splitting on . cuts the path, not a module chain. So the identifier Button never maps to its import, and no call to a named ES import can be resolved cross-file. Affects .js, .ts and .tsx — the largest language group in most indexed repos.

Suggested fix

Walk to import_clause as an unnamed child, then read its children (identifier for default, named_imports → import_specifier with optional alias, namespace_import) to emit one row per binding.


Environment
Commit c0e0bed (main)
Python 3.12.3, Linux

Reproduced with the snippet above.

Activity

  1. added
    bugSomething isn't working
    severity: highWrong graph content, data loss, or a broken workflow with no clear signal
    area: indexerParsing, discovery, resolution, persistence
    on Aug 1, 2026
  2. github-actions commented on Aug 1, 2026

    @github-actions
    Contributor

    Hi! 👋 Join our CodeGraphContext Discord channel to collaborate: https://discord.gg/dR4QY32uYQ

  3. Shashankss1205 commented on Aug 5, 2026

    @Shashankss1205
    CollaboratorAuthor

    Important finding: this cannot be fixed at the parser alone. I implemented the parser fix, measured it, found it regresses call resolution, and backed it out rather than ship it. Recording the evidence so whoever picks this up doesn't repeat the work.

    The parser fix itself is straightforward

    import_clause is an unnamed child, not a field, so child_by_field_name('import') is always None. Walking named_children for the clause, then its children for identifier / namespace_import / named_imports, extracts every binding correctly:

    module (name)          imported_name  alias (local)
    ./exporter.js          default        defaultExport
    ./exporter.js          exportedFunction  exportedFunction
    ./exporter.js          ExportedClass  ExportedClass
    ./exporter.js          exportedVariable  myVar
    ./exporter.js          *              exporter
    

    Two things worth knowing for the implementation:

    • name must stay the module. writer.py:593 does MERGE (m:Module {name: row.name}), so putting the binding in name collapses every default import in the repo into one Module node called default. The binding belongs in imported_name + alias, both of which the writer already reads (:566).
    • namespace_import has no alias field — the local name is a plain identifier child. The existing code's lookup would have failed even if it had been reachable.

    Why it regresses

    Emitting one row per binding — with or without also keeping the original module-level row — costs precise call edges on the TypeScript fixture:

                     main  ->  with binding rows
    CALLS             389  ->  240   (-149)
    HEURISTIC_CALLS     0  ->  122   (+122)
    edges TOTAL      1489  -> 1462   (-27)
    

    Confirmed this is deterministic (two identical regeneration runs produce identical counts, so it is not golden nondeterminism), and confirmed by elimination: emitting only the module row through the same new helper leaves the golden passing unchanged. So the binding rows themselves are what break it.

    The likely mechanism is resolution/calls.py:1776 and :2174, which build:

    local_imports = {
        imp.get("alias") or (imp.get("name") or "").split(".")[-1]: imp.get("name")
        for imp in file_data.get("imports", [])
        ...
    }

    That is a dict comprehension over the import rows, so multiple rows per statement change which key wins and what each maps to. wildcard_imports just below iterates the same list.

    What this issue actually needs

    The parser change and the resolution update in one PR, with the golden delta checked to be additive. Anyone attempting it should regenerate sample_project_javascript and sample_project_typescript and compare edge counts by type — a rise in HEURISTIC_CALLS against a fall in CALLS is the signal that resolution has lost the mapping.

    Leaving this open and unassigned. The underlying bug is real — named ES imports carry no binding data at all today — but the fix is bigger than it looks.

  4. moved this from Backlog tasks to In progress in CGC Progress Boardon Aug 18, 2026
  5. moved this from In progress to Done in CGC Progress Boardon Aug 18, 2026
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

    area: indexerParsing, discovery, resolution, persistencebugSomething isn't workingseverity: highWrong graph content, data loss, or a broken workflow with no clear signal

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions