chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user