fix(js): resolve split `export { .. } from` clauses and a barrel's `default as X` component re-export by if12is · Pull Request #4630 · Graphify-Labs/graphify · GitHub
Skip to content

fix(js): resolve split export { .. } from clauses and a barrel's default as X component re-export - #4630

Open
if12is wants to merge 2 commits into
Graphify-Labs:v8from
CompertMe:upstream/js-barrel-default-reexport
Open

if12is wants to merge 2 commits into
Graphify-Labs:v8from
CompertMe:upstream/js-barrel-default-reexport

Conversation

@if12is

@if12is if12is commented Oct 21, 2026

Copy link
Copy Markdown

Branch: upstream/js-barrel-default-reexport (base: upstream/v8 at b90b9df). Two commits, one
concern: a barrel file that re-exports single-file components must give its importers real edges.
I found no existing issue for this; related to the earlier barrel work around #2034 (ambiguous
default as re-exports) which this keeps intact.

Problem

A component barrel such as resources/js/ui/alert/index.js:

export {
  default as Alert
}
from './Alert.vue'

has two defects.

  1. The layout above (closing } on one line, from '...' on the next) is valid JavaScript, but
    tree-sitter-javascript and tree-sitter-typescript end the statement at the }: the clause becomes
    two stray expression statements, so the module string is never seen and the barrel gets no
    import / re-export edge at all. The same applies to import { .. }\nfrom '...'.
  2. For import { Alert } from '@/ui/alert', the importer's edge targeted a symbol id inside the barrel
    that no real node has, so the imports edge ended on a sourceless placeholder node. A single-file component's default
    export is the file itself, so the edge should land on the .vue / .svelte file node.

Implementation

  • graphify/extractors/resolution.py: new _normalize_js_split_from_clause, which moves a from '...'
    clause that starts the line after a line holding only } up onto the } line, byte for byte the
    same length and with the same line count, so offsets and line numbers hold (from 'x' is a syntax
    error anywhere else, so the pattern cannot match anything but this statement). The JS symbol-fact
    parse applies it as well, so multi-line barrels take part in symbol resolution.
  • graphify/extract.py: the JS/TS parse path imports it and applies it before parsing (alongside the
    existing TS export type * normalisation) and when masking source.
  • graphify/extractors/resolution.py (origin_node_id): a default export of a .vue / .svelte
    file resolves to that file's node. A named import through a barrel resolves there only when the
    barrel exports that name from exactly one module; two default as Alert re-exports stay ambiguous
    and the import is left unresolved.
  • graphify/cache.py: AST cache schema 20 -> 21 (cached JS/TS/Vue extractions lack the barrel edges).

The first commit is the clause normalisation (with the schema bump); the second adds the component
resolution and moves the normaliser so the symbol-fact parse can share it.

Verification

New tests: tests/test_js_split_export_from.py (3 tests: multi-line export clause with from on its
own line; the split form keeps the symbol re-export; the same for an import clause) and
tests/test_js_barrel_default_reexport.py (2 tests: a named import through the barrel targets the
component file; an ambiguous default name is not guessed).

Fails before, passes after (unmodified graphify/ of upstream/v8):

$ uv run pytest tests/test_js_split_export_from.py tests/test_js_barrel_default_reexport.py -q
3 failed, 2 passed       # assert 'ui_tabs' in {'ui_foo'}
                         # {'resources_js_ui_alert_index_alerttitle', ...} != {'resources_js_ui_alert_alert', ...}
$ uv run pytest tests/test_js_split_export_from.py tests/test_js_barrel_default_reexport.py -q
5 passed

Full checks on this branch:

$ uv run pytest tests -q
1 failed, 8474 passed, 15 skipped
$ uv run ruff check .
All checks passed!
$ uv run pyright
641 errors, 0 warnings, 0 informations

Bare upstream/v8 (b90b9df) gives 1 failed, 8469 passed, 15 skipped, ruff clean and the same
641 errors from pyright. The one failure is pre-existing and identical on v8:
tests/test_skill_no_backtick_substitution.py::test_control_a_backticked_unknown_command_is_caught.

