From a9f82ca546b0f3756b071b7ed38b0dc3fb389a39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Afonso=20Janu=C3=A1rio?= Date: Thu, 3 Sep 2026 10:44:36 +0100 Subject: [PATCH] Guard case/parenthesis alignment against malformed input reindent_aligned=True and strip_whitespace=True could both crash on certain malformed SQL where grouping doesn't produce the shape these filters expect. For CASE without a matching END as a direct child (it can end up nested inside a sibling group instead), _process_case would grab a None token and hand it to insert_before, which blew up with ValueError: None is not in list. For a parenthesis whose contents collapse into a single nested group during parsing (e.g. "( AS )"), _stripws_parenthesis assumed at least two direct children and raised IndexError on tokens[1]. Both filters now bail out gracefully instead of assuming a well-formed tree, with regression tests for each case using the exact reproducers from the two issues. --- sqlparse/filters/aligned_indent.py | 6 ++++++ sqlparse/filters/others.py | 10 +++++++--- tests/test_regressions.py | 16 ++++++++++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/sqlparse/filters/aligned_indent.py b/sqlparse/filters/aligned_indent.py index 6ac99d62..0b94095b 100644 --- a/sqlparse/filters/aligned_indent.py +++ b/sqlparse/filters/aligned_indent.py @@ -70,6 +70,12 @@ def _process_case(self, tlist): cases = tlist.get_cases(skip_ws=True) # align the end as well end_token = tlist.token_next_by(m=(T.Keyword, 'END'))[1] + if end_token is None: + # A malformed CASE expression can leave END nested inside a + # sibling group instead of being a direct child of this token + # list (get_cases and token_next_by only look at direct + # children), so there's nothing valid to align it against. + return cases.append((None, [end_token])) condition_width = [len(' '.join(map(str, cond))) if cond else 0 diff --git a/sqlparse/filters/others.py b/sqlparse/filters/others.py index 95bc436c..f6dc7f8d 100644 --- a/sqlparse/filters/others.py +++ b/sqlparse/filters/others.py @@ -112,11 +112,15 @@ def _stripws_identifierlist(self, tlist): return self._stripws_default(tlist) def _stripws_parenthesis(self, tlist): - while tlist.tokens[1].is_whitespace: + # A malformed parenthesis can end up with only one or two direct + # children once grouping is done (e.g. the whole inside collapses + # into a single nested group), so don't assume tokens[1]/tokens[-2] + # are always there. + while len(tlist.tokens) > 2 and tlist.tokens[1].is_whitespace: tlist.tokens.pop(1) - while tlist.tokens[-2].is_whitespace: + while len(tlist.tokens) > 2 and tlist.tokens[-2].is_whitespace: tlist.tokens.pop(-2) - if tlist.tokens[-2].is_group: + if len(tlist.tokens) > 1 and tlist.tokens[-2].is_group: # save to remove the last whitespace while tlist.tokens[-2].tokens[-1].is_whitespace: tlist.tokens[-2].tokens.pop(-1) diff --git a/tests/test_regressions.py b/tests/test_regressions.py index aca7f7b3..b5fc7913 100644 --- a/tests/test_regressions.py +++ b/tests/test_regressions.py @@ -516,3 +516,19 @@ def limit_recursion(): def test_max_recursion(limit_recursion): with pytest.raises(SQLParseError): sqlparse.parse('[' * 1000 + ']' * 1000) + + +def test_stripws_parenthesis_with_no_direct_children_issue885(): + # Malformed input can collapse the whole parenthesis body into a single + # nested group, leaving fewer than two direct children of the + # Parenthesis token list. This used to raise IndexError. + assert sqlparse.format('( AS )', strip_whitespace=True) == '( AS )' + + +def test_aligned_indent_case_without_direct_end_issue886(): + # Malformed CASE expressions can end up with the END keyword nested + # inside a sibling group rather than being a direct child of the Case + # token list, so token_next_by can't find it and used to raise + # ValueError when that None was later used as an insertion point. + sql = "CASE 'a' := WHERE END SELECT GO # ->>" + assert sqlparse.format(sql, reindent_aligned=True) == sql