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

266 lines
8.6 KiB
Go

package languages
import (
"crypto/sha1"
"encoding/hex"
"maps"
"strings"
sitter "github.com/zzet/gortex/internal/parser/tsitter"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/parser"
"github.com/zzet/gortex/internal/routeguard"
)
// goStringContext labels the API position the literal was found in.
// Used as a discriminator on KindString node IDs and on the
// EdgeEmits.Meta["context"] field so analyzers can filter by domain.
type goStringContext string
const (
stringCtxMetric goStringContext = "metric"
stringCtxErrorMsg goStringContext = "error_msg"
stringCtxRoute goStringContext = "route"
stringCtxSQL goStringContext = "sql"
stringCtxLogMessage goStringContext = "log_message"
)
// goMetricMethods is the whitelist of method names where the first
// string-literal argument is taken as the metric name. Limited to
// statsd / dogstatsd-style APIs (which always pass the metric name
// as the first arg). Prometheus needs a separate composite-literal
// extractor (CounterOpts{Name: "..."} etc.) — out of scope here.
//
// Generic method names like Set / Inc / Add are deliberately
// excluded — they appear on too many unrelated types.
var goMetricMethods = map[string]bool{
"Increment": true, // statsd, dogstatsd
"Decrement": true,
"Count": true,
"Gauge": true,
"Histogram": true,
"Distribution": true,
"Timing": true,
"TimeInMilliseconds": true,
"Event": true, // dogstatsd Event
"ServiceCheck": true, // dogstatsd ServiceCheck
}
// goErrorMessageCalls maps (package-or-receiver, function) to the
// "error_msg" context. Both shapes look like selector calls
// syntactically, so we match against the receiver name as a
// heuristic — `errors.New(...)` and `fmt.Errorf(...)` are by far the
// dominant idioms.
var goErrorMessageCalls = map[[2]string]bool{
{"errors", "New"}: true,
{"fmt", "Errorf"}: true,
{"xerrors", "New"}: true, // golang.org/x/xerrors
{"xerrors", "Errorf"}: true,
}
// goRouteMethods is the set of method names that, when called with a
// string-literal path-like first argument, emit a "route" node.
// Mixes net/http (HandleFunc/Handle), gorilla/mux (HandleFunc/Handle),
// chi (Get/Post/...), gin/echo (GET/POST/...), and a handful of
// common router shapes that don't go through the contracts pipeline.
var goRouteMethods = map[string]bool{
"Handle": true,
"HandleFunc": true,
"Get": true,
"Post": true,
"Put": true,
"Delete": true,
"Patch": true,
"Options": true,
"Head": true,
"Connect": true,
"Trace": true,
"GET": true,
"POST": true,
"PUT": true,
"DELETE": true,
"PATCH": true,
"OPTIONS": true,
"HEAD": true,
"CONNECT": true,
"TRACE": true,
}
// goStringEvent is one deferred string-literal observation, queued
// during AST traversal and flushed at end-of-file by emitGoStringEvents.
//
// nodeMeta / edgeMeta are optional; when non-empty their entries are
// merged into the KindString node's Meta and the EdgeEmits' Meta
// respectively. Used to carry domain hints — `dialect` and `tables`
// for SQL strings, `level` for log messages — that downstream
// analyzers (sql_rebuild, log_events, error_surface registry) read
// without re-parsing the value.
type goStringEvent struct {
context goStringContext
method string
value string
line int
nodeMeta map[string]any
edgeMeta map[string]any
}
// detectGoMetric checks a method call against the metric whitelist;
// returns the metric name when arg[0] is a string literal.
func detectGoMetric(callExpr *sitter.Node, method string, src []byte) (string, bool) {
if callExpr == nil {
return "", false
}
if !goMetricMethods[method] {
return "", false
}
return firstStringLiteralArg(callExpr, src)
}
// detectGoErrorMessage checks for errors.New / fmt.Errorf-style calls
// where the first argument is a string literal.
func detectGoErrorMessage(callExpr *sitter.Node, receiver, method string, src []byte) (string, bool) {
if callExpr == nil {
return "", false
}
if !goErrorMessageCalls[[2]string{receiver, method}] {
return "", false
}
return firstStringLiteralArg(callExpr, src)
}
// detectGoRoute checks for HTTP-router shapes where arg[0] is a
// path-like string literal. Path-likeness is enforced (must start
// with "/" or contain a "/" segment) to suppress false positives
// from generic method names like Get/Set on map-like types.
func detectGoRoute(callExpr *sitter.Node, method string, src []byte) (string, bool) {
if callExpr == nil {
return "", false
}
if !goRouteMethods[method] {
return "", false
}
value, ok := firstStringLiteralArg(callExpr, src)
if !ok {
return "", false
}
if !looksLikeRoute(value, method) {
return "", false
}
return value, true
}
// looksLikeRoute is a cheap sanity check — keeps map.Get("foo")
// and similar generics out of the route bucket. Accepts paths that
// start with "/" (most common), "GET /…" / "POST /…" mux-1.22 form,
// or a wildcard segment.
func looksLikeRoute(s, calleeName string) bool {
if s == "" {
return false
}
// The path literal we ultimately classify. For the net/http 1.22+
// "VERB /foo" pattern form, isolate the rooted path portion so the route
// guard sees a literal it can judge ("GET /etc/passwd" must still be
// rejected as a filesystem path, while "GET /users" stays a route).
candidate := s
for _, m := range []string{"GET ", "POST ", "PUT ", "DELETE ", "PATCH ", "OPTIONS ", "HEAD "} {
if strings.HasPrefix(s, m) {
candidate = strings.TrimSpace(s[len(m):])
break
}
}
// Only rooted "/..." literals reach the route bucket; a bare identifier
// (map.Get("foo")) or a verb with no rooted path is not a route.
if !strings.HasPrefix(candidate, "/") {
return false
}
// The shape heuristic accepted the literal — require the route guard to
// agree, so filesystem/config strings stop minting false routes.
return routeguard.IsLikelyHTTPRoute(candidate, calleeName)
}
// firstStringLiteralArg returns the value of the first string-literal
// argument of a call expression, with surrounding quotes stripped. The
// helper is shared with detectGoLogEvent's logic but kept separate so
// each context can apply its own filtering.
func firstStringLiteralArg(callExpr *sitter.Node, src []byte) (string, bool) {
args := callExpr.ChildByFieldName("arguments")
if args == nil {
return "", false
}
for i, _nc := 0, int(args.NamedChildCount()); i < _nc; i++ {
c := args.NamedChild(i)
if c == nil {
continue
}
if c.Type() != "interpreted_string_literal" && c.Type() != "raw_string_literal" {
continue
}
text := strings.Trim(c.Content(src), "\"`")
if text == "" {
return "", false
}
return text, true
}
return "", false
}
// emitGoStringEvents creates one KindString node per (context, value)
// pair seen and an EdgeEmits from the enclosing function/method to
// each. Mirrors emitGoObservabilityEvents — same per-repo dedup
// behaviour, same caller-line lookup contract.
func emitGoStringEvents(events []goStringEvent, callerLookup func(line int) string, filePath string, result *parser.ExtractionResult) {
if len(events) == 0 {
return
}
seen := make(map[string]struct{}, len(events))
for _, e := range events {
callerID := callerLookup(e.line)
if callerID == "" {
continue
}
strID := goStringNodeID(e.context, e.value)
if _, ok := seen[strID]; !ok {
seen[strID] = struct{}{}
nodeMeta := map[string]any{
"context": string(e.context),
"value": e.value,
}
maps.Copy(nodeMeta, e.nodeMeta)
result.Nodes = append(result.Nodes, &graph.Node{
ID: strID,
Kind: graph.KindString,
Name: e.value,
FilePath: filePath, // first sighting; not authoritative
Language: "go",
Meta: nodeMeta,
})
}
edgeMeta := map[string]any{
"context": string(e.context),
"method": e.method,
}
maps.Copy(edgeMeta, e.edgeMeta)
result.Edges = append(result.Edges, &graph.Edge{
From: callerID,
To: strID,
Kind: graph.EdgeEmits,
FilePath: filePath,
Line: e.line,
Origin: graph.OriginASTInferred,
Meta: edgeMeta,
})
}
}
// goStringNodeID composes the canonical synthetic ID for a string
// node. Long values (over 200 chars) are hashed to keep IDs sane —
// the original text is preserved in node.Name and node.Meta["value"].
func goStringNodeID(ctx goStringContext, value string) string {
if len(value) > 200 {
h := sha1.Sum([]byte(value))
return "string::" + string(ctx) + "::sha1:" + hex.EncodeToString(h[:])[:16]
}
return "string::" + string(ctx) + "::" + value
}