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

117 lines
3.3 KiB
Go

package languages
import (
"strings"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/parser"
)
// annotationArgsMaxLen caps the verbatim argument text stored on the
// edge. 200 chars covers route strings, dependency-injection tokens,
// and most decorator literals without bloating large response
// payloads.
const annotationArgsMaxLen = 200
// AnnotationNodeID returns the canonical synthetic annotation node ID
// for a (language, name) pair. The same ID is reused across every
// site that applies the annotation, so `find_usages` on the node
// returns every annotated symbol in one hop. Keep this in sync with
// any consumer that constructs annotation IDs by hand.
func AnnotationNodeID(lang, name string) string {
return "annotation::" + lang + "::" + name
}
// EmitAnnotationEdge appends a synthetic annotation node (idempotent
// per ID) plus an EdgeAnnotated edge from `fromID` to that node.
// `name` is the annotation's bare name (no leading `@`, `#[`, or `[`).
// `args` is the raw inner-parentheses text (e.g. `"/users/:id"` for
// `@Get("/users/:id")`) — pass "" when the annotation has no args.
//
// `seen` deduplicates the synthetic node creation within the current
// extraction pass so multiple files / multiple decorator sites in the
// same file don't each emit a duplicate annotation node. The graph
// layer also dedupes by ID, so missing dedup here is a wasted append
// rather than a correctness bug.
func EmitAnnotationEdge(
fromID, lang, name, args, filePath string,
line int,
result *parser.ExtractionResult,
seen map[string]bool,
) {
if name == "" || fromID == "" {
return
}
annoID := AnnotationNodeID(lang, name)
if !seen[annoID] {
seen[annoID] = true
result.Nodes = append(result.Nodes, &graph.Node{
ID: annoID,
Kind: graph.KindType,
Name: name,
FilePath: filePath,
StartLine: line,
EndLine: line,
Language: lang,
Meta: map[string]any{
"kind": "annotation",
"synthetic": true,
},
})
}
edge := &graph.Edge{
From: fromID,
To: annoID,
Kind: graph.EdgeAnnotated,
FilePath: filePath,
Line: line,
Origin: graph.OriginASTResolved,
}
if trimmed := truncateAnnotationArgs(args); trimmed != "" {
edge.Meta = map[string]any{"args": trimmed}
}
result.Edges = append(result.Edges, edge)
}
// truncateAnnotationArgs trims surrounding whitespace and caps the
// argument text to annotationArgsMaxLen on a rune boundary. Returns
// "" for empty / pure-whitespace input.
func truncateAnnotationArgs(s string) string {
s = strings.TrimSpace(s)
if s == "" {
return ""
}
if len(s) <= annotationArgsMaxLen {
return s
}
cut := annotationArgsMaxLen
for cut > 0 && (s[cut]&0xC0) == 0x80 {
cut--
}
return s[:cut] + "…"
}
// ExtractParenArgs returns the substring between the outermost balanced
// parens in `s`, or "" when there are none. The result excludes the
// parens themselves. Useful when an extractor has the verbatim source
// of a decorator call like `@Get("/users")` and wants just the args.
func ExtractParenArgs(s string) string {
open := strings.IndexByte(s, '(')
if open < 0 {
return ""
}
depth := 0
for i := open; i < len(s); i++ {
switch s[i] {
case '(':
depth++
case ')':
depth--
if depth == 0 {
return s[open+1 : i]
}
}
}
return ""
}