Before/after on two real Laravel + Vue applications (private copies, uv run graphify update <copy> --force from this branch and from bare upstream/v8):

app A (erp) before after app B (easm) before after
JS/Vue imports / re_exports edges that land on a real target 2379 3838 830 830
re_exports edges 67 94
nodes 18192 18113 9871 9871
sourceless nodes 376 297 195 195

In app A, 1432 imports edges changed target (from a sourceless placeholder node named after the
barrel symbol, e.g. index.alert, to the .vue file node) and 27 re_exports edges were added;
the 79 nodes that disappeared are those placeholder nodes. App B has no such barrels, and its graph is unchanged. Edges whose endpoint is not a node: 0
before and after in both.

Limitations

  • Only the } newline from '...' split is normalised; other unusual line breaks inside an
    import/export statement are not.
  • Component resolution covers .vue and .svelte files; other default exports (a default-exported
    function or class in a .js file) are unchanged.
  • A barrel that re-exports the same name from two modules stays unresolved on purpose.
  • Auto-registered components and dynamic imports are not modelled.

…t line

tree-sitter ends the statement at the closing brace of
'export {\n  default as X\n}\nfrom "./X.vue"', so the module string is never
seen and the barrel gets no edges. Move the clause up byte-for-byte.
…to the component

import { Alert } from '@/ui/alert' where the barrel does
export { default as Alert } from './Alert.vue' left the imports edge on a
nonexistent symbol id. A single-file component's default export is the file,
so the edge now targets the .vue/.svelte file node when the barrel exports
that name from exactly one module. The split-from normalisation also feeds the
symbol-fact parse so multi-line barrels take part.
@if12is
if12is requested a review from safishamsi as a code owner October 21, 2026 13:06
Copilot AI balanced review requested due to automatic review settings October 21, 2026 13:06

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

@github-actions

Copy link
Copy Markdown

Thanks for the pull request, @if12is. A maintainer will review it soon.

Want to talk it through while it is in review? Come join us on our Discord server. For longer-form discussion there is also GitHub Discussions.

A couple of things that speed up review: make sure the test suite passes on Python 3.10 and 3.13, and that the change keeps extraction deterministic.

@graphify-labs graphify-labs Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

No issues found by static checks — no coupling regressions or blocking issues in the code graph. That is not the same as safe to merge: see what was not checked.

Not checked: tests were not run; formal verification proved 0 of 4 changed function(s) (2 sampled, not proven, 2 not verified).

Formal verification. PR-changed functions: 2/4 verified (0 proven, 2 may-equivalent, 0 distinguished) · 2 not verified (1 vacuous, 1 unsupported).

Not verified on this run: extract\_js (vacuous: never exercised), \_apply\_symbol\_resolution\_facts (unsupported).


Graphify review — findings

Adds parsing for JS/TS/Vue export { ... } and import { ... } clauses whose from '...' sits on the line after the closing }, so barrel files using that layout now get their import/re-export edges. _normalize_js_split_from_clause rewrites the source before parsing, moving the clause up onto the } line while keeping the byte length and line count unchanged. It runs in extract_js (now for plain JS too, not just TS), extract_vue, and _parse_js_tree. Also resolves a barrel's export { default as X } from './X.vue' (or .svelte) to the single-file component's file node, since no symbol is named default. If two re-exports of the same name make it ambiguous, the import is left unresolved. Bumps _AST_CACHE_SCHEMA to 21 so cached JS/TS/Vue extractions re-run.

No blocking issues surfaced. 1 lower-confidence candidate did not survive cross-model review.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 3861 functions depend on the 599 functions this change touches.

Health — this change adds coupling hotspots:

  • worse: extract() — 991 callers, 59 callees
  • worse: extract_js() — 95 callers, 8 callees
  • worse: extract_vue() — 10 callers, 9 callees
  • worse: _apply_symbol_resolution_facts() — 5 callers, 11 callees
  • worse: _parse_js_tree() — 8 callers, 5 callees

Verification — 3861 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 3610 function(s) in the blast radius were not formally verified this run

Test selection

