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
257 lines
9.8 KiB
Go
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)
|
|
},
|
|
})
|
|
}
|