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
157 lines
5.4 KiB
Go
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")
|
|
}
|