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
61 changes: 61 additions & 0 deletions tests/fixtures/noqa/noqa.py
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,67 @@ def many_raises_function(parameter): # noqa: WPS238
case _:
...

def too_many_locals(): # noqa: WPS482

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In test_noqa only one example is needed, here we test:

  • noqa position
  • full plugin integration

# comment 1
abcd = 1
# comment 2
abcdx = 2
# comment 3
# comment 4
# comment 5
# comment 6
# comment 7
# comment 8
# comment 9
# comment 10
# comment 11
# comment 12
# comment 13
# comment 14
# comment 15
# comment 16

...

def too_many_nested():
# comment 1
abcd = 1
# comment 2
abcdx = 2
def factory():
# this is a edge case
# comment 2
# comment 3
# comment 4
# comment 5
# comment 6
# comment 7
# comment 8
# comment 9
# comment 10
# comment 11
# comment 12
# comment 13
# comment 14
# comment 15
...
def decorator():
# comment 1
# comment 2
# comment 3
# comment 4
# comment 5
# comment 6
# comment 7
# comment 8
...
# comment 6
# comment 7
# comment 8
# comment 9
# comment 10
...


my_print("""
text
Expand Down
1 change: 1 addition & 0 deletions tests/test_checker/test_noqa.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@
'WPS479': 0,
'WPS480': 0, # only triggers on 3.12+
'WPS481': 10,
'WPS482': 1,
'WPS500': 1,
'WPS501': 1,
'WPS502': 0, # disabled since 1.0.0
Expand Down
2 changes: 2 additions & 0 deletions tests/test_checker/test_presets.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from wemake_python_styleguide.checker import Checker
from wemake_python_styleguide.visitors.base import (
BaseFilenameVisitor,
BaseNodeTokenVisitor,
BaseNodeVisitor,
BaseTokenVisitor,
BaseVisitor,
Expand All @@ -18,6 +19,7 @@ def _is_visitor_class(cls) -> bool:
base_classes = {
BaseFilenameVisitor,
BaseNodeVisitor,
BaseNodeTokenVisitor,
BaseTokenVisitor,
BaseVisitor,
}
Expand Down
15 changes: 15 additions & 0 deletions wemake_python_styleguide/logic/tokens/comments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from collections.abc import Sequence
from tokenize import COMMENT, TokenInfo


def count_comments_in_range(
file_tokens: Sequence[TokenInfo],
start_line: int,
end_line: int,
) -> int:
"""Counts comment tokens within a given line range."""
return sum(
1
for token in file_tokens
if token.type == COMMENT and start_line <= token.start[0] <= end_line
)
8 changes: 8 additions & 0 deletions wemake_python_styleguide/options/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,9 @@ class of violations that are forbidden to ignore inline, defaults to
expression.
defaults to
:str:`wemake_python_styleguide.options.defaults.MAX_CONDITIONS`
- ``max-comments-in-function`` - maximum number of comments in a single
function, defaults to
:str:`wemake_python_styleguide.options.defaults.MAX_COMMENTS_IN_FUNCTION`

.. rubric:: Formatter options

Expand Down Expand Up @@ -239,6 +242,11 @@ class Configuration:
defaults.MAX_NOQA_COMMENTS,
'Maximum amount of `noqa` comments per module.',
),
_Option(
'--max-comments-in-function',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that it should be this simple. We should count the code / comments rate.

There might be several important cases:

  • Where the amount of comments are far greater then the code, it is totally valid, if we need to explain something
  • Where there are just some comments, like 1 or 2
  • Where the amount of comments are zero

These cases are valid.
What cases are not that valid?

def some():
    x = 1  # define x
    y = 2  # define y
    z = 3  # define z
    return x + y  # return their sum

We should also ignore type: comments, # noqa comments, # pyright, # ty, # ruff, and # pyrefly comments.

So, basically we need to find a lot of slop code, analyze its comments and patterns, try to formalize it, ban it.

This is a very complex task :)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the details!

defaults.MAX_COMMENTS_IN_FUNCTION,
'Maximum number of comments in a single function.',
),
_Option(
'--nested-classes-whitelist',
defaults.NESTED_CLASSES_WHITELIST,
Expand Down
3 changes: 3 additions & 0 deletions wemake_python_styleguide/options/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,6 @@

#: Whether to show violation shortlinks in the formatter output.
SHOW_VIOLATION_LINKS: Final = False

#: Maximum amount of comments in a single function.
MAX_COMMENTS_IN_FUNCTION: Final = 15
1 change: 1 addition & 0 deletions wemake_python_styleguide/options/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ class ValidatedOptions:
max_conditions: int = attr.ib(validator=[_min_max(min=1)])
show_violation_links: bool
exps_for_one_empty_line: int
max_comments_in_function: int = attr.ib(validator=[_min_max(min=1)])


def validate_options(options: Any) -> ValidatedOptions:
Expand Down
2 changes: 2 additions & 0 deletions wemake_python_styleguide/presets/types/tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from wemake_python_styleguide.visitors.ast import ( # noqa: WPS235
blocks,
builtins,
comments,
compares,
conditions,
decorators,
Expand Down Expand Up @@ -81,6 +82,7 @@
decorators.WrongDecoratorVisitor,
redundancy.RedundantEnumerateVisitor,
pm.MatchSubjectVisitor,
comments.FunctionCommentsVisitor,
# Modules:
modules.EmptyModuleContentsVisitor,
modules.MagicModuleFunctionsVisitor,
Expand Down
29 changes: 29 additions & 0 deletions wemake_python_styleguide/violations/best_practices.py
Original file line number Diff line number Diff line change
Expand Up @@ -3019,3 +3019,32 @@ class Some:

error_template = 'Found a leaking ``for`` loop in a class or module body'
code = 481


@final
class TooManyCommentsViolation(ASTViolation):
"""
Forbid using too many comments.

Reasoning:
Excessive comments clutter the codebase, degrade readability,
and increase cognitive load for maintainers. They often indicate
that the logic is unnecessarily complex.

Solution:
Use comments sparingly and only when they genuinely add value.
Prefer writing self-documenting code through clear variable and
function names. If a comment exists to describe *what* the code
does, consider refactoring the logic or moving the explanation
into a docstring.

Configuration:
This rule is configurable with ``--max-comments-in-function``.
Default:
:str:`wemake_python_styleguide.options.defaults.MAX_COMMENTS_IN_FUNCTION`.

.. versionadded:: 1.7.0
"""

error_template = 'Found too many comments: {0}'
code = 482
73 changes: 73 additions & 0 deletions wemake_python_styleguide/visitors/ast/comments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import ast
from typing import cast, final

from wemake_python_styleguide.logic.tokens.comments import (
count_comments_in_range,
)
from wemake_python_styleguide.types import AnyFunctionDef
from wemake_python_styleguide.violations.best_practices import (
TooManyCommentsViolation,
)
from wemake_python_styleguide.visitors.base import BaseNodeTokenVisitor
from wemake_python_styleguide.visitors.decorators import alias


@final
@alias(
'visit_any_function',
(
'visit_FunctionDef',
'visit_AsyncFunctionDef',
),
)
class FunctionCommentsVisitor(BaseNodeTokenVisitor):
"""Checks comment count limits inside functions."""

def visit_any_function(
self,
node: AnyFunctionDef,
) -> None:
"""Checks comment count for each function."""
self._check_comments_count(node)
self.generic_visit(node)

def _check_comments_count(
self,
node: AnyFunctionDef,
) -> None:
"""Checks whether the function exceeds the max allowed comment count."""
nested_ranges: list[tuple[int, int]] = [
(child.lineno, cast(int, child.end_lineno))
for child in ast.walk(node)
if isinstance(child, AnyFunctionDef) and child is not node
]

nested_ranges.sort()

comments_count = 0
cursor = node.lineno

for n_start, n_end in nested_ranges:
if cursor < n_start:
comments_count += count_comments_in_range(
self.file_tokens,
cursor,
n_start - 1,
)
cursor = n_end + 1

if cursor <= cast(int, node.end_lineno):
comments_count += count_comments_in_range(
self.file_tokens,
cursor,
cast(int, node.end_lineno),
)

if comments_count > self.options.max_comments_in_function:
self.add_violation(
TooManyCommentsViolation(
node,
text=str(comments_count),
baseline=self.options.max_comments_in_function,
),
)