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

157 lines
5.4 KiB
Go

package contracts
import (
"fmt"
"regexp"
"strings"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/parser"
)
var (
// objectRouteCallRE matches an object-config route registration —
// fastify.route({...}) / server.route({...}) — up to the opening brace.
objectRouteCallRE = regexp.MustCompile(`\b\w+\.route\(\s*\{`)
// objRouteMethodRE captures the method field of a route object: a single
// "GET" string or a ['GET','POST'] array.
objRouteMethodRE = regexp.MustCompile("method\\s*:\\s*(\\[[^\\]]*\\]|[\"'`][^\"'`]*[\"'`])")
// objRouteURLRE captures Fastify's url field; objRoutePathRE captures
// Hapi's path field.
objRouteURLRE = regexp.MustCompile("url\\s*:\\s*[\"'`]([^\"'`]+)")
objRoutePathRE = regexp.MustCompile("path\\s*:\\s*[\"'`]([^\"'`]+)")
// objRouteHandlerRE captures a `handler: fnName` reference.
objRouteHandlerRE = regexp.MustCompile(`handler\s*:\s*([A-Za-z_][\w.]*)`)
// expressRouteChainRE matches app.route('/path').get(h).post(h) — the
// chained method form. Group 1 is the path, group 2 the chain tail.
expressRouteChainRE = regexp.MustCompile("\\b\\w+\\.route\\(\\s*[\"'`]([^\"'`]+)[\"'`]\\s*\\)((?:\\s*\\.\\w+\\([^)]*\\))+)")
// chainVerbRE pulls each .get(handler) off the chain tail.
chainVerbRE = regexp.MustCompile(`\.(get|post|put|delete|patch|head|options|all)\(\s*([A-Za-z_][\w.]*)?`)
// jsQuotedTokenRE pulls each quoted verb out of a method array.
jsQuotedTokenRE = regexp.MustCompile("[\"'`]([^\"'`]+)[\"'`]")
)
// extractObjectRouteProviders detects the object-config and method-chain route
// shapes the per-line provider table cannot: Fastify / Hapi
// `route({ method, url|path, handler })` and Express
// `route('/path').get(h).post(h)`. Each expands to one provider contract per
// HTTP method with the handler resolved.
func (h *HTTPExtractor) extractObjectRouteProviders(filePath, text string, lines []string, fileNodes []*graph.Node, lang string, tree *parser.ParseTree) []Contract {
var out []Contract
// Fastify / Hapi: X.route({ method: ..., url|path: ..., handler: ... }).
for _, loc := range objectRouteCallRE.FindAllStringIndex(text, -1) {
braceStart := loc[1] - 1
obj, ok := balancedBraces(text, braceStart)
if !ok {
continue
}
methods := jsObjectMethods(obj)
if len(methods) == 0 {
continue
}
routePath, framework := "", ""
if m := objRouteURLRE.FindStringSubmatch(obj); m != nil {
routePath, framework = m[1], "fastify"
} else if m := objRoutePathRE.FindStringSubmatch(obj); m != nil {
routePath, framework = m[1], "hapi"
}
if routePath == "" {
continue
}
handlerID := ""
if m := objRouteHandlerRE.FindStringSubmatch(obj); m != nil {
handlerID = resolveHandlerIdent(fileNodes, m[1])
}
lineNum := lineOfOffset(text, loc[0])
for _, method := range methods {
out = append(out, h.buildObjectRouteContract(filePath, method, routePath, handlerID, framework, lineNum, lines, fileNodes, lang, tree))
}
}
// Express: app.route('/path').get(h).post(h).
for _, m := range expressRouteChainRE.FindAllStringSubmatchIndex(text, -1) {
routePath := text[m[2]:m[3]]
chain := text[m[4]:m[5]]
lineNum := lineOfOffset(text, m[0])
for _, vm := range chainVerbRE.FindAllStringSubmatch(chain, -1) {
method := strings.ToUpper(vm[1])
handlerID := resolveHandlerIdent(fileNodes, vm[2])
out = append(out, h.buildObjectRouteContract(filePath, method, routePath, handlerID, "express", lineNum, lines, fileNodes, lang, tree))
}
}
return out
}
// buildObjectRouteContract assembles a provider contract for an object-config
// or chained route.
func (h *HTTPExtractor) buildObjectRouteContract(filePath, method, path, symbolID, framework string, lineNum int, lines []string, fileNodes []*graph.Node, lang string, tree *parser.ParseTree) Contract {
normPath, origNames := NormalizeHTTPPathWithParams(path)
meta := map[string]any{
"method": method,
"path": normPath,
"framework": framework,
}
if len(origNames) > 0 {
meta["path_param_names"] = origNames
}
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
}
// jsObjectMethods returns the upper-cased HTTP methods named in a route
// object's method field — one for a string, several for an array.
func jsObjectMethods(obj string) []string {
m := objRouteMethodRE.FindStringSubmatch(obj)
if m == nil {
return nil
}
var out []string
for _, tm := range jsQuotedTokenRE.FindAllStringSubmatch(m[1], -1) {
if v := strings.ToUpper(strings.TrimSpace(tm[1])); v != "" {
out = append(out, v)
}
}
return out
}
// balancedBraces returns the substring from the brace at start through its
// matching close brace, or ("", false) when unbalanced.
func balancedBraces(text string, start int) (string, bool) {
if start < 0 || start >= len(text) || text[start] != '{' {
return "", false
}
depth := 0
for i := start; i < len(text); i++ {
switch text[i] {
case '{':
depth++
case '}':
depth--
if depth == 0 {
return text[start : i+1], true
}
}
}
return "", false
}
// lineOfOffset returns the 1-based line number of a byte offset in text.
func lineOfOffset(text string, off int) int {
if off > len(text) {
off = len(text)
}
return 1 + strings.Count(text[:off], "\n")
}