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
266 lines
8.6 KiB
Go
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
|
|
}
|