PHP: let `$FUNC(...)` match include/require language constructs

ober

bcbae7dc65a8ccfaad966d4a37dc1192e39d0251

diff --git a/lib/semgrep/match/structural.sls b/lib/semgrep/match/structural.sls
index a1d4a28..49b2ecb 100644
--- a/lib/semgrep/match/structural.sls
+++ b/lib/semgrep/match/structural.sls
@@ -1153,6 +1153,38 @@
                         target
                         bindings)])
          (and (not (null? matches)) (car matches))))
+  (def (php-include-like-target-type? node-type)
+       (or (string=? node-type "include_expression")
+           (string=? node-type "include_once_expression")
+           (string=? node-type "require_expression")
+           (string=? node-type "require_once_expression")))
+  (def (node-contains-ellipsis-arg? n)
+       (let loop ([i 0])
+         (and (< i (node-child-count n))
+              (let ([c (node-child n i)])
+                (or (string=? (node-type c) "variadic_placeholder")
+                    (ellipsis-node? c)
+                    (node-contains-ellipsis-arg? c)
+                    (loop (+ i 1)))))))
+  (def (php-include-call-matches
+         language
+         pattern
+         target
+         bindings)
+       (and (php-language? language)
+            (string=? (node-type pattern) "function_call_expression")
+            (php-include-like-target-type? (node-type target))
+            (let ([fn (node-named-child pattern 0)]
+                  [args (node-named-child pattern 1)])
+              (and fn
+                   args
+                   (php-metavariable-name-from-node fn)
+                   (node-contains-ellipsis-arg? args)
+                   (let ([next (bind-metavariable
+                                 (php-metavariable-name-from-node fn)
+                                 (node-child target 0)
+                                 bindings)])
+                     (and next (list next)))))))
   (def (structural-node-matches
          language
          pattern
@@ -1161,6 +1193,8 @@
        (let ([text (node-text pattern)])
          (cond
            [(ellipsis-node? pattern) (list bindings)]
+           [(php-include-call-matches language pattern target bindings) =>
+            (lambda (matches) matches)]
            [(and (php-language? language)
                  (php-metavariable-name-from-node pattern)) =>
             (lambda (name)
diff --git a/src/.jerbuild-hashes b/src/.jerbuild-hashes
index e7f9c9f..5d98ccc 100644
--- a/src/.jerbuild-hashes
+++ b/src/.jerbuild-hashes
@@ -8,6 +8,6 @@
   ("src/semgrep/fix.ss" . "2E5B65B1FEF3B2B1")
   ("src/semgrep/schema/lang.ss" . "CAE2CA859C9A9FD0")
   ("src/semgrep/rule.ss" . "E12C108153C181FA")
-  ("src/semgrep/match/structural.ss" . "8144F1297C91CF9F")
+  ("src/semgrep/match/structural.ss" . "DF9CD3E0C00E5D40")
   ("src/semgrep/main.ss" . "A4EC9E7F2A09D25E")
   ("src/semgrep/cli.ss" . "EBDC4B1DAD3F13CC"))
diff --git a/src/semgrep/match/structural.ss b/src/semgrep/match/structural.ss
index b0dcf15..221408a 100644
--- a/src/semgrep/match/structural.ss
+++ b/src/semgrep/match/structural.ss
@@ -1172,10 +1172,47 @@
     (and (not (null? matches))
          (car matches))))
 
+;; PHP include/require/include_once/require_once are language constructs, not
+;; function calls, so they parse as *_expression nodes. Semgrep still lets a
+;; call pattern `$FUNC(...)` match them, binding $FUNC to the keyword token.
+(def (php-include-like-target-type? node-type)
+  (or (string=? node-type "include_expression")
+      (string=? node-type "include_once_expression")
+      (string=? node-type "require_expression")
+      (string=? node-type "require_once_expression")))
+
+;; The pattern's argument list `(...)` is variadic when it contains an ellipsis
+;; (a variadic_placeholder, or the rewritten __sg_ellipsis__ sentinel node).
+(def (node-contains-ellipsis-arg? n)
+  (let loop ([i 0])
+    (and (< i (node-child-count n))
+         (let ([c (node-child n i)])
+           (or (string=? (node-type c) "variadic_placeholder")
+               (ellipsis-node? c)
+               (node-contains-ellipsis-arg? c)
+               (loop (+ i 1)))))))
+
+(def (php-include-call-matches language pattern target bindings)
+  (and (php-language? language)
+       (string=? (node-type pattern) "function_call_expression")
+       (php-include-like-target-type? (node-type target))
+       (let ([fn (node-named-child pattern 0)]
+             [args (node-named-child pattern 1)])
+         (and fn args
+              (php-metavariable-name-from-node fn)
+              (node-contains-ellipsis-arg? args)
+              (let ([next (bind-metavariable
+                            (php-metavariable-name-from-node fn)
+                            (node-child target 0)
+                            bindings)])
+                (and next (list next)))))))
+
 (def (structural-node-matches language pattern target bindings)
   (let ([text (node-text pattern)])
     (cond
       [(ellipsis-node? pattern) (list bindings)]
+      [(php-include-call-matches language pattern target bindings)
+       => (lambda (matches) matches)]
       ;; PHP metavariable `$NAME` (uppercase) -> bind to the target node.
       [(and (php-language? language)
             (php-metavariable-name-from-node pattern))