Route PHP through the tree-sitter structural matcher
ober
496ac1874fbb974617c305d87a46f9bca9af3a52
--- a/lib/semgrep/match/structural.sls +++ b/lib/semgrep/match/structural.sls @@ -46,6 +46,20 @@ (string=? language "ts") (string=? language "tsx"))) (def (go-language? language) (string=? language "go")) + (def (php-language? language) (string=? language "php")) + (def (metavariable-name-char-uppercase? name) + (and (> (string-length name) 0) + (let ([c (string-ref name 0)]) + (or (char=? c #\_) (char-upper-case? c))))) + (def (php-metavariable-name-from-node n) + (and (string=? (node-type n) "variable_name") + (let ([text (node-text n)]) + (and (> (string-length text) 1) + (char=? (string-ref text 0) #\$) + (let ([name (substring text 1 (string-length text))]) + (and (valid-metavariable-name? name) + (metavariable-name-char-uppercase? name) + name)))))) (def (go-skip-spaces source i len) (if (and (< i len) (char-whitespace? (string-ref source i))) (go-skip-spaces source (+ i 1) len) @@ -197,7 +211,8 @@ #t))))] [(and (char=? (string-ref source i) #\$) (< (+ i 1) len) - (identifier-start? (string-ref source (+ i 1)))) + (identifier-start? (string-ref source (+ i 1))) + (not (php-language? language))) (let name-loop ([j (+ i 2)]) (if (and (< j len) (identifier-rest? (string-ref source j))) (name-loop (+ j 1)) @@ -1146,6 +1161,11 @@ (let ([text (node-text pattern)]) (cond [(ellipsis-node? pattern) (list bindings)] + [(and (php-language? language) + (php-metavariable-name-from-node pattern)) => + (lambda (name) + (let ([next (bind-metavariable name target bindings)]) + (if next (list next) '())))] [(go-deep-expression-matches language pattern @@ -1549,6 +1569,37 @@ (begin (tree-close! (parse-result-tree result)) (loop (cdr scaffolds))))))))))) + (define php-pattern-scaffolds + (list (cons "<?php " "") (cons "<?php " ";"))) + (def (php-parse-pattern rewritten-pattern) + (let ([trimmed (string-trim rewritten-pattern)]) + (let loop ([scaffolds php-pattern-scaffolds]) + (if (null? scaffolds) + (values #f #f) + (let* ([prefix (caar scaffolds)] + [suffix (cdar scaffolds)] + [wrapped (string-append prefix trimmed suffix)] + [result (parse-target-string "php" wrapped)] + [root (parse-result-root result)]) + (if (or (not root) (parse-result-has-errors? result)) + (begin + (when root (node-close! root)) + (tree-close! (parse-result-tree result)) + (loop (cdr scaffolds))) + (let* ([hstart (utf8-length prefix)] + [hend (+ hstart (utf8-length trimmed))] + [hole (and (<= (node-start-byte root) hstart) + (>= (node-end-byte root) hend) + (go-deepest-node-containing + root + hstart + hend))]) + (node-close! root) + (if hole + (values hole result) + (begin + (tree-close! (parse-result-tree result)) + (loop (cdr scaffolds))))))))))) (def (run-structural-matches language normalized @@ -1586,36 +1637,40 @@ (let ([rewritten-pattern (rewrite-metavariables language pattern-source)]) - (if (go-language? language) - (let-values ([(normalized result) - (go-parse-pattern rewritten-pattern)]) - (if (not normalized) - '() - (let ([matches (run-structural-matches - language - normalized - target-root - initial-bindings)]) - (node-close! normalized) - (tree-close! (parse-result-tree result)) - matches))) - (let* ([pattern-result (parse-target-string - language - rewritten-pattern)] - [pattern-root (parse-result-root pattern-result)]) - (let-values ([(normalized owned-nodes) - (normalized-pattern-root - language - pattern-root)]) - (let ([matches (run-structural-matches - language - normalized - target-root - initial-bindings)]) - (for-each node-close! owned-nodes) - (when pattern-root (node-close! pattern-root)) - (tree-close! (parse-result-tree pattern-result)) - matches)))))) + (cond + [(or (go-language? language) (php-language? language)) + (let-values ([(normalized result) + (if (go-language? language) + (go-parse-pattern rewritten-pattern) + (php-parse-pattern rewritten-pattern))]) + (if (not normalized) + '() + (let ([matches (run-structural-matches + language + normalized + target-root + initial-bindings)]) + (node-close! normalized) + (tree-close! (parse-result-tree result)) + matches)))] + [else + (let* ([pattern-result (parse-target-string + language + rewritten-pattern)] + [pattern-root (parse-result-root pattern-result)]) + (let-values ([(normalized owned-nodes) + (normalized-pattern-root + language + pattern-root)]) + (let ([matches (run-structural-matches + language + normalized + target-root + initial-bindings)]) + (for-each node-close! owned-nodes) + (when pattern-root (node-close! pattern-root)) + (tree-close! (parse-result-tree pattern-result)) + matches)))]))) (def (structural-pattern-matches language pattern-source --- a/lib/semgrep/parse/parse-target.sls +++ b/lib/semgrep/parse/parse-target.sls @@ -24,6 +24,7 @@ [(string=? canonical "javascript") 'javascript] [(string=? canonical "typescript") 'javascript] [(string=? canonical "go") 'go] + [(string=? canonical "php") 'php] [else (error 'parse-target-string "unsupported language in current MVP" --- a/lib/semgrep/scan.sls +++ b/lib/semgrep/scan.sls @@ -743,7 +743,6 @@ (string=? canonical "yaml") (string=? canonical "c") (string=? canonical "terraform") - (string=? canonical "php") (string=? canonical "java") (string=? canonical "csharp") (string=? canonical "swift") --- a/src/.jerbuild-hashes +++ b/src/.jerbuild-hashes @@ -2,12 +2,12 @@ ("src/semgrep/result.ss" . "22D23E40B49BA529") ("src/semgrep/output/json.ss" . "293881CFA2ADB7BC") ("src/semgrep/lang.ss" . "6982E07679D20836") - ("src/semgrep/parse/parse-target.ss" . "B1616180DE7038ED") - ("src/semgrep/scan.ss" . "1E18DBE6EC56D12C") - ("src/semgrep/output/text.ss" . "BE476CB84B807FBA") + ("src/semgrep/parse/parse-target.ss" . "984D9B3DED8F3EFE") + ("src/semgrep/scan.ss" . "5A2E9AFF3229A61E") ("src/semgrep/fix.ss" . "2E5B65B1FEF3B2B1") - ("src/semgrep/schema/lang.ss" . "CAE2CA859C9A9FD0") + ("src/semgrep/output/text.ss" . "BE476CB84B807FBA") ("src/semgrep/rule.ss" . "E12C108153C181FA") - ("src/semgrep/match/structural.ss" . "AA1BA746924C8189") + ("src/semgrep/schema/lang.ss" . "CAE2CA859C9A9FD0") + ("src/semgrep/match/structural.ss" . "8144F1297C91CF9F") ("src/semgrep/main.ss" . "A4EC9E7F2A09D25E") ("src/semgrep/cli.ss" . "EBDC4B1DAD3F13CC")) --- a/src/semgrep/match/structural.ss +++ b/src/semgrep/match/structural.ss @@ -57,6 +57,28 @@ (def (go-language? language) (string=? language "go")) +(def (php-language? language) + (string=? language "php")) + +;; A Semgrep metavariable name: first char uppercase or underscore. This is how +;; PHP metavars are distinguished from ordinary `$lowercase` variables. +(def (metavariable-name-char-uppercase? name) + (and (> (string-length name) 0) + (let ([c (string-ref name 0)]) + (or (char=? c #\_) (char-upper-case? c))))) + +;; PHP metavariable: a `variable_name` node `$NAME` whose NAME is uppercase-first +;; (and a valid metavariable name). Returns the name, or #f. +(def (php-metavariable-name-from-node n) + (and (string=? (node-type n) "variable_name") + (let ([text (node-text n)]) + (and (> (string-length text) 1) + (char=? (string-ref text 0) #\$) + (let ([name (substring text 1 (string-length text))]) + (and (valid-metavariable-name? name) + (metavariable-name-char-uppercase? name) + name)))))) + (def (go-skip-spaces source i len) (if (and (< i len) (char-whitespace? (string-ref source i))) (go-skip-spaces source (+ i 1) len) @@ -212,9 +234,13 @@ (let* ([name (string-slice source (+ i 4) j)] [token (ellipsis-metavariable-token language name)]) (values j state #f (add-string-reversed token acc) #t))))] + ;; In PHP `$x` is the variable sigil, so do NOT text-rewrite `$IDENT`; + ;; leave it as a variable_name and recognize metavars at the AST level + ;; (a variable_name whose name is uppercase-first). [(and (char=? (string-ref source i) #\$) (< (+ i 1) len) - (identifier-start? (string-ref source (+ i 1)))) + (identifier-start? (string-ref source (+ i 1))) + (not (php-language? language))) (let name-loop ([j (+ i 2)]) (if (and (< j len) (identifier-rest? (string-ref source j))) @@ -1150,6 +1176,12 @@ (let ([text (node-text pattern)]) (cond [(ellipsis-node? pattern) (list bindings)] + ;; PHP metavariable `$NAME` (uppercase) -> bind to the target node. + [(and (php-language? language) + (php-metavariable-name-from-node pattern)) + => (lambda (name) + (let ([next (bind-metavariable name target bindings)]) + (if next (list next) '())))] [(go-deep-expression-matches language pattern target bindings) => (lambda (matches) matches)] [(go-import-declaration-matches language pattern target bindings) @@ -1529,6 +1561,39 @@ (tree-close! (parse-result-tree result)) (loop (cdr scaffolds))))))))))) +;; PHP patterns are bare fragments (no `<?php`), which parse as raw text, so we +;; wrap them in `<?php ...` and extract the spanned node by byte range. +(define php-pattern-scaffolds + (list (cons "<?php " "") ; statements / declarations + (cons "<?php " ";"))) ; expressions (need a terminating `;`) + +(def (php-parse-pattern rewritten-pattern) + (let ([trimmed (string-trim rewritten-pattern)]) + (let loop ([scaffolds php-pattern-scaffolds]) + (if (null? scaffolds) + (values #f #f) + (let* ([prefix (caar scaffolds)] + [suffix (cdar scaffolds)] + [wrapped (string-append prefix trimmed suffix)] + [result (parse-target-string "php" wrapped)] + [root (parse-result-root result)]) + (if (or (not root) (parse-result-has-errors? result)) + (begin + (when root (node-close! root)) + (tree-close! (parse-result-tree result)) + (loop (cdr scaffolds))) + (let* ([hstart (utf8-length prefix)] + [hend (+ hstart (utf8-length trimmed))] + [hole (and (<= (node-start-byte root) hstart) + (>= (node-end-byte root) hend) + (go-deepest-node-containing root hstart hend))]) + (node-close! root) + (if hole + (values hole result) + (begin + (tree-close! (parse-result-tree result)) + (loop (cdr scaffolds))))))))))) + (def (run-structural-matches language normalized target-root initial-bindings) (let* ([chain-ellipsis? (chain-node-has-ellipsis? normalized)] [raw-matches @@ -1561,15 +1626,20 @@ target-root initial-bindings) (let ([rewritten-pattern (rewrite-metavariables language pattern-source)]) - (if (go-language? language) - (let-values ([(normalized result) (go-parse-pattern rewritten-pattern)]) + (cond + [(or (go-language? language) (php-language? language)) + (let-values ([(normalized result) + (if (go-language? language) + (go-parse-pattern rewritten-pattern) + (php-parse-pattern rewritten-pattern))]) (if (not normalized) '() (let ([matches (run-structural-matches language normalized target-root initial-bindings)]) (node-close! normalized) (tree-close! (parse-result-tree result)) - matches))) + matches)))] + [else (let* ([pattern-result (parse-target-string language rewritten-pattern)] [pattern-root (parse-result-root pattern-result)]) (let-values ([(normalized owned-nodes) @@ -1579,7 +1649,7 @@ (for-each node-close! owned-nodes) (when pattern-root (node-close! pattern-root)) (tree-close! (parse-result-tree pattern-result)) - matches)))))) + matches)))]))) (def (structural-pattern-matches language pattern-source target-root) (structural-pattern-matches-with-bindings --- a/src/semgrep/parse/parse-target.ss +++ b/src/semgrep/parse/parse-target.ss @@ -16,6 +16,7 @@ [(string=? canonical "javascript") 'javascript] [(string=? canonical "typescript") 'javascript] [(string=? canonical "go") 'go] + [(string=? canonical "php") 'php] [else (error 'parse-target-string "unsupported language in current MVP" language)]))) --- a/src/semgrep/scan.ss +++ b/src/semgrep/scan.ss @@ -815,7 +815,6 @@ (string=? canonical "yaml") (string=? canonical "c") (string=? canonical "terraform") - (string=? canonical "php") (string=? canonical "java") (string=? canonical "csharp") (string=? canonical "swift")