From c01e15449e69af389bd81ccc4c56d62ea8e4f1d7 Mon Sep 17 00:00:00 2001 From: Khabib <136262760+Khabib73@users.noreply.github.com> Date: Thu, 2 Jul 2026 20:12:58 +0300 Subject: [PATCH] add TooManyComments (#3689) improve typing --- tests/fixtures/noqa/noqa.py | 61 ++++++++++++++++ tests/test_checker/test_noqa.py | 1 + tests/test_checker/test_presets.py | 2 + .../logic/tokens/comments.py | 15 ++++ wemake_python_styleguide/options/config.py | 8 ++ wemake_python_styleguide/options/defaults.py | 3 + .../options/validation.py | 1 + .../presets/types/tree.py | 2 + .../violations/best_practices.py | 29 ++++++++ .../visitors/ast/comments.py | 73 +++++++++++++++++++ 10 files changed, 195 insertions(+) create mode 100644 wemake_python_styleguide/logic/tokens/comments.py create mode 100644 wemake_python_styleguide/visitors/ast/comments.py diff --git a/tests/fixtures/noqa/noqa.py b/tests/fixtures/noqa/noqa.py index f824919f4..f095c7027 100644 --- a/tests/fixtures/noqa/noqa.py +++ b/tests/fixtures/noqa/noqa.py @@ -625,6 +625,67 @@ def many_raises_function(parameter): # noqa: WPS238 case _: ... +def too_many_locals(): # noqa: WPS482 + # 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 diff --git a/tests/test_checker/test_noqa.py b/tests/test_checker/test_noqa.py index 3d1e60bc0..8738892af 100644 --- a/tests/test_checker/test_noqa.py +++ b/tests/test_checker/test_noqa.py @@ -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 diff --git a/tests/test_checker/test_presets.py b/tests/test_checker/test_presets.py index 52d12cfcd..1b41740b4 100644 --- a/tests/test_checker/test_presets.py +++ b/tests/test_checker/test_presets.py @@ -8,6 +8,7 @@ from wemake_python_styleguide.checker import Checker from wemake_python_styleguide.visitors.base import ( BaseFilenameVisitor, + BaseNodeTokenVisitor, BaseNodeVisitor, BaseTokenVisitor, BaseVisitor, @@ -18,6 +19,7 @@ def _is_visitor_class(cls) -> bool: base_classes = { BaseFilenameVisitor, BaseNodeVisitor, + BaseNodeTokenVisitor, BaseTokenVisitor, BaseVisitor, } diff --git a/wemake_python_styleguide/logic/tokens/comments.py b/wemake_python_styleguide/logic/tokens/comments.py new file mode 100644 index 000000000..fc08c3cdd --- /dev/null +++ b/wemake_python_styleguide/logic/tokens/comments.py @@ -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 + ) diff --git a/wemake_python_styleguide/options/config.py b/wemake_python_styleguide/options/config.py index 47c324ea5..1bd15ff6e 100644 --- a/wemake_python_styleguide/options/config.py +++ b/wemake_python_styleguide/options/config.py @@ -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 @@ -239,6 +242,11 @@ class Configuration: defaults.MAX_NOQA_COMMENTS, 'Maximum amount of `noqa` comments per module.', ), + _Option( + '--max-comments-in-function', + defaults.MAX_COMMENTS_IN_FUNCTION, + 'Maximum number of comments in a single function.', + ), _Option( '--nested-classes-whitelist', defaults.NESTED_CLASSES_WHITELIST, diff --git a/wemake_python_styleguide/options/defaults.py b/wemake_python_styleguide/options/defaults.py index 547303cdf..67127cdff 100644 --- a/wemake_python_styleguide/options/defaults.py +++ b/wemake_python_styleguide/options/defaults.py @@ -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 diff --git a/wemake_python_styleguide/options/validation.py b/wemake_python_styleguide/options/validation.py index 049f2ae9d..c4455ee94 100644 --- a/wemake_python_styleguide/options/validation.py +++ b/wemake_python_styleguide/options/validation.py @@ -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: diff --git a/wemake_python_styleguide/presets/types/tree.py b/wemake_python_styleguide/presets/types/tree.py index 8cb87565d..af794c379 100644 --- a/wemake_python_styleguide/presets/types/tree.py +++ b/wemake_python_styleguide/presets/types/tree.py @@ -4,6 +4,7 @@ from wemake_python_styleguide.visitors.ast import ( # noqa: WPS235 blocks, builtins, + comments, compares, conditions, decorators, @@ -81,6 +82,7 @@ decorators.WrongDecoratorVisitor, redundancy.RedundantEnumerateVisitor, pm.MatchSubjectVisitor, + comments.FunctionCommentsVisitor, # Modules: modules.EmptyModuleContentsVisitor, modules.MagicModuleFunctionsVisitor, diff --git a/wemake_python_styleguide/violations/best_practices.py b/wemake_python_styleguide/violations/best_practices.py index 050ff4efe..1c21a11fe 100644 --- a/wemake_python_styleguide/violations/best_practices.py +++ b/wemake_python_styleguide/violations/best_practices.py @@ -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 diff --git a/wemake_python_styleguide/visitors/ast/comments.py b/wemake_python_styleguide/visitors/ast/comments.py new file mode 100644 index 000000000..531d97be8 --- /dev/null +++ b/wemake_python_styleguide/visitors/ast/comments.py @@ -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, + ), + )