Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

241 lines
8.5 KiB
Go

package languages
import (
"regexp"
"strings"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/parser"
)
// Framework-provided-identifier suppression and template-handler mining for the
// SFC frameworks (Vue/Nuxt, Svelte/SvelteKit, Astro). The script blocks are
// delegated to the TS/JS extractor, which emits a bare-call edge for every
// compiler macro / auto-import the framework provides (defineProps, useFetch,
// $state, …). Those have no user definition, so they would linger as unresolved
// edges (noise) or, worse, mis-bind to an unrelated user symbol of the same
// name. Dropping them keeps the component's call graph precise — and a real,
// user-defined same-named symbol still resolves and is left intact.
func frameworkIdentSet(names ...string) map[string]bool {
m := make(map[string]bool, len(names))
for _, n := range names {
m[n] = true
}
return m
}
var frameworkSuppressionSets = map[string]map[string]bool{
"vue": frameworkIdentSet(
// Vue compiler macros.
"defineProps", "defineEmits", "defineExpose", "defineOptions",
"defineModel", "defineSlots", "withDefaults", "useAttrs", "useSlots",
// Nuxt auto-imports.
"useFetch", "useAsyncData", "useLazyFetch", "useState", "useRoute",
"useRouter", "useRuntimeConfig", "useHead", "useSeoMeta", "useNuxtApp",
"useCookie", "useRequestHeaders", "navigateTo", "definePageMeta",
"defineNuxtConfig", "defineEventHandler",
),
"svelte": frameworkIdentSet(
// Svelte 5 runes.
"$state", "$derived", "$effect", "$props", "$bindable", "$inspect", "$host",
// Svelte 3/4 lifecycle / store helpers.
"onMount", "onDestroy", "beforeUpdate", "afterUpdate", "tick",
),
"astro": frameworkIdentSet("Astro", "Fragment"),
}
// frameworkImportPrefixes are the virtual module roots a framework injects;
// imports from them are framework-provided, not user files.
var frameworkImportPrefixes = map[string][]string{
"svelte": {"$app/", "$env/", "$lib/", "svelte", "@sveltejs/"},
"vue": {"#imports", "#app", "nuxt/"},
"astro": {"astro:", "#imports"},
}
// suppressFrameworkIdents drops the unresolved macro/auto-import edges a
// delegated script produced for lang. Only unresolved targets are touched, so a
// user symbol that genuinely matches the name keeps its resolved edge.
func suppressFrameworkIdents(result *parser.ExtractionResult, lang string) {
set := frameworkSuppressionSets[lang]
prefixes := frameworkImportPrefixes[lang]
if len(set) == 0 && len(prefixes) == 0 {
return
}
kept := result.Edges[:0]
for _, e := range result.Edges {
if e == nil {
continue
}
switch e.Kind {
case graph.EdgeCalls, graph.EdgeReferences, graph.EdgeInstantiates, graph.EdgeReads:
if graph.IsUnresolvedTarget(e.To) && set[graph.UnresolvedName(e.To)] {
continue // framework macro — suppress
}
case graph.EdgeImports:
if graph.IsUnresolvedTarget(e.To) {
name := graph.UnresolvedName(e.To)
if hasAnyPrefix(name, prefixes) {
continue // framework virtual module — suppress
}
}
}
kept = append(kept, e)
}
result.Edges = kept
}
func hasAnyPrefix(s string, prefixes []string) bool {
for _, p := range prefixes {
if strings.HasPrefix(s, p) {
return true
}
}
return false
}
var (
// vueHandlerRe matches a Vue template event binding `@click="onClick"` or
// `v-on:click="onClick"`. Group 2 is the handler identifier.
vueHandlerRe = regexp.MustCompile(`(?:@|v-on:)[\w-]+\s*=\s*"(\w+)`)
// svelteHandlerRe matches a Svelte template binding `on:click={onClick}` or
// the Svelte 5 `onclick={onClick}` attribute form. Group 1 is the handler.
svelteHandlerRe = regexp.MustCompile(`on:?[\w-]+\s*=\s*\{\s*(\w+)`)
// composableDestructureRe matches a script-setup destructure of a Vue/Nuxt
// composable's return — `const { close, open: o } = useModal()`. Group 1 is
// the destructure body, group 2 the `use*` composable. Only `use`-prefixed
// callees qualify (the composable convention), keeping the binding precise.
composableDestructureRe = regexp.MustCompile(`(?:const|let|var)\s*\{([^}]*)\}\s*=\s*(use[A-Z]\w*)\s*\(`)
)
// composableBinding records a template handler that is a composable-destructured
// local: the original member it aliases and the composable it came from.
type composableBinding struct {
original string
composable string
}
// composableHandlerBindings maps each local name destructured from a `use*`
// composable to its original member + composable, so a template handler that
// references the (possibly renamed) local resolves to the composable member
// instead of an unresolved bare name. `const { close: c } = useModal()` maps
// c -> {original: "close", composable: "useModal"}.
func composableHandlerBindings(src []byte) map[string]composableBinding {
out := map[string]composableBinding{}
for _, m := range composableDestructureRe.FindAllSubmatch(src, -1) {
composable := string(m[2])
for _, part := range strings.Split(string(m[1]), ",") {
part = strings.TrimSpace(part)
if part == "" || strings.HasPrefix(part, "...") {
continue
}
// Drop a default value (`open = false`).
if i := strings.IndexByte(part, '='); i >= 0 {
part = strings.TrimSpace(part[:i])
}
orig, local := part, part
if i := strings.IndexByte(part, ':'); i >= 0 { // rename `orig: local`
orig = strings.TrimSpace(part[:i])
local = strings.TrimSpace(part[i+1:])
}
if !isSimpleJSIdent(local) || !isSimpleJSIdent(orig) {
continue
}
if _, ok := out[local]; !ok {
out[local] = composableBinding{original: orig, composable: composable}
}
}
}
return out
}
// isSimpleJSIdent reports whether s is a bare JS identifier (letters, digits,
// `_`, `$`; not starting with a digit).
func isSimpleJSIdent(s string) bool {
if s == "" {
return false
}
for i := 0; i < len(s); i++ {
c := s[i]
switch {
case c == '_' || c == '$':
case c >= 'a' && c <= 'z', c >= 'A' && c <= 'Z':
case c >= '0' && c <= '9':
if i == 0 {
return false
}
default:
return false
}
}
return true
}
// mineTemplateHandlers emits a callback reference edge from the component to
// each event handler bound in its markup (@click / v-on / on:click), resolved
// to the in-script function so "what handles this event" and find_usages reach
// the handler — the template→code link a script-only scanner misses.
func mineTemplateHandlers(src []byte, filePath, componentID, lang string, result *parser.ExtractionResult) {
var re *regexp.Regexp
switch lang {
case "svelte":
re = svelteHandlerRe
case "astro":
return // Astro markup uses framework client directives, not @click.
default:
re = vueHandlerRe
}
funcByName := map[string]string{}
for _, n := range result.Nodes {
if n.Kind == graph.KindFunction || n.Kind == graph.KindMethod {
if _, ok := funcByName[n.Name]; !ok {
funcByName[n.Name] = n.ID
}
}
}
// Composable-destructured handlers (`const { close: c } = useModal()` +
// `@click="c"`) bind to the composable member, not a top-level function.
bindings := composableHandlerBindings(src)
tmpl := blankTemplateRegions(src, lang)
seen := map[string]bool{}
for _, m := range re.FindAllSubmatchIndex(tmpl, -1) {
handler := string(tmpl[m[2]:m[3]])
if handler == "" || seen[handler] {
continue
}
seen[handler] = true
to := "unresolved::" + handler
origin := graph.OriginTextMatched
meta := map[string]any{"ref_context": graph.RefContextCallback, "via": "template_handler"}
if id, ok := funcByName[handler]; ok {
to = id
origin = graph.OriginASTResolved
} else if b, ok := bindings[handler]; ok {
// Reference the composable's original member so the binding is
// traceable (`@click="c"` where c aliases useModal().close).
to = "unresolved::" + b.original
meta["via"] = "composable_handler"
meta["composable"] = b.composable
}
result.Edges = append(result.Edges, &graph.Edge{
From: componentID, To: to, Kind: graph.EdgeReferences,
FilePath: filePath, Line: 1 + strings.Count(string(tmpl[:m[0]]), "\n"),
Origin: origin,
Meta: meta,
})
}
}
// applyFrameworkTemplatePasses runs the suppression + handler-mining passes
// shared by the Vue/Svelte/Astro extractors after their scripts are delegated.
func applyFrameworkTemplatePasses(src []byte, filePath, componentID, lang string, result *parser.ExtractionResult) {
// Mine markup `{expr}` calls first, so the suppression pass can drop any
// rune/macro calls (`$state(...)`) it surfaced before they linger as edges.
mineTemplateExpressionCalls(src, filePath, componentID, lang, result)
suppressFrameworkIdents(result, lang)
mineTemplateHandlers(src, filePath, componentID, lang, result)
}