Files
zzet--gortex/internal/parser/languages/php_string_callable.go
T
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

240 lines
7.4 KiB
Go

package languages
import (
"strings"
"github.com/zzet/gortex/internal/parser"
sitter "github.com/zzet/gortex/internal/parser/tsitter"
)
// phpCallableHOFs is the curated set of PHP higher-order functions whose string
// / array arguments are callables. A string literal naming a function inside
// one of these calls is a function-as-value reference the gate resolves
// repo-wide (unique-or-drop), bypassing the same-file scope.
var phpCallableHOFs = map[string]bool{
"array_map": true,
"array_filter": true,
"array_walk": true,
"array_walk_recursive": true,
"array_reduce": true,
"usort": true,
"uasort": true,
"uksort": true,
"call_user_func": true,
"call_user_func_array": true,
"preg_replace_callback": true,
"register_shutdown_function": true,
"set_error_handler": true,
"spl_autoload_register": true,
"array_udiff": true,
"array_udiff_assoc": true,
"array_uintersect": true,
"array_uintersect_assoc": true,
"forward_static_call": true,
"forward_static_call_array": true,
"preg_replace_callback_array": true,
"register_tick_function": true,
"set_exception_handler": true,
"ob_start": true,
"iterator_apply": true,
"header_register_callback": true,
"is_callable": true,
}
// capturePHPStringCallables records each string / array callable passed to a
// curated higher-order function (`array_map('strtoupper', $xs)`,
// `usort($a, 'cmp')`, `['Foo', 'bar']`, `[$svc, 'handle']`) as a gate-skipping
// function-as-value candidate, so the callee is reachable through the
// registration even though no direct call edge exists.
func (e *PHPExtractor) capturePHPStringCallables(result *parser.ExtractionResult, root *sitter.Node, filePath string, src []byte) {
if root == nil || result == nil {
return
}
funcRanges := buildFuncRanges(result)
if len(funcRanges) == 0 {
return
}
var cands []FnValueCandidate
seen := map[string]bool{}
walkNodes(root, func(n *sitter.Node) {
if n.Type() != "function_call_expression" {
return
}
fn := n.ChildByFieldName("function")
if fn == nil {
return
}
callee := strings.TrimSpace(fn.Content(src))
if i := strings.LastIndex(callee, "\\"); i >= 0 {
callee = callee[i+1:]
}
calleeLower := strings.ToLower(callee)
if !phpCallableHOFs[calleeLower] {
return
}
args := n.ChildByFieldName("arguments")
if args == nil {
return
}
line := int(n.StartPoint().Row) + 1
fromID := findEnclosingFunc(funcRanges, line)
if fromID == "" {
return
}
emit := func(name, recvHint string) {
key := fromID + "|" + name + "|" + recvHint
if seen[key] {
return
}
seen[key] = true
cands = append(cands, FnValueCandidate{
FromID: fromID, Name: name, FilePath: filePath, Line: line,
Form: "php_string_callable", RecvHint: recvHint, Lang: "php", SkipGate: true,
})
}
// preg_replace_callback_array passes a `pattern => callback` map; the
// callbacks live on the value side, so resolve each value rather than
// scanning the array flat (its regex-pattern keys are not callables).
callbackArray := calleeLower == "preg_replace_callback_array"
for i, _nc := 0, int(args.NamedChildCount()); i < _nc; i++ {
a := args.NamedChild(i)
if a == nil {
continue
}
if callbackArray {
for _, cb := range e.phpCallbackArrayValues(a, src) {
emit(cb.name, cb.recvHint)
}
continue
}
name, recvHint, ok := e.phpCallableArg(a, src)
if !ok {
continue
}
emit(name, recvHint)
}
})
EmitFnValueCandidates(result, cands)
}
// phpCallableArg resolves a callable argument node to its (function name,
// receiver hint): a `'fn'` / `'ns\fn'` string, a `'Class::method'` static
// string, or a `[$obj, 'm']` / `['Class', 'm']` array callable. recvHint is the
// class for a method callable, "" for a free function.
func (e *PHPExtractor) phpCallableArg(a *sitter.Node, src []byte) (name, recvHint string, ok bool) {
// Unwrap an `argument` wrapper to its value node.
node := a
if a.Type() == "argument" && a.NamedChildCount() > 0 {
node = a.NamedChild(0)
}
switch node.Type() {
case "string", "encapsed_string":
return phpCallableFromString(e.extractStringContent(node, src))
case "array_creation_expression":
var strs []string
walkNodes(node, func(c *sitter.Node) {
if c.Type() == "string" || c.Type() == "encapsed_string" {
if s := strings.TrimSpace(e.extractStringContent(c, src)); s != "" {
strs = append(strs, s)
}
}
})
switch len(strs) {
case 1:
if isPhpIdent(strs[0]) {
return strs[0], "", true
}
case 2:
if isPhpIdent(phpTrailingSegment(strs[0])) && isPhpIdent(strs[1]) {
return strs[1], phpTrailingSegment(strs[0]), true
}
}
}
return "", "", false
}
// phpCallableRef is a resolved (name, receiver-hint) callable pair.
type phpCallableRef struct{ name, recvHint string }
// phpCallbackArrayValues resolves the value-side callable of each element of
// a `pattern => callback` array literal -- the shape preg_replace_callback_array
// takes. Keys (regex patterns) are skipped; each value runs through
// phpCallableArg so string, `Class::method`, and `[$obj, 'm']` callbacks all
// resolve.
func (e *PHPExtractor) phpCallbackArrayValues(a *sitter.Node, src []byte) []phpCallableRef {
node := a
if a.Type() == "argument" && a.NamedChildCount() > 0 {
node = a.NamedChild(0)
}
if node == nil || node.Type() != "array_creation_expression" {
return nil
}
var out []phpCallableRef
for i, _nc := 0, int(node.NamedChildCount()); i < _nc; i++ {
el := node.NamedChild(i)
if el == nil || el.Type() != "array_element_initializer" {
continue
}
// The value side is the last named child: `key => value` has two,
// a bare `value` element has one.
val := el.NamedChild(int(el.NamedChildCount()) - 1)
if val == nil {
continue
}
if name, recvHint, ok := e.phpCallableArg(val, src); ok {
out = append(out, phpCallableRef{name: name, recvHint: recvHint})
}
}
return out
}
// phpCallableFromString parses a string callable into (function/method name,
// receiver hint). Returns ok=false for any string that is not a callable-name
// shape (a regex pattern, a format string, …).
func phpCallableFromString(s string) (name, recvHint string, ok bool) {
s = strings.TrimSpace(s)
if s == "" {
return "", "", false
}
if i := strings.Index(s, "::"); i >= 0 {
cls := phpTrailingSegment(strings.TrimPrefix(s[:i], "\\"))
member := s[i+2:]
if isPhpIdent(cls) && isPhpIdent(member) {
return member, cls, true
}
return "", "", false
}
bare := phpTrailingSegment(strings.TrimPrefix(s, "\\"))
if isPhpIdent(bare) {
return bare, "", true
}
return "", "", false
}
// phpTrailingSegment returns the last `\`-separated segment of a namespaced name.
func phpTrailingSegment(s string) string {
if i := strings.LastIndex(s, "\\"); i >= 0 {
return s[i+1:]
}
return s
}
// isPhpIdent reports whether s is a bare PHP identifier (a valid function /
// method / class name), excluding regex patterns and other arbitrary strings.
func isPhpIdent(s string) bool {
if s == "" {
return false
}
for i := 0; i < len(s); i++ {
c := s[i]
if c == '_' || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') {
continue
}
if i > 0 && c >= '0' && c <= '9' {
continue
}
return false
}
return true
}