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
400 lines
11 KiB
Go
400 lines
11 KiB
Go
package languages
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
"github.com/zzet/gortex/internal/parser"
|
|
sitter "github.com/zzet/gortex/internal/parser/tsitter"
|
|
)
|
|
|
|
// emitCSharpFunctionShape emits KindParam/EdgeParamOf/EdgeTypedAs/
|
|
// EdgeReturns/KindGenericParam edges for a C# method or constructor
|
|
// declaration. Parameter shapes (`Foo Bar`, `Foo Bar = default`,
|
|
// `params Foo[] xs`, generic `Foo<Bar> baz`) all flow through this
|
|
// helper so DI containers and codegen tooling see dependencies.
|
|
func emitCSharpFunctionShape(ownerID string, methodNode *sitter.Node, src []byte, filePath string, declLine int, result *parser.ExtractionResult) {
|
|
if methodNode == nil {
|
|
return
|
|
}
|
|
if params := methodNode.ChildByFieldName("parameters"); params != nil {
|
|
emitCSharpParamNodes(ownerID, params, src, filePath, declLine, result)
|
|
}
|
|
if rt := csharpReturnTypeRaw(methodNode, src); rt != "" {
|
|
emitCSharpReturnEdges(ownerID, rt, filePath, declLine, result)
|
|
}
|
|
emitCSharpGenericParamNodes(ownerID, methodNode, src, filePath, declLine, result)
|
|
}
|
|
|
|
func emitCSharpParamNodes(ownerID string, params *sitter.Node, src []byte, filePath string, declLine int, result *parser.ExtractionResult) {
|
|
pos := 0
|
|
for i, _nc := 0, int(params.NamedChildCount()); i < _nc; i++ {
|
|
decl := params.NamedChild(i)
|
|
if decl == nil {
|
|
continue
|
|
}
|
|
if decl.Type() != "parameter" {
|
|
continue
|
|
}
|
|
var name, typeRaw string
|
|
variadic := false
|
|
if n := decl.ChildByFieldName("name"); n != nil {
|
|
name = n.Content(src)
|
|
}
|
|
if t := decl.ChildByFieldName("type"); t != nil {
|
|
typeRaw = strings.TrimSpace(t.Content(src))
|
|
}
|
|
// Look for `params` modifier — variadic in C#.
|
|
for j, _nc := 0, int(decl.NamedChildCount()); j < _nc; j++ {
|
|
c := decl.NamedChild(j)
|
|
if c != nil && c.Type() == "parameter_modifier" && strings.Contains(c.Content(src), "params") {
|
|
variadic = true
|
|
break
|
|
}
|
|
}
|
|
if name == "" || name == "_" {
|
|
continue
|
|
}
|
|
paramID := ownerID + "#param:" + name + "@" + strconv.Itoa(pos)
|
|
meta := map[string]any{"position": pos}
|
|
if variadic {
|
|
meta["variadic"] = true
|
|
}
|
|
if typeRaw != "" {
|
|
meta["type"] = typeRaw
|
|
}
|
|
startLine := int(decl.StartPoint().Row) + 1
|
|
if startLine == 0 {
|
|
startLine = declLine
|
|
}
|
|
result.Nodes = append(result.Nodes, &graph.Node{
|
|
ID: paramID,
|
|
Kind: graph.KindParam,
|
|
Name: name,
|
|
FilePath: filePath,
|
|
StartLine: startLine,
|
|
EndLine: int(decl.EndPoint().Row) + 1,
|
|
Language: "csharp",
|
|
Meta: meta,
|
|
})
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: paramID,
|
|
To: ownerID,
|
|
Kind: graph.EdgeParamOf,
|
|
FilePath: filePath,
|
|
Line: startLine,
|
|
Origin: graph.OriginASTResolved,
|
|
})
|
|
if canon := canonicalizeCSharpTypeRef(typeRaw); canon != "" && !isCSharpPrimitive(canon) {
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: paramID,
|
|
To: "unresolved::" + canon,
|
|
Kind: graph.EdgeTypedAs,
|
|
FilePath: filePath,
|
|
Line: startLine,
|
|
Origin: graph.OriginASTInferred,
|
|
})
|
|
}
|
|
pos++
|
|
}
|
|
}
|
|
|
|
func csharpReturnTypeRaw(methodNode *sitter.Node, src []byte) string {
|
|
// In tree-sitter-c-sharp, method_declaration has a `type` field
|
|
// for the return type. Constructors don't (the type is implicitly
|
|
// the enclosing class).
|
|
if rt := methodNode.ChildByFieldName("type"); rt != nil {
|
|
return strings.TrimSpace(rt.Content(src))
|
|
}
|
|
if rt := methodNode.ChildByFieldName("returns"); rt != nil {
|
|
return strings.TrimSpace(rt.Content(src))
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func emitCSharpReturnEdges(ownerID, returnText, filePath string, line int, result *parser.ExtractionResult) {
|
|
if returnText == "" {
|
|
return
|
|
}
|
|
t := canonicalizeCSharpTypeRef(returnText)
|
|
if t == "" || isCSharpPrimitive(t) {
|
|
return
|
|
}
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: ownerID,
|
|
To: "unresolved::" + t,
|
|
Kind: graph.EdgeReturns,
|
|
FilePath: filePath,
|
|
Line: line,
|
|
Origin: graph.OriginASTInferred,
|
|
Meta: map[string]any{
|
|
"position": 0,
|
|
},
|
|
})
|
|
}
|
|
|
|
func emitCSharpGenericParamNodes(ownerID string, methodNode *sitter.Node, src []byte, filePath string, line int, result *parser.ExtractionResult) {
|
|
tparams := methodNode.ChildByFieldName("type_parameters")
|
|
if tparams == nil {
|
|
for i, _nc := 0, int(methodNode.NamedChildCount()); i < _nc; i++ {
|
|
c := methodNode.NamedChild(i)
|
|
if c != nil && c.Type() == "type_parameter_list" {
|
|
tparams = c
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if tparams == nil {
|
|
return
|
|
}
|
|
for i, _nc := 0, int(tparams.NamedChildCount()); i < _nc; i++ {
|
|
tp := tparams.NamedChild(i)
|
|
if tp == nil || tp.Type() != "type_parameter" {
|
|
continue
|
|
}
|
|
var name string
|
|
for j, _nc := 0, int(tp.NamedChildCount()); j < _nc; j++ {
|
|
c := tp.NamedChild(j)
|
|
if c != nil && c.Type() == "identifier" {
|
|
name = c.Content(src)
|
|
break
|
|
}
|
|
}
|
|
if name == "" {
|
|
continue
|
|
}
|
|
gpID := ownerID + "#tparam:" + name
|
|
result.Nodes = append(result.Nodes, &graph.Node{
|
|
ID: gpID,
|
|
Kind: graph.KindGenericParam,
|
|
Name: name,
|
|
FilePath: filePath,
|
|
StartLine: line,
|
|
EndLine: line,
|
|
Language: "csharp",
|
|
})
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: gpID,
|
|
To: ownerID,
|
|
Kind: graph.EdgeMemberOf,
|
|
FilePath: filePath,
|
|
Line: line,
|
|
Origin: graph.OriginASTResolved,
|
|
})
|
|
}
|
|
}
|
|
|
|
func canonicalizeCSharpTypeRef(t string) string {
|
|
t = strings.TrimSpace(t)
|
|
if t == "" {
|
|
return ""
|
|
}
|
|
// Strip nullable suffix `?`.
|
|
t = strings.TrimSuffix(t, "?")
|
|
t = strings.TrimSpace(t)
|
|
// Strip array suffix.
|
|
for strings.HasSuffix(t, "[]") {
|
|
t = strings.TrimSuffix(t, "[]")
|
|
t = strings.TrimSpace(t)
|
|
}
|
|
// Unwrap container generics.
|
|
for _, wrapper := range []string{
|
|
"Task", "ValueTask", "List", "IList", "IEnumerable",
|
|
"ICollection", "IReadOnlyList", "IReadOnlyCollection",
|
|
"IAsyncEnumerable", "Nullable", "Span", "ReadOnlySpan",
|
|
} {
|
|
prefix := wrapper + "<"
|
|
if strings.HasPrefix(t, prefix) && strings.HasSuffix(t, ">") {
|
|
inner := t[len(prefix) : len(t)-1]
|
|
return canonicalizeCSharpTypeRef(inner)
|
|
}
|
|
}
|
|
if idx := strings.Index(t, "<"); idx > 0 {
|
|
t = t[:idx]
|
|
}
|
|
if idx := strings.LastIndex(t, "."); idx >= 0 {
|
|
t = t[idx+1:]
|
|
}
|
|
return strings.TrimSpace(t)
|
|
}
|
|
|
|
func isCSharpPrimitive(t string) bool {
|
|
switch t {
|
|
case "", "var", "void", "bool", "byte", "sbyte", "short", "ushort",
|
|
"int", "uint", "long", "ulong", "float", "double", "decimal",
|
|
"char", "string", "object", "dynamic":
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// emitCSharpTypeUseEdges emits one EdgeTypedAs from ownerID to the bare
|
|
// named type used in a variable / field / property annotation. The type
|
|
// is canonicalised to its simple named form (nullable `T?`, arrays
|
|
// `T[]`, and container generics like `List<T>` / `Task<T>` are unwrapped
|
|
// by canonicalizeCSharpTypeRef) and primitives / `var` are skipped so
|
|
// the graph isn't flooded with unresolved::int / unresolved::var edges
|
|
// that never land. Edges ride at OriginASTInferred — the binding is a
|
|
// tree-sitter inference, not an LSP-checked fact — so a type that a local
|
|
// declaration uses (`HttpResponse resp = Get();`) becomes a traversable
|
|
// reference even without a language server.
|
|
func emitCSharpTypeUseEdges(ownerID, typeText, filePath string, line int, result *parser.ExtractionResult) {
|
|
if ownerID == "" {
|
|
return
|
|
}
|
|
canon := canonicalizeCSharpTypeRef(typeText)
|
|
if canon == "" || isCSharpPrimitive(canon) {
|
|
return
|
|
}
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: ownerID,
|
|
To: "unresolved::" + canon,
|
|
Kind: graph.EdgeTypedAs,
|
|
FilePath: filePath,
|
|
Line: line,
|
|
Origin: graph.OriginASTInferred,
|
|
})
|
|
}
|
|
|
|
// emitCSharpAsyncSpawns walks a C# method body for `await` expressions
|
|
// and Task.Run / Task.Factory.StartNew / ThreadPool.QueueUserWorkItem
|
|
// calls. Mode is "async" for await, "task" for Task.Run, "thread" for
|
|
// thread-pool queues.
|
|
func emitCSharpAsyncSpawns(ownerID string, body *sitter.Node, src []byte, filePath string, result *parser.ExtractionResult) {
|
|
if body == nil {
|
|
return
|
|
}
|
|
seen := map[string]bool{}
|
|
emit := func(target, mode string, line int) {
|
|
if target == "" {
|
|
return
|
|
}
|
|
key := mode + "\x00" + target
|
|
if seen[key] {
|
|
return
|
|
}
|
|
seen[key] = true
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: ownerID,
|
|
To: "unresolved::" + target,
|
|
Kind: graph.EdgeSpawns,
|
|
FilePath: filePath,
|
|
Line: line,
|
|
Origin: graph.OriginASTInferred,
|
|
Meta: map[string]any{
|
|
"mode": mode,
|
|
},
|
|
})
|
|
}
|
|
walkCSharpNodes(body, func(n *sitter.Node) bool {
|
|
switch n.Type() {
|
|
case "method_declaration", "lambda_expression", "anonymous_method_expression",
|
|
"local_function_statement":
|
|
return false
|
|
case "await_expression":
|
|
line := int(n.StartPoint().Row) + 1
|
|
// Look for an inner invocation_expression to grab the
|
|
// callee name.
|
|
for i, _nc := 0, int(n.NamedChildCount()); i < _nc; i++ {
|
|
c := n.NamedChild(i)
|
|
if c == nil {
|
|
continue
|
|
}
|
|
if c.Type() == "invocation_expression" {
|
|
if name := csharpInvocationTargetName(c, src); name != "" {
|
|
emit(name, "async", line)
|
|
}
|
|
}
|
|
}
|
|
case "invocation_expression":
|
|
fn := n.ChildByFieldName("function")
|
|
if fn == nil {
|
|
return true
|
|
}
|
|
line := int(n.StartPoint().Row) + 1
|
|
if fn.Type() == "member_access_expression" {
|
|
expr := fn.ChildByFieldName("expression")
|
|
name := fn.ChildByFieldName("name")
|
|
if expr != nil && name != nil {
|
|
obj := expr.Content(src)
|
|
meth := name.Content(src)
|
|
switch obj {
|
|
case "Task":
|
|
switch meth {
|
|
case "Run", "Factory":
|
|
emit("Task."+meth, "task", line)
|
|
}
|
|
case "Task.Factory":
|
|
if meth == "StartNew" {
|
|
emit("Task.Factory.StartNew", "task", line)
|
|
}
|
|
case "ThreadPool":
|
|
if meth == "QueueUserWorkItem" {
|
|
emit("ThreadPool.QueueUserWorkItem", "thread", line)
|
|
}
|
|
case "Parallel":
|
|
switch meth {
|
|
case "ForEach", "For", "Invoke":
|
|
emit("Parallel."+meth, "parallel", line)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
|
|
func walkCSharpNodes(n *sitter.Node, visit func(*sitter.Node) bool) {
|
|
if n == nil {
|
|
return
|
|
}
|
|
if !visit(n) {
|
|
return
|
|
}
|
|
for i, _nc := 0, int(n.NamedChildCount()); i < _nc; i++ {
|
|
walkCSharpNodes(n.NamedChild(i), visit)
|
|
}
|
|
}
|
|
|
|
func csharpInvocationTargetName(call *sitter.Node, src []byte) string {
|
|
fn := call.ChildByFieldName("function")
|
|
if fn == nil {
|
|
return ""
|
|
}
|
|
switch fn.Type() {
|
|
case "identifier":
|
|
return fn.Content(src)
|
|
case "member_access_expression":
|
|
if name := fn.ChildByFieldName("name"); name != nil {
|
|
return name.Content(src)
|
|
}
|
|
case "generic_name":
|
|
if name := fn.ChildByFieldName("name"); name != nil {
|
|
return name.Content(src)
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// csharpFunctionBody returns the body block of a C# method
|
|
// declaration. Expression-bodied methods (`=> expr`) have a different
|
|
// shape that we don't walk (no spawn-like calls in idiomatic
|
|
// expression bodies).
|
|
func csharpFunctionBody(methodNode *sitter.Node) *sitter.Node {
|
|
if methodNode == nil {
|
|
return nil
|
|
}
|
|
if b := methodNode.ChildByFieldName("body"); b != nil {
|
|
return b
|
|
}
|
|
for i, _nc := 0, int(methodNode.NamedChildCount()); i < _nc; i++ {
|
|
c := methodNode.NamedChild(i)
|
|
if c != nil && c.Type() == "block" {
|
|
return c
|
|
}
|
|
}
|
|
return nil
|
|
}
|