Test selection

231 of 438 test file(s) selected (53%) via static blast radius.

  • tests/test_affected_overrides.py — impact
  • tests/test_astro_extraction.py — impact
  • tests/test_astro_import_ids.py — impact
  • tests/test_blade_extractor.py — impact
  • tests/test_build.py — impact
  • tests/test_builtin_global_type_refs.py — impact
  • tests/test_c_include_resolution.py — impact
  • tests/test_cache.py — impact
  • tests/test_cache_stale_import_target.py — impact
  • tests/test_case_sensitive_resolution.py — impact
  • tests/test_charmap_encoding.py — impact
  • tests/test_chunking.py — impact
  • tests/test_cjs_module_extension.py — impact
  • tests/test_cobol_extractor.py — impact
  • tests/test_cpp_method_declarations.py — impact
  • tests/test_cpp_nested_and_cli.py — impact
  • tests/test_cpp_objc_cross_file_calls.py — impact
  • tests/test_cpp_template_type_params.py — impact
  • tests/test_cross_extension_reexport_self_cycle.py — impact
  • tests/test_cross_language_call_resolution.py — impact
  • tests/test_cross_repo_external_call_guards.py — impact
  • tests/test_cross_repo_member_calls.py — impact
  • tests/test_csharp_call_site_generic_args.py — impact
  • tests/test_csharp_delegate_declarations.py — impact
  • tests/test_csharp_enum_members.py — impact
  • tests/test_csharp_field_generic_args.py — impact
  • tests/test_csharp_foreach_loop_variable.py — impact
  • tests/test_csharp_generic_callsites.py — impact
  • tests/test_csharp_interface_dispatch.py — impact
  • tests/test_csharp_member_calls.py — impact
  • tests/test_csharp_member_chain_calls.py — impact
  • tests/test_csharp_member_nodes.py — impact
  • tests/test_csharp_object_creation.py — impact
  • tests/test_csharp_partial_classes.py — impact
  • tests/test_csharp_tuple_type_refs.py — impact
  • tests/test_csharp_type_resolution.py — impact
  • tests/test_csharp_underscore_collisions.py — impact
  • tests/test_csharp_var_call_return_type.py — impact
  • tests/test_csharp_verbatim_interpolation.py — impact
  • tests/test_definition_file_portability.py — impact
  • tests/test_detect.py — impact
  • tests/test_dotnet.py — impact
  • tests/test_duplicate_annotation_edges.py — impact
  • tests/test_ejs_extractor.py — impact
  • tests/test_elixir_defdelegate.py — impact
  • tests/test_elixir_import_resolution.py — impact
  • tests/test_elixir_keyword_def_calls.py — impact
  • tests/test_elixir_qualified_calls.py — impact
  • tests/test_elixir_unqualified_call_scope.py — impact
  • tests/test_erlang_extractor.py — impact
  • … and 181 more

Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.

Formal verification

Could not verify: Could not verify extract\_js.

The verifier did not have enough to check extract\_js, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: no capturable inputs from the test suite; property tier: not verifiable: all 9 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly NameError — names the real obstacle, not a sampling gap)

No difference found (not proven): No behavior difference found in extract\_vue (not a proof).

The verifier ran both versions of extract\_vue on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

Could not verify: Could not verify \_apply\_symbol\_resolution\_facts.

The verifier did not have enough to check \_apply\_symbol\_resolution\_facts, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: no capturable inputs from the test suite; property tier: parameter `facts` is annotated `_SymbolResolutionFacts` — outside the synthesizable primitive/collection set

No difference found (not proven): No behavior difference found in \_parse\_js\_tree (not a proof).

The verifier ran both versions of \_parse\_js\_tree on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

· 2 grounded finding(s) anchored inline below; 3 more finding(s) on lines outside this diff (see the check run).

_SFC_SUFFIXES = frozenset({".vue", ".svelte"})


def _apply_symbol_resolution_facts(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — _apply_symbol_resolution_facts()

fans out to 11 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression — _parse_js_tree()

8 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

This branch has not been deployed

No deployments
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.

3 participants