From b78aad09362a17717720e01626356005c5ef32c0 Mon Sep 17 00:00:00 2001 From: Valentin Courdy Date: Thu, 2 Jul 2026 13:36:15 +0200 Subject: [PATCH] Fix ValueError when formatting procedures using label syntax (bar:LOOP) A colon immediately preceded by a word character is a label separator (MySQL/Oracle label:LOOP), never a bind-variable prefix. The tokenizer lexed bar:LOOP as WORD('bar') + VARIABLE(':LOOP'), swallowing the LOOP keyword: the indent-opener branch in SqlFormatter::format() never fired while END LOOP and END still decremented the indent level, driving it to -1 and crashing str_repeat() with a ValueError. Guard the ':' alternative of the VARIABLE pattern with a negative lookbehind (? --- src/Tokenizer.php | 2 +- tests/TokenizerTest.php | 16 ++++++++++++++++ tests/clihighlight.txt | 6 ++++++ tests/compress.txt | 2 ++ tests/format-highlight.html | 6 ++++++ tests/format.txt | 6 ++++++ tests/highlight.html | 4 ++++ tests/sql.sql | 4 ++++ 8 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/Tokenizer.php b/src/Tokenizer.php index a62bfe3..c8be330 100644 --- a/src/Tokenizer.php +++ b/src/Tokenizer.php @@ -862,7 +862,7 @@ private function makeTokenizeRegexes(): array ) EOD, // User-defined variable, possibly with quoted name - Token::TOKEN_TYPE_VARIABLE => '[@:](?:[\w.$]++|(?&t_' . Token::TOKEN_TYPE_BACKTICK_QUOTE . ')|(?&t_' . Token::TOKEN_TYPE_QUOTE . '))', + Token::TOKEN_TYPE_VARIABLE => '(?:@|(? '(?:\d+(?:\.\d+)?|0x[\da-fA-F]+|0b[01]+)(?=$|\s|"\'`|' . $regexBoundaries . ')', // punctuation and symbols diff --git a/tests/TokenizerTest.php b/tests/TokenizerTest.php index 73d0b40..06c531b 100644 --- a/tests/TokenizerTest.php +++ b/tests/TokenizerTest.php @@ -1672,6 +1672,22 @@ public static function tokenizeData(): Generator ], 'select json #> null', ]; + + yield 'label directly followed by LOOP is not a bind variable' => [ + [ + new Token(Token::TOKEN_TYPE_WORD, 'bar'), + new Token(Token::TOKEN_TYPE_BOUNDARY, ':'), + new Token(Token::TOKEN_TYPE_WORD, 'loop'), + ], + 'bar:loop', + ]; + + yield 'bind variable' => [ + [ + new Token(Token::TOKEN_TYPE_VARIABLE, ':param'), + ], + ':param', + ]; } public function testTokenizeLongConcat(): void diff --git a/tests/clihighlight.txt b/tests/clihighlight.txt index ceaf391..5cef9b1 100644 --- a/tests/clihighlight.txt +++ b/tests/clihighlight.txt @@ -1214,3 +1214,9 @@ MY_NON_TOP_LEVEL_KEYWORD_FX_3(); --- SELECT text ~* '\w+' +--- +CREATE PROCEDURE `foo`() BEGIN + bar : LOOP + -- Nothing + END LOOP bar; +END diff --git a/tests/compress.txt b/tests/compress.txt index 6ec44ca..67a1594 100644 --- a/tests/compress.txt +++ b/tests/compress.txt @@ -117,3 +117,5 @@ SELECT '{}'::json #> '{}' SELECT vector1 <#> vector2 --- SELECT text ~* '\w+' +--- +CREATE PROCEDURE `foo`() BEGIN bar:LOOP END LOOP bar; END diff --git a/tests/format-highlight.html b/tests/format-highlight.html index bafb6f2..ef0bfac 100644 --- a/tests/format-highlight.html +++ b/tests/format-highlight.html @@ -1214,3 +1214,9 @@ ---
SELECT
   text ~* '\w+'
+--- +
CREATE PROCEDURE `foo`() BEGIN
+  bar : LOOP
+    -- Nothing
+  END LOOP bar;
+END
diff --git a/tests/format.txt b/tests/format.txt index d3bea98..3c4b0d3 100644 --- a/tests/format.txt +++ b/tests/format.txt @@ -1212,3 +1212,9 @@ SELECT --- SELECT text ~* '\w+' +--- +CREATE PROCEDURE `foo`() BEGIN + bar : LOOP + -- Nothing + END LOOP bar; +END diff --git a/tests/highlight.html b/tests/highlight.html index 9d24e6c..d6b668e 100644 --- a/tests/highlight.html +++ b/tests/highlight.html @@ -431,3 +431,7 @@
SELECT vector1 <#> vector2
---
SELECT text ~* '\w+'
+--- +
CREATE PROCEDURE `foo`() BEGIN bar:LOOP
+  -- Nothing
+END LOOP bar; END
diff --git a/tests/sql.sql b/tests/sql.sql index 2c72c6f..8e3b57a 100644 --- a/tests/sql.sql +++ b/tests/sql.sql @@ -431,3 +431,7 @@ SELECT '{}'::json #> '{}' SELECT vector1 <#> vector2 --- SELECT text ~* '\w+' +--- +CREATE PROCEDURE `foo`() BEGIN bar:LOOP + -- Nothing +END LOOP bar; END