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

201 lines
6.7 KiB
Go

package contracts
import (
"fmt"
"regexp"
"strings"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/parser"
)
// C# ASP.NET attribute-routing prefix-join. A controller's class-level
// `[Route("api/[controller]")]` is the shared prefix of every action route, and
// a method may declare a verb-less `[Route("search")]` (a route with no HTTP-verb
// constraint). The per-line httpPatterns scan sees method routes in isolation, so
// this pass supplies the class context: it joins the controller prefix onto each
// RELATIVE method template (an absolute `/...` or `~/...` template ignores the
// prefix, per ASP.NET) and materialises a contract for each verb-less route.
var (
csharpRouteAttrRe = regexp.MustCompile(`\[Route\(\s*"([^"]+)"\s*\)\]`)
csharpHTTPVerbAttrRe = regexp.MustCompile(`\[Http(?:Get|Post|Put|Delete|Patch|Head|Options)\b`)
csharpClassDeclRe = regexp.MustCompile(`\bclass\s+(\w+)`)
)
// csController is a C# controller class's resolved class-level route prefix and
// line span, used to prefix-join its method routes.
type csController struct {
prefix string
startLine int
endLine int
}
// csVerblessRoute is a method-level `[Route("...")]` with no HTTP-verb attribute
// in the same attribute block -- an ASP.NET route whose verb is unconstrained.
type csVerblessRoute struct {
template string
line int
}
// csharpScanControllerRoutes walks the C# source once, returning each
// controller class's class-level [Route(...)] prefix (with [controller]
// expanded) and each method-level verb-less [Route("...")] site.
func csharpScanControllerRoutes(lines []string, fileNodes []*graph.Node) ([]csController, []csVerblessRoute) {
var controllers []csController
var verbless []csVerblessRoute
pendingRoute := ""
pendingVerb := false
blockLine := -1
reset := func() { pendingRoute, pendingVerb, blockLine = "", false, -1 }
for i, raw := range lines {
line := strings.TrimSpace(raw)
if line == "" {
continue
}
if m := csharpRouteAttrRe.FindStringSubmatch(line); m != nil {
pendingRoute = m[1]
if blockLine < 0 {
blockLine = i
}
continue
}
if csharpHTTPVerbAttrRe.MatchString(line) {
pendingVerb = true
if blockLine < 0 {
blockLine = i
}
continue
}
if strings.HasPrefix(line, "[") {
if blockLine < 0 {
blockLine = i
}
continue
}
// A declaration (or other code) terminates the attribute block.
if cm := csharpClassDeclRe.FindStringSubmatch(line); cm != nil {
if pendingRoute != "" {
prefix := csharpExpandRouteTokens(pendingRoute, cm[1], "")
start, end := csharpClassSpan(cm[1], fileNodes, i+1, len(lines))
controllers = append(controllers, csController{prefix: prefix, startLine: start, endLine: end})
}
} else if pendingRoute != "" && !pendingVerb && strings.Contains(line, "(") {
ln := blockLine
if ln < 0 {
ln = i
}
verbless = append(verbless, csVerblessRoute{template: pendingRoute, line: ln + 1})
}
reset()
}
return controllers, verbless
}
// csharpClassSpan returns the 1-based [start,end] line span of a class -- from
// fileNodes when present, else a fallback of the declaration line to EOF.
func csharpClassSpan(name string, fileNodes []*graph.Node, declLine, total int) (int, int) {
for _, n := range fileNodes {
if n != nil && n.Name == name && n.Kind == graph.KindType {
return n.StartLine, n.EndLine
}
}
return declLine, total
}
// csharpExpandRouteTokens resolves ASP.NET route tokens: [controller] -> the
// controller name minus a trailing "Controller", lower-cased; [action] -> the
// action (method) name, lower-cased. Tokens are matched case-insensitively.
func csharpExpandRouteTokens(tmpl, controller, action string) string {
out := csharpReplaceToken(tmpl, "controller", strings.ToLower(strings.TrimSuffix(controller, "Controller")))
if action != "" {
out = csharpReplaceToken(out, "action", strings.ToLower(action))
}
return out
}
// csharpReplaceToken replaces every case-insensitive `[token]` in s with val.
func csharpReplaceToken(s, token, val string) string {
needle := "[" + token + "]"
for {
lower := strings.ToLower(s)
idx := strings.Index(lower, needle)
if idx < 0 {
return s
}
s = s[:idx] + val + s[idx+len(needle):]
}
}
// csharpJoinControllerRoute prefix-joins a controller's class-level route onto a
// method's RELATIVE template. A template that is absolute (starts with "/" or
// "~/") ignores the controller prefix, per ASP.NET routing.
func csharpJoinControllerRoute(path string, controllers []csController, line int, action string) string {
if strings.HasPrefix(path, "/") || strings.HasPrefix(path, "~/") {
return path
}
prefix := csharpControllerPrefixAt(controllers, line)
if prefix == "" {
return path
}
prefix = csharpReplaceToken(prefix, "action", strings.ToLower(action))
return strings.TrimRight(prefix, "/") + "/" + strings.TrimLeft(path, "/")
}
// csharpControllerPrefixAt returns the class-level route prefix of the innermost
// controller whose span contains line, or "" when none.
func csharpControllerPrefixAt(controllers []csController, line int) string {
best := ""
bestStart := -1
for _, c := range controllers {
if line >= c.startLine && line <= c.endLine && c.startLine > bestStart {
bestStart = c.startLine
best = c.prefix
}
}
return best
}
// csharpActionName returns the bare method name of the symbol enclosing line.
func csharpActionName(fileNodes []*graph.Node, line int) string {
id := findEnclosingSymbol(fileNodes, line)
if i := strings.LastIndex(id, "::"); i >= 0 {
id = id[i+2:]
}
if i := strings.LastIndex(id, "."); i >= 0 {
return id[i+1:]
}
return id
}
// csharpVerblessContracts materialises a route Contract (verb "ANY") for each
// verb-less method-level [Route("...")], prefix-joined to its controller.
func (h *HTTPExtractor) csharpVerblessContracts(filePath string, lines []string, fileNodes []*graph.Node, controllers []csController, verbless []csVerblessRoute, lang string, tree *parser.ParseTree) []Contract {
var out []Contract
for _, vr := range verbless {
path := csharpJoinControllerRoute(vr.template, controllers, vr.line, csharpActionName(fileNodes, vr.line))
normPath, origNames := NormalizeHTTPPathWithParams(path)
method := "ANY"
c := Contract{
ID: fmt.Sprintf("http::%s::%s", method, normPath),
Type: ContractHTTP,
Role: RoleProvider,
SymbolID: findEnclosingSymbol(fileNodes, vr.line),
FilePath: filePath,
Line: vr.line,
Meta: map[string]any{
"method": method,
"path": normPath,
"framework": "aspnet",
},
Confidence: 0.9,
}
if len(origNames) > 0 {
c.Meta["path_param_names"] = origNames
}
EnrichHTTPContractWithTree(&c, lines, fileNodes, lang, tree)
out = append(out, c)
}
return out
}