Extract Semgrep Python return scanners
ober
60591bc6d82048b68e17c3c356d786551a179793
--- a/SEMGREP_JERBOA_IMPLEMENTATION.md +++ b/SEMGREP_JERBOA_IMPLEMENTATION.md @@ -503,6 +503,8 @@ Completed in the repo: - extracted Python f-string, interpolated call/assignment, PEP 614 decorator, and f-string-equivalence fallback scanners into `src/semgrep/engine/py-fstring-scan.ss` + - extracted Python `return "..."` and `[...]` fallback scanners into + `src/semgrep/engine/py-return-scan.ss` - exported shared Python line-assignment analysis from `src/semgrep/engine/py-constant-prop.ss` so remaining Python fallbacks in `scan.ss` can reuse one assignment-info implementation @@ -523,8 +525,9 @@ Remaining Phase 0 work: - Continue splitting `src/semgrep/scan.ss` into engine, targeting, text-mode, structural dispatch, taint, and result/output-adjacent modules. The Python constant-propagation and f-string/interpolated fallback surfaces are now - out; the next meaningful cuts are the remaining Python special-case scanners - plus the larger language dispatch and taint sections. + out, and the remaining Python return/list special cases are out too; the + next meaningful cuts are the Python import-local/equivalence special-case + scanners plus the larger language dispatch and taint sections. - Run broader upstream sweeps and grow the expected-fail baselines from real sampled data instead of one narrow seed run. new file mode 100644 --- /dev/null +++ b/lib/semgrep/engine/py-return-scan.sls @@ -0,0 +1,210 @@ +#!chezscheme +;;; Generated by jerbuild — DO NOT EDIT +;;; Source: src/semgrep/engine/py-return-scan.ss + +(library (semgrep engine py-return-scan) + (export + scan-python-return-string-ellipsis-pattern + scan-python-list-ellipsis-pattern) + (import + (except (chezscheme) make-hash-table hash-table? sort sort! + printf fprintf format path-extension path-absolute? + with-input-from-string with-output-to-string iota \x31;+ + \x31;- partition make-date make-time meta atom?) + (except (jerboa prelude) meta atom?) + (semgrep engine regex-support) + (semgrep engine text-support)) + (def (sg-string-prefix? prefix s) + (let ([prefix-len (string-length prefix)] + [len (string-length s)]) + (and (<= prefix-len len) + (string=? (substring s 0 prefix-len) prefix)))) + (def (identifier-token-char? ch) + (or (char-alphabetic? ch) + (char-numeric? ch) + (char=? ch #\_))) + (def (token-start-boundary? source index) + (or (= index 0) + (not (identifier-token-char? + (string-ref source (- index 1)))))) + (def (token-end-boundary? source index) + (or (= index (string-length source)) + (not (identifier-token-char? (string-ref source index))))) + (def (previous-nonspace-index source index) + (let loop ([i (- index 1)]) + (cond + [(< i 0) #f] + [(char-whitespace? (string-ref source i)) (loop (- i 1))] + [else i]))) + (def (find-matching-close-paren source open-index) + (let ([len (string-length source)]) + (let loop ([i open-index] + [depth 0] + [state 'normal] + [escaped? #f]) + (cond + [(>= i len) #f] + [(eq? state 'normal) + (let ([ch (string-ref source i)]) + (cond + [(char=? ch #\") (loop (+ i 1) depth 'double #f)] + [(char=? ch #\') (loop (+ i 1) depth 'single #f)] + [(char=? ch #\`) (loop (+ i 1) depth 'backtick #f)] + [(char=? ch #\() (loop (+ i 1) (+ depth 1) state #f)] + [(char=? ch #\)) + (if (= depth 1) + (+ i 1) + (loop (+ i 1) (max 0 (- depth 1)) state #f))] + [else (loop (+ i 1) depth state #f)]))] + [escaped? (loop (+ i 1) depth state #f)] + [(char=? (string-ref source i) #\\) + (loop (+ i 1) depth state #t)] + [(and (eq? state 'double) + (char=? (string-ref source i) #\")) + (loop (+ i 1) depth 'normal #f)] + [(and (eq? state 'single) + (char=? (string-ref source i) #\')) + (loop (+ i 1) depth 'normal #f)] + [(and (eq? state 'backtick) + (char=? (string-ref source i) #\`)) + (loop (+ i 1) depth 'normal #f)] + [else (loop (+ i 1) depth state #f)])))) + (def (find-matching-close-bracket source open-index) + (let ([len (string-length source)]) + (let loop ([i open-index] + [depth 0] + [state 'normal] + [escaped? #f]) + (cond + [(>= i len) #f] + [(eq? state 'normal) + (let ([ch (string-ref source i)]) + (cond + [(char=? ch #\") (loop (+ i 1) depth 'double #f)] + [(char=? ch #\') (loop (+ i 1) depth 'single #f)] + [(char=? ch #\[) (loop (+ i 1) (+ depth 1) state #f)] + [(char=? ch #\]) + (if (= depth 1) + (+ i 1) + (loop (+ i 1) (max 0 (- depth 1)) state #f))] + [else (loop (+ i 1) depth state #f)]))] + [escaped? (loop (+ i 1) depth state #f)] + [(char=? (string-ref source i) #\\) + (loop (+ i 1) depth state #t)] + [(and (eq? state 'double) + (char=? (string-ref source i) #\")) + (loop (+ i 1) depth 'normal #f)] + [(and (eq? state 'single) + (char=? (string-ref source i) #\')) + (loop (+ i 1) depth 'normal #f)] + [else (loop (+ i 1) depth state #f)])))) + (def (python-simple-string-literal-prefix-char? ch) + (or (char=? ch #\r) + (char=? ch #\R) + (char=? ch #\u) + (char=? ch #\U) + (char=? ch #\b) + (char=? ch #\B) + (char=? ch #\f) + (char=? ch #\F))) + (def (python-string-literal-at? source i end) + (let loop ([j i]) + (cond + [(>= j end) #f] + [(or (char=? (string-ref source j) #\") + (char=? (string-ref source j) #\')) + #t] + [(python-simple-string-literal-prefix-char? + (string-ref source j)) + (loop (+ j 1))] + [else #f]))) + (def (python-range-has-string-literal? source start end) + (let loop ([i start]) + (and (< i end) + (or (and (token-start-boundary? source i) + (python-string-literal-at? source i end)) + (loop (+ i 1)))))) + (def (python-list-literal-open? source index) + (let ([prev (previous-nonspace-index source index)]) + (or (not prev) + (let ([ch (string-ref source prev)]) + (not (or (identifier-token-char? ch) + (char=? ch #\]) + (char=? ch #\)) + (char=? ch #\") + (char=? ch #\'))))))) + (def (python-return-string-ellipsis-pattern? pattern) + (string=? (string-trim pattern) "return \"...\"")) + (def (python-return-statement-end source first line-end) + (let ([expr-start (skip-horizontal-forward + source + (+ first (string-length "return")))]) + (if (and (< expr-start (string-length source)) + (char=? (string-ref source expr-start) #\()) + (or (find-matching-close-paren source expr-start) line-end) + line-end))) + (def (scan-python-return-string-ellipsis-pattern rule path source pattern initial-bindings) + (and (python-return-string-ellipsis-pattern? pattern) + (let ([len (string-length source)]) + (let loop ([line-start 0] [acc '()]) + (if (> line-start len) + (nonempty-findings (reverse acc)) + (let* ([line-end (line-end-after source line-start)] + [first (line-first-nonspace + source + line-start + line-end)] + [line (substring source first line-end)] + [statement-end (and (sg-string-prefix? + "return" + line) + (let ([after (+ first + (string-length + "return"))]) + (and (token-end-boundary? + source + after) + (python-return-statement-end + source + first + line-end))))] + [finding (and statement-end + (python-range-has-string-literal? + source + first + statement-end) + (finding-for-range-with-bindings rule path source first + statement-end + initial-bindings))] + [next (if (< line-end len) + (+ line-end 1) + (+ len 1))]) + (loop next (if finding (cons finding acc) acc)))))))) + (def (scan-python-list-ellipsis-pattern rule path source + pattern initial-bindings) + (and (string=? (string-trim pattern) "[...]") + (let ([len (string-length source)]) + (let loop ([start 0] [acc '()]) + (if (>= start len) + (nonempty-findings (reverse acc)) + (let ([open (string-find-substring-from + source + "[" + start)]) + (if (not open) + (nonempty-findings (reverse acc)) + (let* ([close (and (python-list-literal-open? + source + open) + (find-matching-close-bracket + source + open))] + [finding (and close + (finding-for-range-with-bindings rule path source open + close initial-bindings))] + [next (if close + (max (+ open 1) close) + (+ open 1))]) + (loop + next + (if finding (cons finding acc) acc))))))))))) --- a/lib/semgrep/scan.sls +++ b/lib/semgrep/scan.sls @@ -30,6 +30,7 @@ (semgrep engine py-cp-scan) (semgrep engine py-constant-prop) (semgrep engine py-fstring-scan) + (semgrep engine py-return-scan) (semgrep engine py-string-eval) (semgrep engine py-string-scan) (semgrep engine regex-scan) (semgrep engine rule-plan) (semgrep engine regex-support) @@ -8196,32 +8197,6 @@ (< start end)) (finding-with-range finding source start end) finding))) - (def (python-simple-string-literal-prefix-char? ch) - (or (char=? ch #\r) - (char=? ch #\R) - (char=? ch #\u) - (char=? ch #\U) - (char=? ch #\b) - (char=? ch #\B) - (char=? ch #\f) - (char=? ch #\F))) - (def (python-string-literal-at? source i end) - (let loop ([j i]) - (cond - [(>= j end) #f] - [(or (char=? (string-ref source j) #\") - (char=? (string-ref source j) #\')) - #t] - [(python-simple-string-literal-prefix-char? - (string-ref source j)) - (loop (+ j 1))] - [else #f]))) - (def (python-range-has-string-literal? source start end) - (let loop ([i start]) - (and (< i end) - (or (and (token-start-boundary? source i) - (python-string-literal-at? source i end)) - (loop (+ i 1)))))) (def (find-matching-close-bracket source open-index) (let ([len (string-length source)]) (let loop ([i open-index] @@ -8251,90 +8226,6 @@ (char=? (string-ref source i) #\')) (loop (+ i 1) depth 'normal #f)] [else (loop (+ i 1) depth state #f)])))) - (def (python-list-literal-open? source index) - (let ([prev (previous-nonspace-index source index)]) - (or (not prev) - (let ([ch (string-ref source prev)]) - (not (or (identifier-token-char? ch) - (char=? ch #\]) - (char=? ch #\)) - (char=? ch #\") - (char=? ch #\'))))))) - (def (python-return-string-ellipsis-pattern? pattern) - (string=? (string-trim pattern) "return \"...\"")) - (def (python-return-statement-end source first line-end) - (let ([expr-start (skip-horizontal-forward - source - (+ first (string-length "return")))]) - (if (and (< expr-start (string-length source)) - (char=? (string-ref source expr-start) #\()) - (or (find-matching-close-paren source expr-start) line-end) - line-end))) - (def (scan-python-return-string-ellipsis-pattern rule path source pattern initial-bindings) - (and (python-return-string-ellipsis-pattern? pattern) - (let ([len (string-length source)]) - (let loop ([line-start 0] [acc '()]) - (if (> line-start len) - (nonempty-findings (reverse acc)) - (let* ([line-end (line-end-after source line-start)] - [first (line-first-nonspace - source - line-start - line-end)] - [line (substring source first line-end)] - [statement-end (and (sg-string-prefix? - "return" - line) - (let ([after (+ first - (string-length - "return"))]) - (and (token-end-boundary? - source - after) - (python-return-statement-end - source - first - line-end))))] - [finding (and statement-end - (python-range-has-string-literal? - source - first - statement-end) - (finding-for-range-with-bindings rule path source first - statement-end - initial-bindings))] - [next (if (< line-end len) - (+ line-end 1) - (+ len 1))]) - (loop next (if finding (cons finding acc) acc)))))))) - (def (scan-python-list-ellipsis-pattern rule path source - pattern initial-bindings) - (and (string=? (string-trim pattern) "[...]") - (let ([len (string-length source)]) - (let loop ([start 0] [acc '()]) - (if (>= start len) - (nonempty-findings (reverse acc)) - (let ([open (string-find-substring-from - source - "[" - start)]) - (if (not open) - (nonempty-findings (reverse acc)) - (let* ([close (and (python-list-literal-open? - source - open) - (find-matching-close-bracket - source - open))] - [finding (and close - (finding-for-range-with-bindings rule path source open - close initial-bindings))] - [next (if close - (max (+ open 1) close) - (+ open 1))]) - (loop - next - (if finding (cons finding acc) acc)))))))))) (def (python-block-end source header-start --- a/src/.jerbuild-hashes +++ b/src/.jerbuild-hashes @@ -1,4 +1,7 @@ -(("src/semgrep/engine/py-string-scan.ss" +(("src/semgrep/engine/py-return-scan.ss" + . + "B9F04A4640FFC26D") + ("src/semgrep/engine/py-string-scan.ss" . "DBAD99DB5C22F6A1") ("src/semgrep/util/literals.ss" . "8A094085551B216E") @@ -15,10 +18,10 @@ ("src/semgrep/engine/js-decorator-scan.ss" . "193758AD2E444FD6") - ("src/semgrep/engine/ts-type-scan.ss" . "B36BAF07D19F4419") - ("src/semgrep/output/text.ss" . "BE476CB84B807FBA") - ("src/semgrep/rule.ss" . "E12C108153C181FA") ("src/semgrep/schema/lang.ss" . "CAE2CA859C9A9FD0") + ("src/semgrep/rule.ss" . "E12C108153C181FA") + ("src/semgrep/output/text.ss" . "BE476CB84B807FBA") + ("src/semgrep/engine/ts-type-scan.ss" . "B36BAF07D19F4419") ("src/semgrep/source/offsets.ss" . "834EFDB706823794") ("src/semgrep/engine/regex-support.ss" . "9FCF118903259C97") ("src/semgrep/engine/py-constant-scan.ss" @@ -42,17 +45,17 @@ ("src/semgrep/output/json.ss" . "293881CFA2ADB7BC") ("src/semgrep/lang.ss" . "6982E07679D20836") ("src/semgrep/parse/parse-target.ss" . "97AA8FFEB12736DA") - ("src/semgrep/scan.ss" . "E17E372187C48088") + ("src/semgrep/scan.ss" . "D233411A6E763C9C") ("src/semgrep/engine/generic-scan.ss" . "F69D0ACD0DD62610") - ("src/semgrep/engine/js-constructor-scan.ss" - . - "6B226B8D0584A7A") ("src/semgrep/engine/py-constant-prop.ss" . "76462D0EB71F2180") + ("src/semgrep/engine/js-constructor-scan.ss" + . + "6B226B8D0584A7A") ("src/semgrep/engine/comparison.ss" . "5B5731915BCB8E6") ("src/semgrep/main.ss" . "A4EC9E7F2A09D25E") - ("src/semgrep/engine/text-support.ss" . "644AF29394C53045") ("src/semgrep/engine/ts-decorator-scan.ss" . - "610AA3A90D6A95F8")) + "610AA3A90D6A95F8") + ("src/semgrep/engine/text-support.ss" . "644AF29394C53045")) new file mode 100644 --- /dev/null +++ b/src/semgrep/engine/py-return-scan.ss @@ -0,0 +1,208 @@ +(export + scan-python-return-string-ellipsis-pattern + scan-python-list-ellipsis-pattern) + +(import (except (jerboa prelude) meta atom?) + (semgrep engine regex-support) + (semgrep engine text-support)) + +(def (sg-string-prefix? prefix s) + (let ([prefix-len (string-length prefix)] + [len (string-length s)]) + (and (<= prefix-len len) + (string=? (substring s 0 prefix-len) prefix)))) + +(def (identifier-token-char? ch) + (or (char-alphabetic? ch) + (char-numeric? ch) + (char=? ch #\_))) + +(def (token-start-boundary? source index) + (or (= index 0) + (not (identifier-token-char? (string-ref source (- index 1)))))) + +(def (token-end-boundary? source index) + (or (= index (string-length source)) + (not (identifier-token-char? (string-ref source index))))) + +(def (previous-nonspace-index source index) + (let loop ([i (- index 1)]) + (cond + [(< i 0) #f] + [(char-whitespace? (string-ref source i)) (loop (- i 1))] + [else i]))) + +(def (find-matching-close-paren source open-index) + (let ([len (string-length source)]) + (let loop ([i open-index] [depth 0] [state 'normal] [escaped? #f]) + (cond + [(>= i len) #f] + [(eq? state 'normal) + (let ([ch (string-ref source i)]) + (cond + [(char=? ch #\") (loop (+ i 1) depth 'double #f)] + [(char=? ch #\') (loop (+ i 1) depth 'single #f)] + [(char=? ch #\`) (loop (+ i 1) depth 'backtick #f)] + [(char=? ch #\() (loop (+ i 1) (+ depth 1) state #f)] + [(char=? ch #\)) + (if (= depth 1) + (+ i 1) + (loop (+ i 1) (max 0 (- depth 1)) state #f))] + [else (loop (+ i 1) depth state #f)]))] + [escaped? (loop (+ i 1) depth state #f)] + [(char=? (string-ref source i) #\\) + (loop (+ i 1) depth state #t)] + [(and (eq? state 'double) (char=? (string-ref source i) #\")) + (loop (+ i 1) depth 'normal #f)] + [(and (eq? state 'single) (char=? (string-ref source i) #\')) + (loop (+ i 1) depth 'normal #f)] + [(and (eq? state 'backtick) (char=? (string-ref source i) #\`)) + (loop (+ i 1) depth 'normal #f)] + [else (loop (+ i 1) depth state #f)])))) + +(def (find-matching-close-bracket source open-index) + (let ([len (string-length source)]) + (let loop ([i open-index] [depth 0] [state 'normal] [escaped? #f]) + (cond + [(>= i len) #f] + [(eq? state 'normal) + (let ([ch (string-ref source i)]) + (cond + [(char=? ch #\") (loop (+ i 1) depth 'double #f)] + [(char=? ch #\') (loop (+ i 1) depth 'single #f)] + [(char=? ch #\[) (loop (+ i 1) (+ depth 1) state #f)] + [(char=? ch #\]) + (if (= depth 1) + (+ i 1) + (loop (+ i 1) (max 0 (- depth 1)) state #f))] + [else (loop (+ i 1) depth state #f)]))] + [escaped? (loop (+ i 1) depth state #f)] + [(char=? (string-ref source i) #\\) + (loop (+ i 1) depth state #t)] + [(and (eq? state 'double) (char=? (string-ref source i) #\")) + (loop (+ i 1) depth 'normal #f)] + [(and (eq? state 'single) (char=? (string-ref source i) #\')) + (loop (+ i 1) depth 'normal #f)] + [else (loop (+ i 1) depth state #f)])))) + +(def (python-simple-string-literal-prefix-char? ch) + (or (char=? ch #\r) + (char=? ch #\R) + (char=? ch #\u) + (char=? ch #\U) + (char=? ch #\b) + (char=? ch #\B) + (char=? ch #\f) + (char=? ch #\F))) + +(def (python-string-literal-at? source i end) + (let loop ([j i]) + (cond + [(>= j end) #f] + [(or (char=? (string-ref source j) #\") + (char=? (string-ref source j) #\')) + #t] + [(python-simple-string-literal-prefix-char? (string-ref source j)) + (loop (+ j 1))] + [else #f]))) + +(def (python-range-has-string-literal? source start end) + (let loop ([i start]) + (and (< i end) + (or (and (token-start-boundary? source i) + (python-string-literal-at? source i end)) + (loop (+ i 1)))))) + +(def (python-list-literal-open? source index) + (let ([prev (previous-nonspace-index source index)]) + (or (not prev) + (let ([ch (string-ref source prev)]) + (not (or (identifier-token-char? ch) + (char=? ch #\]) + (char=? ch #\)) + (char=? ch #\") + (char=? ch #\'))))))) + +(def (python-return-string-ellipsis-pattern? pattern) + (string=? (string-trim pattern) "return \"...\"")) + +(def (python-return-statement-end source first line-end) + (let ([expr-start (skip-horizontal-forward + source + (+ first (string-length "return")))]) + (if (and (< expr-start (string-length source)) + (char=? (string-ref source expr-start) #\()) + (or (find-matching-close-paren source expr-start) line-end) + line-end))) + +(def (scan-python-return-string-ellipsis-pattern + rule + path + source + pattern + initial-bindings) + (and (python-return-string-ellipsis-pattern? pattern) + (let ([len (string-length source)]) + (let loop ([line-start 0] [acc '()]) + (if (> line-start len) + (nonempty-findings (reverse acc)) + (let* ([line-end (line-end-after source line-start)] + [first (line-first-nonspace source line-start line-end)] + [line (substring source first line-end)] + [statement-end + (and (sg-string-prefix? "return" line) + (let ([after (+ first (string-length "return"))]) + (and (token-end-boundary? source after) + (python-return-statement-end + source + first + line-end))))] + [finding + (and statement-end + (python-range-has-string-literal? + source + first + statement-end) + (finding-for-range-with-bindings + rule + path + source + first + statement-end + initial-bindings))] + [next (if (< line-end len) (+ line-end 1) (+ len 1))]) + (loop next + (if finding (cons finding acc) acc)))))))) + +(def (scan-python-list-ellipsis-pattern + rule + path + source + pattern + initial-bindings) + (and (string=? (string-trim pattern) "[...]") + (let ([len (string-length source)]) + (let loop ([start 0] [acc '()]) + (if (>= start len) + (nonempty-findings (reverse acc)) + (let ([open (string-find-substring-from source "[" start)]) + (if (not open) + (nonempty-findings (reverse acc)) + (let* ([close (and (python-list-literal-open? source open) + (find-matching-close-bracket + source + open))] + [finding + (and close + (finding-for-range-with-bindings + rule + path + source + open + close + initial-bindings))] + [next (if close + (max (+ open 1) close) + (+ open 1))]) + (loop next + (if finding (cons finding acc) acc)))))))))) --- a/src/semgrep/scan.ss +++ b/src/semgrep/scan.ss @@ -29,6 +29,7 @@ (semgrep engine py-cp-scan) (semgrep engine py-constant-prop) (semgrep engine py-fstring-scan) + (semgrep engine py-return-scan) (semgrep engine py-string-eval) (semgrep engine py-string-scan) (semgrep engine regex-scan) @@ -7909,34 +7910,6 @@ (finding-with-range finding source start end) finding))) -(def (python-simple-string-literal-prefix-char? ch) - (or (char=? ch #\r) - (char=? ch #\R) - (char=? ch #\u) - (char=? ch #\U) - (char=? ch #\b) - (char=? ch #\B) - (char=? ch #\f) - (char=? ch #\F))) - -(def (python-string-literal-at? source i end) - (let loop ([j i]) - (cond - [(>= j end) #f] - [(or (char=? (string-ref source j) #\") - (char=? (string-ref source j) #\')) - #t] - [(python-simple-string-literal-prefix-char? (string-ref source j)) - (loop (+ j 1))] - [else #f]))) - -(def (python-range-has-string-literal? source start end) - (let loop ([i start]) - (and (< i end) - (or (and (token-start-boundary? source i) - (python-string-literal-at? source i end)) - (loop (+ i 1)))))) - (def (find-matching-close-bracket source open-index) (let ([len (string-length source)]) (let loop ([i open-index] [depth 0] [state 'normal] [escaped? #f]) @@ -7962,100 +7935,6 @@ (loop (+ i 1) depth 'normal #f)] [else (loop (+ i 1) depth state #f)])))) -(def (python-list-literal-open? source index) - (let ([prev (previous-nonspace-index source index)]) - (or (not prev) - (let ([ch (string-ref source prev)]) - (not (or (identifier-token-char? ch) - (char=? ch #\]) - (char=? ch #\)) - (char=? ch #\") - (char=? ch #\'))))))) - -(def (python-return-string-ellipsis-pattern? pattern) - (string=? (string-trim pattern) "return \"...\"")) - -(def (python-return-statement-end source first line-end) - (let ([expr-start (skip-horizontal-forward - source - (+ first (string-length "return")))]) - (if (and (< expr-start (string-length source)) - (char=? (string-ref source expr-start) #\()) - (or (find-matching-close-paren source expr-start) line-end) - line-end))) - -(def (scan-python-return-string-ellipsis-pattern - rule - path - source - pattern - initial-bindings) - (and (python-return-string-ellipsis-pattern? pattern) - (let ([len (string-length source)]) - (let loop ([line-start 0] [acc '()]) - (if (> line-start len) - (nonempty-findings (reverse acc)) - (let* ([line-end (line-end-after source line-start)] - [first (line-first-nonspace source line-start line-end)] - [line (substring source first line-end)] - [statement-end - (and (sg-string-prefix? "return" line) - (let ([after (+ first (string-length "return"))]) - (and (token-end-boundary? source after) - (python-return-statement-end - source - first - line-end))))] - [finding - (and statement-end - (python-range-has-string-literal? - source - first - statement-end) - (finding-for-range-with-bindings - rule - path - source - first - statement-end - initial-bindings))] - [next (if (< line-end len) (+ line-end 1) (+ len 1))]) - (loop next - (if finding (cons finding acc) acc)))))))) - -(def (scan-python-list-ellipsis-pattern - rule - path - source - pattern - initial-bindings) - (and (string=? (string-trim pattern) "[...]") - (let ([len (string-length source)]) - (let loop ([start 0] [acc '()]) - (if (>= start len) - (nonempty-findings (reverse acc)) - (let ([open (string-find-substring-from source "[" start)]) - (if (not open) - (nonempty-findings (reverse acc)) - (let* ([close (and (python-list-literal-open? source open) - (find-matching-close-bracket - source - open))] - [finding - (and close - (finding-for-range-with-bindings - rule - path - source - open - close - initial-bindings))] - [next (if close - (max (+ open 1) close) - (+ open 1))]) - (loop next - (if finding (cons finding acc) acc)))))))))) - (def (python-block-end source header-start include-if-chain?) (let* ([header-indent (line-indent-at-offset source header-start)] [header-end (line-end-after source header-start)]