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

257 lines
9.8 KiB
Go

package contracts
import (
"bytes"
"slices"
"strings"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/parser"
)
// Framework route-pass registry. The per-line httpPatterns table handles the
// simple `app.get('/x', h)` route shapes; the *structural* route passes
// (Django urlpatterns, DRF routers, Rails resources, file-based routes,
// Flask/Express object forms) need cross-symbol resolution and run as
// node-aware passes. This registry is the single front door over those
// passes: it gives each one a name, a language filter, a crash-isolated
// Detect pre-filter, and an Extract step, and lets a new framework register
// without editing http.go.
//
// This is the *extract-time* route registry. Its sibling, the post-resolution
// cross-language dispatch synthesizer registry, lives in
// internal/resolver/framework_synth.go (defaultFrameworkSynthesizers); the
// two are a pair — one binds routes as Contracts at extract time, the other
// synthesizes dynamic-dispatch call edges after resolution.
// RouteExtractCtx bundles everything HTTPExtractor.extract threads into a
// node-aware route pass.
type RouteExtractCtx struct {
FilePath string
Src []byte
Text string
Lines []string
FileNodes []*graph.Node
Lang string
Tree *parser.ParseTree
H *HTTPExtractor
}
// FrameworkRoutePass is one registered structural route extractor.
type FrameworkRoutePass interface {
// Name is a stable tag (e.g. "django", "rails").
Name() string
// Languages returns the languages this pass applies to; empty = all.
Languages() []string
// Detect is a cheap pre-filter on the file path + source.
Detect(filePath string, src []byte) bool
// Extract emits the pass's route Contracts.
Extract(ctx *RouteExtractCtx) []Contract
}
// routePass adapts a function set into a FrameworkRoutePass, so the existing
// node-aware passes register without a bespoke type each.
type routePass struct {
name string
langs []string
detect func(filePath string, src []byte) bool
run func(h *HTTPExtractor, ctx *RouteExtractCtx) []Contract
}
func (p *routePass) Name() string { return p.name }
func (p *routePass) Languages() []string { return p.langs }
func (p *routePass) Detect(filePath string, src []byte) bool {
return p.detect != nil && p.detect(filePath, src)
}
func (p *routePass) Extract(ctx *RouteExtractCtx) []Contract {
if ctx == nil || ctx.H == nil || p.run == nil {
return nil
}
return p.run(ctx.H, ctx)
}
// frameworkRoutePasses is the ordered registry; ordering is registration
// order and is deterministic.
var frameworkRoutePasses []FrameworkRoutePass
// RegisterFrameworkRoutePass adds a route pass to the registry. This is the
// public extension point — a new framework registers from an init() with no
// edits to http.go.
func RegisterFrameworkRoutePass(p FrameworkRoutePass) {
if p != nil {
frameworkRoutePasses = append(frameworkRoutePasses, p)
}
}
// RegisteredFrameworkRoutePasses returns the registry in order.
func RegisteredFrameworkRoutePasses() []FrameworkRoutePass {
out := make([]FrameworkRoutePass, len(frameworkRoutePasses))
copy(out, frameworkRoutePasses)
return out
}
// ApplicableFrameworkRoutePasses returns the registered passes whose language
// filter admits lang (empty Languages = all).
func ApplicableFrameworkRoutePasses(lang string) []FrameworkRoutePass {
var out []FrameworkRoutePass
for _, p := range frameworkRoutePasses {
langs := p.Languages()
if len(langs) == 0 || slices.Contains(langs, lang) {
out = append(out, p)
}
}
return out
}
// DetectFrameworks returns the names of every registered pass whose Detect
// claims the file. Each Detect is crash-isolated, so one panicking pass does
// not abort the rest.
func DetectFrameworks(filePath string, src []byte) []string {
var out []string
for _, p := range frameworkRoutePasses {
if safeFrameworkDetect(p, filePath, src) {
out = append(out, p.Name())
}
}
return out
}
// safeFrameworkDetect runs a pass's Detect with a panic firewall, mirroring
// the extractor-level safeExtract isolation.
func safeFrameworkDetect(p FrameworkRoutePass, filePath string, src []byte) (claimed bool) {
defer func() {
if recover() != nil {
claimed = false
}
}()
return p.Detect(filePath, src)
}
// safeFrameworkExtract runs a pass's Extract with a panic firewall.
func safeFrameworkExtract(p FrameworkRoutePass, ctx *RouteExtractCtx) (out []Contract) {
defer func() {
if recover() != nil {
out = nil
}
}()
return p.Extract(ctx)
}
// runFrameworkRoutePasses offers a file to every language-applicable pass and
// returns the union of their route Contracts, crash-isolated per pass.
func runFrameworkRoutePasses(ctx *RouteExtractCtx) []Contract {
var out []Contract
for _, p := range ApplicableFrameworkRoutePasses(ctx.Lang) {
if !safeFrameworkDetect(p, ctx.FilePath, ctx.Src) {
continue
}
out = append(out, safeFrameworkExtract(p, ctx)...)
}
return out
}
func init() {
// Structural route passes, in their historical run order. The httpPatterns
// table (per-line regex routes) and the Go-AST / React-Router / client-alias
// passes stay hard-wired in extract — they are not structural framework
// passes keyed by a content/path pre-filter.
RegisterFrameworkRoutePass(&routePass{
name: "file-based", langs: nil,
detect: func(filePath string, _ []byte) bool { return isFileBasedRouteFile(filePath) },
run: func(h *HTTPExtractor, c *RouteExtractCtx) []Contract {
return h.extractFileBasedRoutes(c.FilePath, c.Text, c.Lines, c.FileNodes, c.Lang, c.Tree)
},
})
RegisterFrameworkRoutePass(&routePass{
name: "flask-restful", langs: []string{"python"},
detect: func(_ string, src []byte) bool { return bytes.Contains(src, []byte("add_resource")) },
run: func(h *HTTPExtractor, c *RouteExtractCtx) []Contract {
return h.extractFlaskRestfulRoutes(c.FilePath, c.Text, c.Lines, c.FileNodes, c.Lang, c.Tree)
},
})
RegisterFrameworkRoutePass(&routePass{
name: "flask-add-url-rule", langs: []string{"python"},
detect: func(_ string, src []byte) bool { return bytes.Contains(src, []byte("add_url_rule")) },
run: func(h *HTTPExtractor, c *RouteExtractCtx) []Contract {
return h.extractFlaskAddURLRule(c.FilePath, c.Text, c.Lines, c.FileNodes, c.Lang, c.Tree)
},
})
RegisterFrameworkRoutePass(&routePass{
name: "django", langs: []string{"python"},
detect: func(_ string, src []byte) bool { return djangoRouteCallRE.Match(src) },
run: func(h *HTTPExtractor, c *RouteExtractCtx) []Contract {
return h.extractDjangoRoutes(c.FilePath, c.Text, c.Lines, c.FileNodes, c.Lang, c.Tree)
},
})
RegisterFrameworkRoutePass(&routePass{
name: "drf", langs: []string{"python"},
detect: func(_ string, src []byte) bool { return bytes.Contains(src, []byte(".register(")) },
run: func(h *HTTPExtractor, c *RouteExtractCtx) []Contract {
return h.extractDRFRoutes(c.FilePath, c.Text, c.Lines, c.FileNodes, c.Lang, c.Tree)
},
})
RegisterFrameworkRoutePass(&routePass{
name: "flask-decorator", langs: []string{"python"},
detect: func(_ string, src []byte) bool { return bytes.Contains(src, []byte(".route(")) },
run: func(h *HTTPExtractor, c *RouteExtractCtx) []Contract {
return h.extractFlaskDecoratorRoutes(c.FilePath, c.Text, c.Lines, c.FileNodes, c.Lang, c.Tree)
},
})
RegisterFrameworkRoutePass(&routePass{
name: "rails-resources", langs: []string{"ruby"},
detect: func(_ string, src []byte) bool { return bytes.Contains(src, []byte("resource")) },
run: func(h *HTTPExtractor, c *RouteExtractCtx) []Contract {
return h.extractRailsResourceRoutes(c.FilePath, c.Text, c.Lines, c.FileNodes, c.Lang, c.Tree)
},
})
RegisterFrameworkRoutePass(&routePass{
name: "laravel-resources", langs: []string{"php"},
detect: func(_ string, src []byte) bool {
return bytes.Contains(src, []byte("Route::resource")) || bytes.Contains(src, []byte("Route::apiResource"))
},
run: func(h *HTTPExtractor, c *RouteExtractCtx) []Contract {
return h.extractLaravelResourceRoutes(c.FilePath, c.Text, c.Lines, c.FileNodes, c.Lang, c.Tree)
},
})
// Drupal *.routing.yml routes (langs nil: gated by filename, not language).
RegisterFrameworkRoutePass(&routePass{
name: "drupal", langs: nil,
detect: func(filePath string, _ []byte) bool {
return strings.HasSuffix(filePath, ".routing.yml") || strings.HasSuffix(filePath, ".routing.yaml")
},
run: func(h *HTTPExtractor, c *RouteExtractCtx) []Contract {
return h.extractDrupalRoutes(c.FilePath, c.Text, c.Lines, c.FileNodes, c.Lang, c.Tree)
},
})
// Rust Axum chained-method `.route(...)` + Actix builder
// (`web::resource(...).route(web::get().to(h))`) routes, with `web::scope`
// prefix joining.
RegisterFrameworkRoutePass(&routePass{
name: "rust-routes", langs: []string{"rust"},
detect: func(_ string, src []byte) bool {
return bytes.Contains(src, []byte(".route(")) || bytes.Contains(src, []byte("web::resource"))
},
run: func(h *HTTPExtractor, c *RouteExtractCtx) []Contract {
return h.extractRustRoutes(c.FilePath, c.Text, c.Lines, c.FileNodes, c.Lang, c.Tree)
},
})
// Play Framework conf/routes (langs nil: gated by filename, not language —
// the file is extensionless).
RegisterFrameworkRoutePass(&routePass{
name: "play", langs: nil,
detect: func(filePath string, _ []byte) bool {
return strings.HasSuffix(filePath, "conf/routes") || strings.HasSuffix(filePath, ".routes")
},
run: func(h *HTTPExtractor, c *RouteExtractCtx) []Contract {
return h.extractPlayRoutes(c.FilePath, c.Text, c.Lines, c.FileNodes, c.Lang, c.Tree)
},
})
RegisterFrameworkRoutePass(&routePass{
name: "express-objects", langs: []string{"typescript", "javascript"},
detect: func(_ string, src []byte) bool { return bytes.Contains(src, []byte(".route(")) },
run: func(h *HTTPExtractor, c *RouteExtractCtx) []Contract {
return h.extractObjectRouteProviders(c.FilePath, c.Text, c.Lines, c.FileNodes, c.Lang, c.Tree)
},
})
}