diff --git a/tests/test_tinycss2.py b/tests/test_tinycss2.py index b10f5c9..fb1130a 100644 --- a/tests/test_tinycss2.py +++ b/tests/test_tinycss2.py @@ -153,6 +153,20 @@ def test_nth(input): return parse_nth(input) +@pytest.mark.parametrize('invalid', ['+', '+/**/', 'n+', 'n +', '-n-', '2n +']) +def test_nth_invalid_does_not_crash(invalid): + # Truncated/invalid An+B fragments must return None per parse_nth's + # documented contract, not raise StopIteration or AttributeError. + assert parse_nth(invalid) is None + + +def test_nth_leading_plus_whitespace_still_invalid(): + # The fix for the above must not make '+ n' (whitespace after a leading + # '+') accidentally valid: only '+n' is a valid nth expression. + assert parse_nth('+n') == (1, 0) + assert parse_nth('+ n') is None + + def _number(value): if value is None: return 'none' diff --git a/tinycss2/nth.py b/tinycss2/nth.py index c5e924b..225a3ac 100644 --- a/tinycss2/nth.py +++ b/tinycss2/nth.py @@ -59,8 +59,10 @@ def parse_nth(input): if match: return parse_end(tokens, 1, int(match.group(1))) elif token == '+': - token = next(tokens) # Whitespace after an initial '+' is invalid. - if token.type == 'ident': + # Whitespace after an initial '+' is invalid, so the next token is read + # without skipping it. ``None`` is used when the iterator is exhausted. + token = next(tokens, None) + if token is not None and token.type == 'ident': ident = token.lower_value if ident == 'n': return parse_b(tokens, 1) @@ -87,7 +89,7 @@ def parse_b(tokens, a): def parse_signless_b(tokens, a, b_sign): token = _next_significant(tokens) - if (token.type == 'number' and token.is_integer and + if (token is not None and token.type == 'number' and token.is_integer and token.representation[0] not in '-+'): return parse_end(tokens, a, b_sign * token.int_value)