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
201 lines
6.7 KiB
Go
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
|
|
}
|