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
117 lines
3.3 KiB
Go
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 ""
|
|
}
|