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
353 lines
9.7 KiB
Go
353 lines
9.7 KiB
Go
package languages
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
"github.com/zzet/gortex/internal/parser"
|
|
sitter "github.com/zzet/gortex/internal/parser/tsitter"
|
|
)
|
|
|
|
// Laravel event binding. A Laravel app dispatches an event
|
|
// (`event(new OrderShipped($o))`, `OrderShipped::dispatch($o)`) handled by a
|
|
// listener's typed `handle(OrderShipped $e)` method — discovered either by
|
|
// the `App\Listeners` convention or by the `$listen` map in an
|
|
// EventServiceProvider. The dispatch is keyed on the event type, which the
|
|
// static call graph cannot see. This pass tags listeners (two sources) and
|
|
// stamps a placeholder per dispatch site; the resolver fans out to every
|
|
// matching listener.
|
|
|
|
// laravelEventViaTag must match resolver.laravelEventVia — the languages
|
|
// package does not import resolver, so the two agree by value.
|
|
const laravelEventViaTag = "laravel-event"
|
|
|
|
// captureLaravelEvents tags listener handle methods (Source 1: typed handle
|
|
// under a Listeners namespace; Source 2: the EventServiceProvider $listen
|
|
// map) and stamps event-dispatch placeholders. Runs at the tail of Extract.
|
|
func captureLaravelEvents(result *parser.ExtractionResult, root *sitter.Node, filePath string, src []byte) {
|
|
if root == nil || result == nil {
|
|
return
|
|
}
|
|
nodesByLine := map[int][]*graph.Node{}
|
|
classNodeByName := map[string]*graph.Node{}
|
|
for _, n := range result.Nodes {
|
|
if n == nil {
|
|
continue
|
|
}
|
|
if n.Kind == graph.KindMethod || n.Kind == graph.KindFunction {
|
|
nodesByLine[n.StartLine] = append(nodesByLine[n.StartLine], n)
|
|
}
|
|
if n.Kind == graph.KindType {
|
|
classNodeByName[laravelSimpleName(n.Name)] = n
|
|
}
|
|
}
|
|
|
|
fileNamespace := laravelFileNamespace(root, src)
|
|
|
|
laravelWalk(root, func(n *sitter.Node) {
|
|
switch n.Type() {
|
|
case "method_declaration":
|
|
// Source 1: a typed handle method under a Listeners namespace.
|
|
if laravelMethodName(n, src) != "handle" {
|
|
return
|
|
}
|
|
if !strings.Contains(fileNamespace, "Listeners") {
|
|
return
|
|
}
|
|
if t := laravelFirstParamType(n, src); t != "" {
|
|
laravelTagHandle(nodesByLine, n, src, t)
|
|
}
|
|
case "property_declaration":
|
|
// Source 2: the $listen map of an EventServiceProvider.
|
|
laravelTagListenMap(n, src, classNodeByName)
|
|
}
|
|
})
|
|
|
|
// Dispatch sites.
|
|
funcRanges := buildFuncRanges(result)
|
|
seen := map[string]bool{}
|
|
laravelWalk(root, func(n *sitter.Node) {
|
|
evType := ""
|
|
switch n.Type() {
|
|
case "function_call_expression":
|
|
// event(new XEvent(...))
|
|
if fn := n.ChildByFieldName("function"); fn == nil || fn.Content(src) != "event" {
|
|
return
|
|
}
|
|
evType = laravelFirstNewArgType(n, src)
|
|
case "scoped_call_expression":
|
|
// Event::dispatch(new XEvent()) or XEvent::dispatch(...)
|
|
name := ""
|
|
if nm := n.ChildByFieldName("name"); nm != nil {
|
|
name = nm.Content(src)
|
|
}
|
|
if name != "dispatch" {
|
|
return
|
|
}
|
|
scope := ""
|
|
if sc := n.ChildByFieldName("scope"); sc != nil {
|
|
scope = laravelSimpleName(sc.Content(src))
|
|
}
|
|
if scope == "Event" {
|
|
evType = laravelFirstNewArgType(n, src)
|
|
} else {
|
|
evType = scope // the event dispatches itself
|
|
}
|
|
default:
|
|
return
|
|
}
|
|
if evType == "" {
|
|
return
|
|
}
|
|
line := int(n.StartPoint().Row) + 1
|
|
from := findEnclosingFunc(funcRanges, line)
|
|
if from == "" {
|
|
return
|
|
}
|
|
k := from + "\x00" + evType
|
|
if seen[k] {
|
|
return
|
|
}
|
|
seen[k] = true
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: from,
|
|
To: "unresolved::*.handle",
|
|
Kind: graph.EdgeCalls,
|
|
FilePath: filePath,
|
|
Line: line,
|
|
Meta: map[string]any{"via": laravelEventViaTag, "laravel_event_type": evType},
|
|
})
|
|
})
|
|
}
|
|
|
|
// laravelTagHandle stamps Meta["laravel_listener_type"] on a handle method.
|
|
func laravelTagHandle(nodesByLine map[int][]*graph.Node, method *sitter.Node, src []byte, evType string) {
|
|
line := int(method.StartPoint().Row) + 1
|
|
for _, n := range nodesByLine[line] {
|
|
if n.Name != "handle" {
|
|
continue
|
|
}
|
|
if n.Meta == nil {
|
|
n.Meta = map[string]any{}
|
|
}
|
|
n.Meta["laravel_listener_type"] = evType
|
|
return
|
|
}
|
|
}
|
|
|
|
// laravelTagListenMap parses a `$listen = [Event::class => [Listener::class,
|
|
// ...]]` property and stamps the encoded map on the enclosing class node so
|
|
// the resolver can bind listeners declared in another file.
|
|
func laravelTagListenMap(prop *sitter.Node, src []byte, classNodeByName map[string]*graph.Node) {
|
|
if !laravelPropertyNamed(prop, "listen", src) {
|
|
return
|
|
}
|
|
arr := laravelFindDescendant(prop, "array_creation_expression")
|
|
if arr == nil {
|
|
return
|
|
}
|
|
var entries []string
|
|
for i, _nc := 0, int(arr.NamedChildCount()); i < _nc; i++ {
|
|
el := arr.NamedChild(i)
|
|
if el == nil || el.Type() != "array_element_initializer" || el.NamedChildCount() < 2 {
|
|
continue
|
|
}
|
|
event := laravelSimpleName(phpExtractClassRef(el.NamedChild(0), src))
|
|
listeners := laravelClassRefList(el.NamedChild(1), src)
|
|
if event == "" || len(listeners) == 0 {
|
|
continue
|
|
}
|
|
entries = append(entries, event+"=>"+strings.Join(listeners, ","))
|
|
}
|
|
if len(entries) == 0 {
|
|
return
|
|
}
|
|
className := laravelEnclosingClassName(prop, src)
|
|
cn := classNodeByName[laravelSimpleName(className)]
|
|
if cn == nil {
|
|
return
|
|
}
|
|
if cn.Meta == nil {
|
|
cn.Meta = map[string]any{}
|
|
}
|
|
cn.Meta["laravel_listen_map"] = strings.Join(entries, ";")
|
|
}
|
|
|
|
// laravelClassRefList collects the simple class names from an array of
|
|
// `Listener::class` entries (or a single bare class ref).
|
|
func laravelClassRefList(n *sitter.Node, src []byte) []string {
|
|
var out []string
|
|
if n == nil {
|
|
return out
|
|
}
|
|
if n.Type() == "array_creation_expression" {
|
|
for i, _nc := 0, int(n.NamedChildCount()); i < _nc; i++ {
|
|
el := n.NamedChild(i)
|
|
if el == nil {
|
|
continue
|
|
}
|
|
ref := el
|
|
if el.Type() == "array_element_initializer" && el.NamedChildCount() > 0 {
|
|
ref = el.NamedChild(int(el.NamedChildCount()) - 1)
|
|
}
|
|
if c := laravelSimpleName(phpExtractClassRef(ref, src)); c != "" {
|
|
out = append(out, c)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
if c := laravelSimpleName(phpExtractClassRef(n, src)); c != "" {
|
|
out = append(out, c)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// laravelFirstNewArgType returns the constructed type of a call's first
|
|
// `new X(...)` argument.
|
|
func laravelFirstNewArgType(call *sitter.Node, src []byte) string {
|
|
args := call.ChildByFieldName("arguments")
|
|
if args == nil {
|
|
return ""
|
|
}
|
|
for i, _nc := 0, int(args.NamedChildCount()); i < _nc; i++ {
|
|
a := args.NamedChild(i)
|
|
if a == nil {
|
|
continue
|
|
}
|
|
expr := a
|
|
if a.Type() == "argument" && a.NamedChildCount() > 0 {
|
|
expr = a.NamedChild(0)
|
|
}
|
|
if expr != nil && expr.Type() == "object_creation_expression" {
|
|
if t := laravelNewType(expr, src); t != "" {
|
|
return t
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// laravelNewType returns the simple class name of a `new X(...)`.
|
|
func laravelNewType(newExpr *sitter.Node, src []byte) string {
|
|
for i, _nc := 0, int(newExpr.NamedChildCount()); i < _nc; i++ {
|
|
c := newExpr.NamedChild(i)
|
|
if c == nil {
|
|
continue
|
|
}
|
|
switch c.Type() {
|
|
case "name", "qualified_name":
|
|
return laravelSimpleName(c.Content(src))
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// laravelFirstParamType returns the simple type name of a method's first
|
|
// formal parameter.
|
|
func laravelFirstParamType(method *sitter.Node, src []byte) string {
|
|
params := method.ChildByFieldName("parameters")
|
|
if params == nil {
|
|
return ""
|
|
}
|
|
for i, _nc := 0, int(params.NamedChildCount()); i < _nc; i++ {
|
|
p := params.NamedChild(i)
|
|
if p == nil || p.Type() != "simple_parameter" {
|
|
continue
|
|
}
|
|
if t := p.ChildByFieldName("type"); t != nil {
|
|
return laravelSimpleName(strings.TrimPrefix(strings.TrimSpace(t.Content(src)), "?"))
|
|
}
|
|
return ""
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func laravelMethodName(method *sitter.Node, src []byte) string {
|
|
if n := method.ChildByFieldName("name"); n != nil {
|
|
return n.Content(src)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// laravelPropertyNamed reports whether a property_declaration declares
|
|
// $<name>.
|
|
func laravelPropertyNamed(prop *sitter.Node, name string, src []byte) bool {
|
|
found := false
|
|
laravelWalk(prop, func(n *sitter.Node) {
|
|
if found || n.Type() != "property_element" {
|
|
return
|
|
}
|
|
if vn := n.ChildByFieldName("name"); vn != nil {
|
|
if strings.TrimPrefix(strings.TrimSpace(vn.Content(src)), "$") == name {
|
|
found = true
|
|
}
|
|
} else if strings.TrimPrefix(strings.TrimSpace(n.Content(src)), "$") == name {
|
|
found = true
|
|
}
|
|
})
|
|
return found
|
|
}
|
|
|
|
// laravelEnclosingClassName walks up to the nearest class_declaration name.
|
|
func laravelEnclosingClassName(node *sitter.Node, src []byte) string {
|
|
for n := node; n != nil; n = n.Parent() {
|
|
if n.Type() == "class_declaration" {
|
|
if nm := n.ChildByFieldName("name"); nm != nil {
|
|
return nm.Content(src)
|
|
}
|
|
return ""
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// laravelFileNamespace returns the file's namespace name, or "".
|
|
func laravelFileNamespace(root *sitter.Node, src []byte) string {
|
|
ns := ""
|
|
laravelWalk(root, func(n *sitter.Node) {
|
|
if ns != "" || n.Type() != "namespace_definition" {
|
|
return
|
|
}
|
|
if nm := laravelFindDescendant(n, "namespace_name"); nm != nil {
|
|
ns = nm.Content(src)
|
|
}
|
|
})
|
|
return ns
|
|
}
|
|
|
|
// laravelFindDescendant returns the first descendant of n of the given type.
|
|
func laravelFindDescendant(n *sitter.Node, typ string) *sitter.Node {
|
|
var found *sitter.Node
|
|
laravelWalk(n, func(c *sitter.Node) {
|
|
if found == nil && c.Type() == typ {
|
|
found = c
|
|
}
|
|
})
|
|
return found
|
|
}
|
|
|
|
// laravelSimpleName returns the last segment of a `\`- or `::`-qualified
|
|
// PHP name.
|
|
func laravelSimpleName(s string) string {
|
|
s = strings.TrimSpace(s)
|
|
if i := strings.LastIndex(s, "::"); i >= 0 {
|
|
s = s[i+2:]
|
|
}
|
|
if i := strings.LastIndexByte(s, '\\'); i >= 0 {
|
|
s = s[i+1:]
|
|
}
|
|
return strings.TrimSpace(s)
|
|
}
|
|
|
|
// laravelWalk visits n and all its named descendants.
|
|
func laravelWalk(n *sitter.Node, fn func(*sitter.Node)) {
|
|
if n == nil {
|
|
return
|
|
}
|
|
fn(n)
|
|
for i, _nc := 0, int(n.NamedChildCount()); i < _nc; i++ {
|
|
laravelWalk(n.NamedChild(i), fn)
|
|
}
|
|
}
|