Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ Semantic versioning in our case means:
But, in the future we might change the configuration names/logic,
change the client facing API, change code conventions significantly, etc.

## Unreleased

### Features

- Adds `WPS367`: forbid redundant trailing colon in slice, #1071

## 1.6.2

### Bugfixes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
'3:None:2',
'3:7:None',
'3:7:1',
'::1',
'1::1',
],
)
def test_one_redundant_subscript(
Expand Down Expand Up @@ -44,6 +46,7 @@ def test_one_redundant_subscript(
'None:None',
'3:None:1',
':None:None',
'0::1',
],
)
def test_two_redundant_subscript(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import pytest

from wemake_python_styleguide.violations.consistency import (
RedundantTrailingSliceViolation,
)
from wemake_python_styleguide.visitors.tokenize.subscripts import (
RedundantTrailingSliceVisitor,
)

# Wrong:
trailing_colon_cases = [
'a[1:4:]',
'a[1::]',
'a[:4:]',
'a[None::]',
'a[1:None:]',
'a[1 + 2::]',
]

# Correct:
correct_cases = [
'a[1:4]',
'a[1:]',
'a[:4]',
'a[::]', # caught by NonStrictSliceOperationsViolation
'a[1:4:5]',
'a[1:4:None]',
'a[1]',
'a[1:4:1]', # caught by RedundantSubscriptViolation, not ours
'a[b[1:4:5]]', # nested valid slice
'a[b[1:4:]]', # nested invalid — should flag inner
'a[{1: 2}]', # dict literal inside subscript
'a[(1, 2)]', # tuple inside subscript
'a[lambda x: x]', # lambda inside subscript
]


@pytest.mark.parametrize('code', trailing_colon_cases)
def test_redundant_trailing_colon(
parse_tokens,
assert_errors,
default_options,
code,
):
"""Ensure trailing colon in slice is forbidden."""
file_tokens = parse_tokens(code)
visitor = RedundantTrailingSliceVisitor(
default_options,
file_tokens=file_tokens,
)
visitor.run()
assert_errors(visitor, [RedundantTrailingSliceViolation])


@pytest.mark.parametrize('code', correct_cases)
def test_correct_slice_not_flagged(
parse_tokens,
assert_errors,
default_options,
code,
):
"""Ensure valid slices do not raise violation."""
file_tokens = parse_tokens(code)
visitor = RedundantTrailingSliceVisitor(
default_options,
file_tokens=file_tokens,
)
visitor.run()
assert_errors(visitor, [])
2 changes: 2 additions & 0 deletions wemake_python_styleguide/presets/types/file_tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
conditions,
primitives,
statements,
subscripts,
syntax,
)

Expand All @@ -21,4 +22,5 @@
statements.MultilineStringVisitor,
conditions.IfElseVisitor,
primitives.MultilineFormattedStringTokenVisitor,
subscripts.RedundantTrailingSliceVisitor,
)
33 changes: 33 additions & 0 deletions wemake_python_styleguide/violations/consistency.py
Original file line number Diff line number Diff line change
Expand Up @@ -2511,3 +2511,36 @@ class MeaninglessBooleanOperationViolation(ASTViolation):

error_template = 'Found meaningless boolean operation'
code = 366


@final
class RedundantTrailingSliceViolation(TokenizeViolation):
"""
Forbid redundant trailing colon in subscript slice.

Reasoning:
Trailing colon inside a subscript slice does not change behavior.
For example, ``a[1:4:]`` is parsed identically to ``a[1:4]``.
It adds visual noise for no reason.

Solution:
Remove the trailing colon.

Example::

# Correct:
a[1:4]
a[1:]
a[:4]

# Wrong:
a[1:4:]
a[1::]
a[:4:]

.. versionadded:: 1.7.0

"""

error_template = 'Found redundant trailing slice colon'
code = 367
95 changes: 95 additions & 0 deletions wemake_python_styleguide/visitors/tokenize/subscripts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import tokenize
from typing import Final, TypedDict, final

from wemake_python_styleguide.violations import consistency
from wemake_python_styleguide.visitors.base import BaseTokenVisitor

_INSIGNIFICANT_TYPES: Final = frozenset((
tokenize.NL,
tokenize.NEWLINE,
tokenize.COMMENT,
tokenize.INDENT,
tokenize.DEDENT,
tokenize.ENCODING,
tokenize.ENDMARKER,
))


_COLON_COUNT: Final = 'colon_count'
_LAST_WAS_COLON: Final = 'last_was_colon'
_LAST_COLON: Final = 'last_colon'
_HAS_NON_COLON: Final = 'has_non_colon'


@final
class _SliceBracketState(TypedDict):
Comment thread
f1sherFM marked this conversation as resolved.
"""Mutable state tracker for a single ``[...]`` bracket level."""

colon_count: int
last_was_colon: bool
last_colon: tokenize.TokenInfo | None
has_non_colon: bool


@final
class RedundantTrailingSliceVisitor(BaseTokenVisitor):
"""Check for redundant trailing colon in subscript slices."""

def __init__(self, *args, **kwargs) -> None:
"""Initialize state for bracket tracking."""
super().__init__(*args, **kwargs)
self._bracket_stack: list[_SliceBracketState] = []

def visit(self, token: tokenize.TokenInfo) -> None:
"""Track brackets and colons to detect trailing colon."""
self._maybe_push_bracket(token)
self._maybe_pop_bracket(token)
self._maybe_track_colon(token)
self._maybe_track_other(token)
super().visit(token)

def _maybe_push_bracket(self, token: tokenize.TokenInfo) -> None:
if token.exact_type == tokenize.OP and token.string == '[':
self._bracket_stack.append({
_COLON_COUNT: 0,
_LAST_WAS_COLON: False,
_LAST_COLON: None,
_HAS_NON_COLON: False,
})

def _maybe_pop_bracket(self, token: tokenize.TokenInfo) -> None:
if not (token.exact_type == tokenize.OP and token.string == ']'):
return
if not self._bracket_stack:
return
entry = self._bracket_stack.pop()
if (
entry[_LAST_WAS_COLON]
and entry[_COLON_COUNT] >= 2
and entry[_HAS_NON_COLON]
):
self.add_violation(
consistency.RedundantTrailingSliceViolation(
entry[_LAST_COLON],
),
)

def _maybe_track_colon(self, token: tokenize.TokenInfo) -> None:
if not (
token.exact_type == tokenize.OP
and token.string == ':'
and self._bracket_stack
):
return
entry = self._bracket_stack[-1]
entry[_COLON_COUNT] += 1
entry[_LAST_WAS_COLON] = True
entry[_LAST_COLON] = token

def _maybe_track_other(self, token: tokenize.TokenInfo) -> None:
if token.type in _INSIGNIFICANT_TYPES or not self._bracket_stack:
return
entry = self._bracket_stack[-1]
entry[_LAST_WAS_COLON] = False
if token.string != ':':
entry[_HAS_NON_COLON] = True
Loading