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

216 lines
8.5 KiB
Go

package graph
import "strings"
// ConcurrencyAnnotation carries two cheap, high-signal concurrency
// facts about a symbol — typically a caller surfaced by a navigation
// query. Both flags default to false; the *Why fields are populated
// only when the matching flag is true, so a zero-value annotation is
// safe to attach unconditionally and to omit from a response.
type ConcurrencyAnnotation struct {
// SyncGuarded is true when the symbol is a method (or a closure
// defined on a method-bearing type) whose receiver / parent type
// holds a lock — a sync.Mutex / sync.RWMutex field in Go, a Mutex
// / RwLock field in Rust, a ReentrantLock field in Java, a
// SemaphoreSlim-typed field in C#. Calls made from inside such a
// type are presumptively lock-protected, which matters when
// reasoning about whether a change is concurrency-safe.
SyncGuarded bool `json:"sync_guarded,omitempty"`
SyncGuardedWhy string `json:"sync_guarded_why,omitempty"`
// CrossConcurrent is true when the symbol is launched across a
// concurrency boundary — it is the target of an EdgeSpawns edge,
// i.e. a `go` statement / goroutine closure in Go, an async / Promise
// / worker entry in JS-TS, a threading.Thread / async def in
// Python, a spawned thread in Rust. Any call the symbol makes runs
// on a different goroutine / thread than its lexical parent.
CrossConcurrent bool `json:"cross_concurrent,omitempty"`
CrossConcurrentWhy string `json:"cross_concurrent_why,omitempty"`
}
// Any reports whether either concurrency flag is set. Callers use it
// to decide whether an annotation is worth attaching / serialising.
func (c ConcurrencyAnnotation) Any() bool {
return c.SyncGuarded || c.CrossConcurrent
}
// ClassifyConcurrency derives the concurrency-safety annotation for a
// node. It reads through the Reader contract — so it works unchanged
// against a base graph or a per-session overlay view — and does no
// parser work: it reuses substrate that existing extractors already
// emit:
//
// - EdgeMemberOf links a method / field / closure to its parent
// type or enclosing function.
// - EdgeSpawns links a caller to a function / closure it launches
// asynchronously (goroutine, async, promise, worker pool).
// - A field node carries Meta["field_type"] with the verbatim
// declared type text.
//
// Language coverage:
//
// - sync_guarded relies on typed field nodes. Go, Rust, Java, and C#
// emit KindField nodes with Meta["field_type"], so a lock-holding
// receiver type is detected for those languages. TypeScript and
// PHP model class properties as KindVariable without a typed
// field-type, and Python does not materialise instance attributes
// as nodes at all — sync_guarded is therefore not reported for
// those languages (it stays false rather than guessing).
// - cross_concurrent relies only on EdgeSpawns and so covers every
// language whose extractor emits spawn edges (Go full; TS / Python
// / Rust / Kotlin / C# for the spawn patterns they detect).
//
// An unknown / missing node yields a zero-value annotation.
func ClassifyConcurrency(r Reader, nodeID string) ConcurrencyAnnotation {
var ann ConcurrencyAnnotation
if r == nil || r.GetNode(nodeID) == nil {
return ann
}
if why := spawnedAsConcurrent(r, nodeID); why != "" {
ann.CrossConcurrent = true
ann.CrossConcurrentWhy = why
}
if field, typeName := receiverLockField(r, r.GetNode(nodeID)); field != "" {
ann.SyncGuarded = true
ann.SyncGuardedWhy = "receiver type " + typeName +
" holds a lock (" + field + "); calls from here are presumptively lock-protected"
}
return ann
}
// spawnedAsConcurrent returns a human-readable explanation when the
// node is the target of at least one EdgeSpawns edge, and "" otherwise.
// The explanation names the spawn mode (goroutine / async / promise /
// worker_pool) recorded on the edge's Meta when available.
func spawnedAsConcurrent(r Reader, nodeID string) string {
for _, e := range r.GetInEdges(nodeID) {
if e.Kind != EdgeSpawns {
continue
}
mode, _ := e.Meta["mode"].(string)
switch mode {
case "goroutine":
return "launched as a goroutine — runs on a different goroutine than its caller"
case "async":
return "launched as an async task — runs off the caller's synchronous path"
case "promise":
return "launched inside a promise — runs off the caller's synchronous path"
case "worker_pool":
return "dispatched to a worker pool — runs on a pool thread, not the caller's"
default:
return "launched across a concurrency boundary (spawned), not called synchronously"
}
}
return ""
}
// receiverLockField finds the parent / receiver type of a method (or a
// closure whose enclosing scope is a method-bearing type) and reports
// the first lock-typed field declared on that type. Returns the field
// name and the type name, or ("", "") when the node is not a method,
// has no resolvable receiver type, or the type holds no lock.
func receiverLockField(r Reader, n *Node) (field, typeName string) {
if n == nil || (n.Kind != KindMethod && n.Kind != KindClosure) {
return "", ""
}
// A method (or closure) reaches its owner through EdgeMemberOf.
// For a closure the owner is usually the enclosing function, not a
// type — receiverTypeOf walks one extra hop in that case so a
// closure defined inside a method still resolves to the method's
// receiver type.
typeNode := receiverTypeOf(r, n)
if typeNode == nil {
return "", ""
}
// A type's fields point at it via EdgeMemberOf; walk the inbound
// member_of edges and inspect each field's declared type.
for _, e := range r.GetInEdges(typeNode.ID) {
if e.Kind != EdgeMemberOf {
continue
}
fn := r.GetNode(e.From)
if fn == nil || fn.Kind != KindField {
continue
}
ft, _ := fn.Meta["field_type"].(string)
if isLockTypeName(ft) {
return fn.Name, typeNode.Name
}
}
return "", ""
}
// receiverTypeOf resolves the type a method / closure belongs to. A
// method points straight at its receiver type via EdgeMemberOf. A
// closure points at its enclosing function / method; when that owner
// is itself a method the walk takes one more EdgeMemberOf hop so a
// closure spawned inside a method is still attributed to the method's
// receiver type. Returns nil when no KindType / KindInterface owner is
// reachable within those two hops.
func receiverTypeOf(r Reader, n *Node) *Node {
for _, e := range r.GetOutEdges(n.ID) {
if e.Kind != EdgeMemberOf {
continue
}
owner := r.GetNode(e.To)
if owner == nil {
continue
}
if owner.Kind == KindType || owner.Kind == KindInterface {
return owner
}
// Closure → enclosing method → receiver type (second hop).
if n.Kind == KindClosure && owner.Kind == KindMethod {
for _, e2 := range r.GetOutEdges(owner.ID) {
if e2.Kind != EdgeMemberOf {
continue
}
t := r.GetNode(e2.To)
if t != nil && (t.Kind == KindType || t.Kind == KindInterface) {
return t
}
}
}
}
return nil
}
// isLockTypeName reports whether a declared field-type string names a
// mutual-exclusion primitive. Matching is on the trailing type name so
// package / module qualifiers (`sync.Mutex`, `tokio::sync::Mutex`,
// `std::sync::RwLock`, `java.util.concurrent.locks.ReentrantLock`) and
// a single leading pointer / reference marker do not defeat it.
// Recognised across Go, Rust, Java, and C# — the languages whose
// extractors emit typed KindField nodes.
func isLockTypeName(fieldType string) bool {
t := strings.TrimSpace(fieldType)
if t == "" {
return false
}
t = strings.TrimPrefix(t, "*") // Go pointer-to-mutex
t = strings.TrimPrefix(t, "&") // Rust reference
// Drop a generic parameter list — Rust's Mutex<T> / RwLock<T>,
// C#'s lock wrappers — so the bare type name is what we test.
if i := strings.IndexByte(t, '<'); i >= 0 {
t = t[:i]
}
// Reduce a qualified path to its trailing segment.
for _, sep := range []string{"::", "."} {
if i := strings.LastIndex(t, sep); i >= 0 {
t = t[i+len(sep):]
}
}
switch strings.ToLower(strings.TrimSpace(t)) {
case "mutex", "rwmutex", // Go sync.Mutex / sync.RWMutex
"rwlock", // Rust std::sync::RwLock
"reentrantlock", // Java ReentrantLock
"reentrantreadwritelock", // Java ReentrantReadWriteLock
"readwritelock", "lock", // Java Lock / ReadWriteLock interfaces
"semaphore", // Java / C# Semaphore
"semaphoreslim", // C# SemaphoreSlim
"readerwriterlockslim", // C# ReaderWriterLockSlim
"spinlock": // Rust spin::Mutex-style / C# SpinLock
return true
}
return false
}