Skip to content

Vue/TS: no call edge for calls in template expressions or in top-level destructuring declarations #2340

Closed

Description

@deniskern

codegraph 1.6.2, Linux arm64, Nuxt 4 project (Vue SFCs with <script setup lang="ts">, auto-imported composables). codegraph callers <composable> misses most real callers. It reduces to two extraction gaps; auto-import itself is not the cause (an auto-imported call in a plain const x = useFoo() is found).

Repro (any Vue project, codegraph sync after adding the files)

<!-- A.vue: missed -->
<script setup lang="ts">
const { a } = useFoo(1)
</script>

<!-- F.vue: missed -->
<script setup lang="ts">
const [b] = useFoo(1)
</script>

<!-- B.vue: found -->
<script setup lang="ts">
const x = useFoo(1)
</script>

<!-- H.vue: missed -->
<template><div>{{ useBar(link) }}</div></template>
// G.ts: missed (module scope)
const { a } = useFoo(1)
export const g = a

// D.ts: found (inside a function)
export function probe() { const { a } = useFoo(1); return a }

Expected

A calls edge from the component (or file) to the callee for top-level destructuring declarations, and for call expressions inside template interpolations and bound attributes (:to="useBar(link.location)").

Actual

No edge and no unresolved_refs row. On a real production Nuxt project, callers for a layout composable returned 0 of 22 calling components, and for a URL helper 1 of 7 calling files — so impact/callers answers for composables are mostly empty in Nuxt codebases.

No config option seems to cover it (README lists exclude, include, includeIgnored, deprioritize).

Activity

  1. colbymchenry commented on Oct 5, 2026

    @colbymchenry
    Owner

    Thanks @deniskern for narrowing this down to two gaps. That made both easy to find.

    Both are fixed on main in #2357:

    • Top-level destructuring. A declaration like const { a } = useFoo(1) or const [b] = useFoo(1) creates no symbol of its own, and the extractor skipped it before reading the right-hand side. So at the top of a .ts/.js file or a <script setup> block, the call was never recorded. The initializer, and any default value in the pattern, is now read the way a function body reads it. The call belongs to the component in <script setup>, or to the file at module level. Released builds parse TypeScript and JavaScript in the native parser, which has the same fix.
    • Template calls. Calls in {{ }} interpolations and in directive values (:to="useBar(link.location)", v-if, v-show, v-model, @click="save(item)", custom v-*) are now recorded as calls from the component, on their real line. Names the template binds itself are never taken for project functions: v-for items, slot props, and arrow parameters. Neither are $t/$emit-style helpers or the globals Vue allows.

    On your repro, callers useFoo now lists A.vue, B.vue, F.vue, G.ts and D.ts, and callers useBar lists H.vue and I.vue. Before, they found 2 callers and 0. On elk, a Nuxt app, const { t } = useI18n() went from no callers to 71.

    Validating on elk turned up two related fixes, included in the same change. A handler like @click="open = false" or @click="emit('close')" no longer links to an unrelated function of the same name. And a component's own function named like a Nuxt helper, such as clearError, is now the one its calls reach.

    This ships in the next release. Re-index after upgrading (codegraph index) to pick it up. Thanks also to @drakeo338 (#2341) and @L0garithmic (#2073), whose work this builds on. Both are credited in the changelog.

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