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

206 lines
7.3 KiB
Go

package contracts
import (
"fmt"
"regexp"
"strings"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/parser"
)
// Rust route extraction for the builder/chained forms the per-line httpPatterns
// table cannot express:
//
// - chained Axum methods: `.route("/u", get(list).post(create))` — one
// Contract per HTTP method, each bound to its own handler;
// - the Actix builder: `web::resource("/u").route(web::get().to(list))`,
// with `web::scope("/api")` prefixing the resources nested inside it.
//
// Axum `.nest("/api", router)` prefixing is already handled by route_prefix.go
// (framework "axum"); Actix scopes nest resources inline, so the prefix is
// joined here from the enclosing-call region of each `web::scope`.
var (
// rustAxumRouteHeadRE matches the start of an axum `.route("/path", …)`
// call. Group 1 is the path; the builder chain follows the comma.
rustAxumRouteHeadRE = regexp.MustCompile(`\.route\(\s*"([^"]+)"\s*,`)
// rustMethodHandlerRE matches a `method(handler)` in an axum builder
// chain — `get(list)`, `post(create)`.
rustMethodHandlerRE = regexp.MustCompile(`\b(get|post|put|delete|patch|head|options)\s*\(\s*(\w+)`)
// rustActixResourceRE matches `web::resource("/path")`.
rustActixResourceRE = regexp.MustCompile(`web::resource\(\s*"([^"]+)"\s*\)`)
// rustActixRouteRE matches `.route(web::get().to(handler))` /
// `.to_async(handler)` inside an Actix resource chain.
rustActixRouteRE = regexp.MustCompile(`\.route\(\s*web::(get|post|put|delete|patch|head|options)\(\)\s*\.to(?:_async)?\(\s*(\w+)`)
// rustActixScopeRE matches `web::scope("/prefix")`.
rustActixScopeRE = regexp.MustCompile(`web::scope\(\s*"([^"]+)"\s*\)`)
)
// extractRustRoutes emits the chained-Axum and Actix-builder route Contracts.
func (h *HTTPExtractor) extractRustRoutes(filePath, text string, lines []string, fileNodes []*graph.Node, lang string, tree *parser.ParseTree) []Contract {
var out []Contract
out = append(out, h.extractAxumChainedRoutes(filePath, text, lines, fileNodes, lang, tree)...)
out = append(out, h.extractActixBuilderRoutes(filePath, text, lines, fileNodes, lang, tree)...)
return out
}
// extractAxumChainedRoutes scans every `.route("/path", <chain>)` for each
// `method(handler)` in its builder chain, emitting one Contract per method.
func (h *HTTPExtractor) extractAxumChainedRoutes(filePath, text string, lines []string, fileNodes []*graph.Node, lang string, tree *parser.ParseTree) []Contract {
var out []Contract
for _, head := range rustAxumRouteHeadRE.FindAllStringSubmatchIndex(text, -1) {
path := text[head[2]:head[3]]
open := strings.IndexByte(text[head[0]:], '(')
if open < 0 {
continue
}
open += head[0]
closeIdx := rustMatchingParen(text, open)
if closeIdx < 0 || head[1] > closeIdx {
continue
}
chain := text[head[1]:closeIdx]
lineNum := lineAtOffset(lines, head[0])
seen := map[string]bool{}
for _, mh := range rustMethodHandlerRE.FindAllStringSubmatchIndex(chain, -1) {
method := strings.ToUpper(chain[mh[2]:mh[3]])
handler := chain[mh[4]:mh[5]]
if seen[method] {
continue
}
seen[method] = true
out = append(out, h.buildRustRoute(filePath, lines, fileNodes, lang, tree, method, path, handler, lineNum, "axum"))
}
}
return out
}
// extractActixBuilderRoutes emits one Contract per `.route(web::method().to(h))`
// of every `web::resource("/path")`, joining any enclosing `web::scope` prefix.
func (h *HTTPExtractor) extractActixBuilderRoutes(filePath, text string, lines []string, fileNodes []*graph.Node, lang string, tree *parser.ParseTree) []Contract {
scopes := rustActixScopes(text)
var out []Contract
locs := rustActixResourceRE.FindAllStringSubmatchIndex(text, -1)
for i, loc := range locs {
path := rustJoinPath(rustScopePrefix(scopes, loc[0]), text[loc[2]:loc[3]])
regionEnd := len(text)
if i+1 < len(locs) {
regionEnd = locs[i+1][0]
}
region := text[loc[1]:regionEnd]
lineNum := lineAtOffset(lines, loc[0])
for _, mh := range rustActixRouteRE.FindAllStringSubmatch(region, -1) {
method := strings.ToUpper(mh[1])
out = append(out, h.buildRustRoute(filePath, lines, fileNodes, lang, tree, method, path, mh[2], lineNum, "actix"))
}
}
return out
}
// buildRustRoute assembles a route Contract, binding the handler identifier to
// its same-file function and running the body schema enricher.
func (h *HTTPExtractor) buildRustRoute(filePath string, lines []string, fileNodes []*graph.Node, lang string, tree *parser.ParseTree, method, path, handler string, lineNum int, framework string) Contract {
normPath, origNames := NormalizeHTTPPathWithParams(path)
meta := map[string]any{"method": method, "path": normPath, "framework": framework}
if len(origNames) > 0 {
meta["path_param_names"] = origNames
}
symbolID := findEnclosingSymbol(fileNodes, lineNum)
if handler != "" {
meta["handler_ident"] = handler
if hID := resolveHandlerIdent(fileNodes, handler); hID != "" {
symbolID = hID
}
}
c := Contract{
ID: fmt.Sprintf("http::%s::%s", method, normPath),
Type: ContractHTTP,
Role: RoleProvider,
SymbolID: symbolID,
FilePath: filePath,
Line: lineNum,
Meta: meta,
Confidence: 0.9,
}
EnrichHTTPContractWithTree(&c, lines, fileNodes, lang, tree)
return c
}
// rustScopeRegion is one `web::scope("/prefix")` and the text span its nested
// resources occupy (the enclosing call's balanced region).
type rustScopeRegion struct {
start, end int
prefix string
}
// rustActixScopes locates every `web::scope("/prefix")` and the text region of
// the call that encloses it, so resources inside that region inherit the prefix.
func rustActixScopes(text string) []rustScopeRegion {
var out []rustScopeRegion
for _, m := range rustActixScopeRE.FindAllStringSubmatchIndex(text, -1) {
out = append(out, rustScopeRegion{
start: m[0],
end: rustEnclosingClose(text, m[0]),
prefix: text[m[2]:m[3]],
})
}
return out
}
// rustScopePrefix returns the concatenated prefixes of every scope region that
// contains pos, outermost first.
func rustScopePrefix(scopes []rustScopeRegion, pos int) string {
var b strings.Builder
for _, s := range scopes {
if pos > s.start && pos < s.end {
b.WriteString(s.prefix)
}
}
return b.String()
}
// rustEnclosingClose returns the index of the close paren of the call that
// wraps the scope at scopeStart (the `(` immediately preceding it), or
// len(text) when the scope is not an argument to a wrapping call.
func rustEnclosingClose(text string, scopeStart int) int {
i := scopeStart - 1
for i >= 0 && (text[i] == ' ' || text[i] == '\t' || text[i] == '\n' || text[i] == '\r') {
i--
}
if i < 0 || text[i] != '(' {
return len(text)
}
if c := rustMatchingParen(text, i); c >= 0 {
return c
}
return len(text)
}
// rustMatchingParen returns the index of the close paren matching the open
// paren at index open, or -1.
func rustMatchingParen(text string, open int) int {
depth := 0
for j := open; j < len(text); j++ {
switch text[j] {
case '(':
depth++
case ')':
depth--
if depth == 0 {
return j
}
}
}
return -1
}
// rustJoinPath concatenates a scope prefix and a resource path, collapsing the
// boundary slash.
func rustJoinPath(prefix, path string) string {
if prefix == "" {
return path
}
return strings.TrimRight(prefix, "/") + "/" + strings.TrimLeft(path, "/")
}