rulebook_pylint.checkers

 1from __future__ import annotations
 2
 3from typing import TYPE_CHECKING
 4
 5from pylint.utils import register_plugins
 6
 7from rulebook_pylint.checkers.abbreviation_as_word import AbbreviationAsWordChecker
 8from rulebook_pylint.checkers.block_comment_clip import BlockCommentClipChecker
 9from rulebook_pylint.checkers.block_comment_trim import BlockCommentTrimChecker
10from rulebook_pylint.checkers.case_separator import CaseSeparatorChecker
11from rulebook_pylint.checkers.comment_spaces import CommentSpacesChecker
12from rulebook_pylint.checkers.comment_trim import CommentTrimChecker
13from rulebook_pylint.checkers.common_function_position import CommonFunctionPositionChecker
14from rulebook_pylint.checkers.complicated_assertion import ComplicatedAssertionChecker
15from rulebook_pylint.checkers.complicated_assignment import ComplicatedAssignmentChecker
16from rulebook_pylint.checkers.confusing_assertion import ConfusingAssertionChecker
17from rulebook_pylint.checkers.duplicate_blank_line import DuplicateBlankLineChecker
18from rulebook_pylint.checkers.duplicate_blank_line_in_block_comment import \
19    DuplicateBlankLineInBlockCommentChecker
20from rulebook_pylint.checkers.duplicate_blank_line_in_comment import \
21    DuplicateBlankLineInCommentChecker
22from rulebook_pylint.checkers.duplicate_space import DuplicateSpaceChecker
23from rulebook_pylint.checkers.generic_name import GenericNameChecker
24from rulebook_pylint.checkers.inner_class_position import InnerClassPositionChecker
25from rulebook_pylint.checkers.internal_error import InternalErrorChecker
26from rulebook_pylint.checkers.lonely_case import LonelyCaseChecker
27from rulebook_pylint.checkers.lowercase_hexadecimal import LowercaseHexadecimalChecker
28from rulebook_pylint.checkers.meaningless_word import MeaninglessWordChecker
29from rulebook_pylint.checkers.member_order import MemberOrderChecker
30from rulebook_pylint.checkers.member_separator import MemberSeparatorChecker
31from rulebook_pylint.checkers.named_import_order import NamedImportOrderChecker
32from rulebook_pylint.checkers.nested_if_else import NestedIfElseChecker
33from rulebook_pylint.checkers.parameter_wrap import ParameterWrapChecker
34from rulebook_pylint.checkers.parentheses_clip import ParenthesesClipChecker
35from rulebook_pylint.checkers.parentheses_trim import ParenthesesTrimChecker
36from rulebook_pylint.checkers.redundant_default import RedundantDefaultChecker
37from rulebook_pylint.checkers.todo_comment import TodoCommentChecker
38from rulebook_pylint.checkers.trailing_comma import TrailingCommaChecker
39from rulebook_pylint.checkers.unnecessary_abstract import UnnecessaryAbstractChecker
40from rulebook_pylint.checkers.unnecessary_blank_line_after_colon import \
41    UnnecessaryBlankLineAfterColonChecker
42from rulebook_pylint.checkers.unnecessary_continue import UnnecessaryContinueChecker
43from rulebook_pylint.checkers.unnecessary_leading_blank_line import \
44    UnnecessaryLeadingBlankLineChecker
45
46if TYPE_CHECKING:
47    from pylint.lint import PyLinter
48
49
50def initialize(linter: PyLinter) -> None:
51    register_plugins(linter, __path__[0])
52
53
54__all__: list[str] = [
55    'AbbreviationAsWordChecker',
56    'BlockCommentClipChecker',
57    'BlockCommentTrimChecker',
58    'CaseSeparatorChecker',
59    'CommentSpacesChecker',
60    'CommentTrimChecker',
61    'CommonFunctionPositionChecker',
62    'ComplicatedAssertionChecker',
63    'ComplicatedAssignmentChecker',
64    'ConfusingAssertionChecker',
65    'DuplicateBlankLineChecker',
66    'DuplicateBlankLineInBlockCommentChecker',
67    'DuplicateBlankLineInCommentChecker',
68    'DuplicateSpaceChecker',
69    'GenericNameChecker',
70    'InnerClassPositionChecker',
71    'InternalErrorChecker',
72    'LonelyCaseChecker',
73    'LowercaseHexadecimalChecker',
74    'MeaninglessWordChecker',
75    'MemberOrderChecker',
76    'MemberSeparatorChecker',
77    'NamedImportOrderChecker',
78    'NestedIfElseChecker',
79    'ParameterWrapChecker',
80    'ParenthesesClipChecker',
81    'ParenthesesTrimChecker',
82    'RedundantDefaultChecker',
83    'TodoCommentChecker',
84    'TrailingCommaChecker',
85    'UnnecessaryAbstractChecker',
86    'UnnecessaryBlankLineAfterColonChecker',
87    'UnnecessaryContinueChecker',
88    'UnnecessaryLeadingBlankLineChecker',
89    'initialize',
90]
class AbbreviationAsWordChecker(rulebook_pylint.checkers.rulebook_checkers.RulebookChecker):
16class AbbreviationAsWordChecker(RulebookChecker):
17    """See detail: https://hanggrian.github.io/rulebook/rules/#abbreviation-as-word"""
18    _MSG: str = 'abbreviation.as.word'
19
20    _ABBREVIATION_REGEX: Pattern = re(r'[A-Z]{3,}(?=[A-Z][a-z]|$)')
21
22    name: str = 'abbreviation-as-word'
23    msgs: dict[str, tuple[str, str, str]] = Messages.of(_MSG)
24
25    def visit_classdef(self, node: ClassDef) -> None:
26        # checks for violation
27        if not self._ABBREVIATION_REGEX.findall(node.name):
28            return
29        self.add_message(
30            self._MSG,
31            node=node,
32            args= \
33                self._ABBREVIATION_REGEX.sub(
34                    lambda m: m.group(0)[0] + m.group(0)[1:].lower(),
35                    node.name,
36                ),
37            col_offset= \
38                node.col_offset + 6 \
39                    if node.col_offset is not None \
40                    else None,
41        )
name: str = 'abbreviation-as-word'

Name of the provider.

msgs: dict[str, tuple[str, str, str]] = {'E6143': ("Rename abbreviation to '%s'.", 'abbreviation.as.word', 'https://github.com/hanggrian/rulebook')}
def visit_classdef(self, node: astroid.nodes.scoped_nodes.scoped_nodes.ClassDef) -> None:
25    def visit_classdef(self, node: ClassDef) -> None:
26        # checks for violation
27        if not self._ABBREVIATION_REGEX.findall(node.name):
28            return
29        self.add_message(
30            self._MSG,
31            node=node,
32            args= \
33                self._ABBREVIATION_REGEX.sub(
34                    lambda m: m.group(0)[0] + m.group(0)[1:].lower(),
35                    node.name,
36                ),
37            col_offset= \
38                node.col_offset + 6 \
39                    if node.col_offset is not None \
40                    else None,
41        )
class BlockCommentClipChecker(rulebook_pylint.checkers.rulebook_checkers.RulebookChecker):
15class BlockCommentClipChecker(RulebookChecker):
16    """See detail: https://hanggrian.github.io/rulebook/rules/#block-comment-clip"""
17    _MSG: str = 'block.comment.clip'
18
19    _SINGLELINE_TEMPLATE = 6  # """"""
20
21    name: str = 'block-comment-clip'
22    msgs: dict[str, tuple[str, str, str]] = Messages.of(_MSG)
23    options: Options = (
24        MAX_LINE_LENGTH_OPTION,
25    )
26
27    _max_line_length: int = 100
28
29    def open(self) -> None:
30        self._max_line_length = self.linter.config.rulebook_max_line_length
31
32    def visit_module(self, node: Module) -> None:
33        self._process(node.doc_node)
34
35    def visit_classdef(self, node: ClassDef) -> None:
36        self._process(node.doc_node)
37
38    def visit_functiondef(self, node: FunctionDef) -> None:
39        self._process(node.doc_node)
40
41    def _process(self, docstring: Const | None) -> None:
42        # checks for violation
43        if docstring is None:
44            return
45        if '\n' not in docstring.value:
46            return
47        line: str = docstring.value.strip()
48        if '\n' in line:
49            return
50        if docstring.col_offset is None:
51            return
52        text_length: int = docstring.col_offset + len(line)
53        if text_length + self._SINGLELINE_TEMPLATE <= self._max_line_length:
54            self.add_message(self._MSG, node=docstring)
name: str = 'block-comment-clip'

Name of the provider.

msgs: dict[str, tuple[str, str, str]] = {'E6144': ('Convert into single-line.', 'block.comment.clip', 'https://github.com/hanggrian/rulebook')}
options: 'Options' = (('rulebook-max-line-length', {'default': 100, 'type': 'int', 'metavar': '<integer>', 'help': 'Max length of a line.'}),)

Options provided by this provider.

def open(self) -> None:
29    def open(self) -> None:
30        self._max_line_length = self.linter.config.rulebook_max_line_length

Called before visiting project (i.e. set of modules).

def visit_module(self, node: astroid.nodes.scoped_nodes.scoped_nodes.Module) -> None:
32    def visit_module(self, node: Module) -> None:
33        self._process(node.doc_node)
def visit_classdef(self, node: astroid.nodes.scoped_nodes.scoped_nodes.ClassDef) -> None:
35    def visit_classdef(self, node: ClassDef) -> None:
36        self._process(node.doc_node)
def visit_functiondef(self, node: astroid.nodes.scoped_nodes.scoped_nodes.FunctionDef) -> None:
38    def visit_functiondef(self, node: FunctionDef) -> None:
39        self._process(node.doc_node)
class BlockCommentTrimChecker(rulebook_pylint.checkers.rulebook_checkers.RulebookChecker):
16class BlockCommentTrimChecker(RulebookChecker):
17    """See detail: https://hanggrian.github.io/rulebook/rules/#block-comment-trim"""
18    _MSG_FIRST: str = 'block.comment.trim.first'
19    _MSG_LAST: str = 'block.comment.trim.last'
20
21    _MULTIPLE_EMPTY_LINES: Pattern = re(r'\n\n\s*$')
22
23    name: str = 'block-comment-trim'
24    msgs: dict[str, tuple[str, str, str]] = Messages.of(_MSG_FIRST, _MSG_LAST)
25
26    def visit_module(self, node: Module) -> None:
27        self._process(node.doc_node)
28
29    def visit_classdef(self, node: ClassDef) -> None:
30        self._process(node.doc_node)
31
32    def visit_functiondef(self, node: FunctionDef) -> None:
33        self._process(node.doc_node)
34
35    def _process(self, docstring: Const | None) -> None:
36        # checks for violation
37        if docstring is None:
38            return
39        if docstring.value.startswith('\n\n'):
40            self.add_message(self._MSG_FIRST, node=docstring, line=docstring.lineno)
41        if self._MULTIPLE_EMPTY_LINES.search(docstring.value):
42            self.add_message(self._MSG_LAST, node=docstring, line=docstring.end_lineno)
name: str = 'block-comment-trim'

Name of the provider.

msgs: dict[str, tuple[str, str, str]] = {'E6145': ('Remove blank line after \'"""\'.', 'block.comment.trim.first', 'https://github.com/hanggrian/rulebook'), 'E6146': ('Remove blank line before \'"""\'.', 'block.comment.trim.last', 'https://github.com/hanggrian/rulebook')}
def visit_module(self, node: astroid.nodes.scoped_nodes.scoped_nodes.Module) -> None:
26    def visit_module(self, node: Module) -> None:
27        self._process(node.doc_node)
def visit_classdef(self, node: astroid.nodes.scoped_nodes.scoped_nodes.ClassDef) -> None:
29    def visit_classdef(self, node: ClassDef) -> None:
30        self._process(node.doc_node)
def visit_functiondef(self, node: astroid.nodes.scoped_nodes.scoped_nodes.FunctionDef) -> None:
32    def visit_functiondef(self, node: FunctionDef) -> None:
33        self._process(node.doc_node)
class CaseSeparatorChecker(rulebook_pylint.checkers.rulebook_checkers.RulebookFileChecker):
16class CaseSeparatorChecker(RulebookFileChecker):
17    """See detail: https://hanggrian.github.io/rulebook/rules/#case-separator"""
18    _MSG_MISSING: str = 'case.separator.missing'
19    _MSG_UNEXPECTED: str = 'case.separator.unexpected'
20
21    name: str = 'case-separator'
22    msgs: dict[str, tuple[str, str, str]] = Messages.of(_MSG_MISSING, _MSG_UNEXPECTED)
23
24    def visit_match(self, node: Match) -> None:
25        # collect cases
26        if not node.cases:
27            return
28
29        # checks for violation
30        has_multiline = \
31            any(
32                is_multiline(match_case) or
33                has_comment_above(self.lines, match_case) for
34                match_case in node.cases
35            )
36        for (i, match_case) in enumerate(node.cases):
37            if i == 0:
38                continue
39            last_match_case: MatchCase = node.cases[i - 1]
40            match_case_fromlineno: int = \
41                get_fromlineno_before(self.lines, match_case, last_match_case)
42            last_body: NodeNG = last_match_case.body[-1]
43            if has_multiline:
44                if last_body.tolineno - 1 != match_case_fromlineno - 2:
45                    self.add_message(
46                        self._MSG_MISSING,
47                        line=last_body.lineno,
48                        end_lineno=last_body.end_lineno,
49                        col_offset=last_body.col_offset,
50                        end_col_offset=last_body.end_col_offset,
51                    )
52            elif last_body.tolineno - 1 != match_case_fromlineno - 1:
53                self.add_message(
54                    self._MSG_UNEXPECTED,
55                    line=last_body.lineno,
56                    end_lineno=last_body.end_lineno,
57                    col_offset=last_body.col_offset,
58                    end_col_offset=last_body.end_col_offset,
59                )
name: str = 'case-separator'

Name of the provider.

msgs: dict[str, tuple[str, str, str]] = {'E6147': ('Add blank line after multiline branch.', 'case.separator.missing', 'https://github.com/hanggrian/rulebook'), 'E6148': ('Remove blank line after single-line branch.', 'case.separator.unexpected', 'https://github.com/hanggrian/rulebook')}
def visit_match(self, node: astroid.nodes.node_classes.Match) -> None:
24    def visit_match(self, node: Match) -> None:
25        # collect cases
26        if not node.cases:
27            return
28
29        # checks for violation
30        has_multiline = \
31            any(
32                is_multiline(match_case) or
33                has_comment_above(self.lines, match_case) for
34                match_case in node.cases
35            )
36        for (i, match_case) in enumerate(node.cases):
37            if i == 0:
38                continue
39            last_match_case: MatchCase = node.cases[i - 1]
40            match_case_fromlineno: int = \
41                get_fromlineno_before(self.lines, match_case, last_match_case)
42            last_body: NodeNG = last_match_case.body[-1]
43            if has_multiline:
44                if last_body.tolineno - 1 != match_case_fromlineno - 2:
45                    self.add_message(
46                        self._MSG_MISSING,
47                        line=last_body.lineno,
48                        end_lineno=last_body.end_lineno,
49                        col_offset=last_body.col_offset,
50                        end_col_offset=last_body.end_col_offset,
51                    )
52            elif last_body.tolineno - 1 != match_case_fromlineno - 1:
53                self.add_message(
54                    self._MSG_UNEXPECTED,
55                    line=last_body.lineno,
56                    end_lineno=last_body.end_lineno,
57                    col_offset=last_body.col_offset,
58                    end_col_offset=last_body.end_col_offset,
59                )
class CommentSpacesChecker(rulebook_pylint.checkers.rulebook_checkers.RulebookTokenChecker):
15class CommentSpacesChecker(RulebookTokenChecker):
16    """See detail: https://hanggrian.github.io/rulebook/rules/#comment-spaces"""
17    _MSG: str = 'comment.spaces'
18
19    name: str = 'comment-spaces'
20    msgs: dict[str, tuple[str, str, str]] = Messages.of(_MSG)
21
22    def process_tokens(self, tokens: list[TokenInfo]) -> None:
23        # checks for violation
24        for token in [t for t in tokens if t.type == COMMENT]:
25            if token.string.startswith('# ') or \
26                token.string.startswith('#!') or \
27                all(c == '#' for c in token.string):
28                continue
29            self.add_message(self._MSG, line=token.start[0], col_offset=token.start[1])
name: str = 'comment-spaces'

Name of the provider.

msgs: dict[str, tuple[str, str, str]] = {'E6149': ("Put one space after '#'.", 'comment.spaces', 'https://github.com/hanggrian/rulebook')}
def process_tokens(self, tokens: list[tokenize.TokenInfo]) -> None:
22    def process_tokens(self, tokens: list[TokenInfo]) -> None:
23        # checks for violation
24        for token in [t for t in tokens if t.type == COMMENT]:
25            if token.string.startswith('# ') or \
26                token.string.startswith('#!') or \
27                all(c == '#' for c in token.string):
28                continue
29            self.add_message(self._MSG, line=token.start[0], col_offset=token.start[1])

Should be overridden by subclasses.

class CommentTrimChecker(rulebook_pylint.checkers.rulebook_checkers.RulebookTokenChecker):
16class CommentTrimChecker(RulebookTokenChecker):
17    """See detail: https://hanggrian.github.io/rulebook/rules/#comment-trim"""
18    _MSG: str = 'comment.trim'
19
20    name: str = 'comment-trim'
21    msgs: dict[str, tuple[str, str, str]] = Messages.of(_MSG)
22
23    def process_tokens(self, tokens: list[TokenInfo]) -> None:
24        for i, token in enumerate(tokens):
25            # target comment
26            if token.type != COMMENT:
27                continue
28
29            # continue if this comment is first line
30            if i - 2 >= 0 and \
31                tokens[i - 1].type == NL and \
32                tokens[i - 2].type == COMMENT:
33                return
34
35            # iterate to find last
36            j: int = i
37            while j + 2 < len(tokens) and \
38                tokens[j + 1].type == NL and \
39                tokens[j + 2].type == COMMENT:
40                j += 2
41            curr_token: TokenInfo = tokens[j]
42
43            # skip blank comment
44            if curr_token is token:
45                return
46
47            # checks for violation
48            if is_comment_empty(token):
49                self.add_message(
50                    self._MSG,
51                    line=token.start[0],
52                    col_offset=token.start[1],
53                )
54            if is_comment_empty(curr_token):
55                self.add_message(
56                    self._MSG,
57                    line=curr_token.start[0],
58                    col_offset=curr_token.start[1],
59                )
60            return
name: str = 'comment-trim'

Name of the provider.

msgs: dict[str, tuple[str, str, str]] = {'E6150': ("Remove blank line after '#'.", 'comment.trim', 'https://github.com/hanggrian/rulebook')}
def process_tokens(self, tokens: list[tokenize.TokenInfo]) -> None:
23    def process_tokens(self, tokens: list[TokenInfo]) -> None:
24        for i, token in enumerate(tokens):
25            # target comment
26            if token.type != COMMENT:
27                continue
28
29            # continue if this comment is first line
30            if i - 2 >= 0 and \
31                tokens[i - 1].type == NL and \
32                tokens[i - 2].type == COMMENT:
33                return
34
35            # iterate to find last
36            j: int = i
37            while j + 2 < len(tokens) and \
38                tokens[j + 1].type == NL and \
39                tokens[j + 2].type == COMMENT:
40                j += 2
41            curr_token: TokenInfo = tokens[j]
42
43            # skip blank comment
44            if curr_token is token:
45                return
46
47            # checks for violation
48            if is_comment_empty(token):
49                self.add_message(
50                    self._MSG,
51                    line=token.start[0],
52                    col_offset=token.start[1],
53                )
54            if is_comment_empty(curr_token):
55                self.add_message(
56                    self._MSG,
57                    line=curr_token.start[0],
58                    col_offset=curr_token.start[1],
59                )
60            return

Should be overridden by subclasses.

class CommonFunctionPositionChecker(rulebook_pylint.checkers.rulebook_checkers.RulebookChecker):
15class CommonFunctionPositionChecker(RulebookChecker):
16    """See detail: https://hanggrian.github.io/rulebook/rules/#common-function-position"""
17    _MSG: str = 'common.function.position'
18
19    _COMMON_FUNCTIONS: frozenset[str] = \
20        frozenset([
21            '__str__',
22            '__hash__',
23            '__eq__',
24            '__new__',
25            '__del__',
26            '__repr__',
27            '__bytes__',
28            '__format__',
29            '__lt__',
30            '__le__',
31            '__ne__',
32            '__gt__',
33            '__ge__',
34            '__bool__',
35        ])
36
37    name: str = 'common-function-position'
38    msgs: dict[str, tuple[str, str, str]] = Messages.of(_MSG)
39
40    def visit_functiondef(self, node: FunctionDef) -> None:
41        # target special function
42        if node.name not in self._COMMON_FUNCTIONS:
43            return
44
45        current: NodeNG | None = node
46        while current is not None:
47            # checks for violation
48            if isinstance(current, FunctionDef) and \
49                not has_decorator(current, 'staticmethod') and \
50                current.name not in self._COMMON_FUNCTIONS:
51                self.add_message(self._MSG, node=node, args=node.name)
52                return
53
54            current = current.next_sibling()
name: str = 'common-function-position'

Name of the provider.

msgs: dict[str, tuple[str, str, str]] = {'E6151': ("Move '%s' to last.", 'common.function.position', 'https://github.com/hanggrian/rulebook')}
def visit_functiondef(self, node: astroid.nodes.scoped_nodes.scoped_nodes.FunctionDef) -> None:
40    def visit_functiondef(self, node: FunctionDef) -> None:
41        # target special function
42        if node.name not in self._COMMON_FUNCTIONS:
43            return
44
45        current: NodeNG | None = node
46        while current is not None:
47            # checks for violation
48            if isinstance(current, FunctionDef) and \
49                not has_decorator(current, 'staticmethod') and \
50                current.name not in self._COMMON_FUNCTIONS:
51                self.add_message(self._MSG, node=node, args=node.name)
52                return
53
54            current = current.next_sibling()
class ComplicatedAssertionChecker(rulebook_pylint.checkers.rulebook_checkers.RulebookChecker):
14class ComplicatedAssertionChecker(RulebookChecker):
15    """See detail: https://hanggrian.github.io/rulebook/rules/#complicated-assertion"""
16    _MSG: str = 'complicated.assertion'
17
18    name: str = 'complicated-assertion'
19    msgs: dict[str, tuple[str, str, str]] = Messages.of(_MSG)
20
21    _BOOLEAN_ASSERTIONS: frozenset[str] = frozenset(['assertTrue', 'assertFalse'])
22    _EQUALITY_ASSERTIONS: frozenset[str] = frozenset(['assertEqual', 'assertNotEqual'])
23
24    def visit_classdef(self, node: ClassDef) -> None:
25        # find built-in tests
26        if not any(isinstance(b, Name) and b.name == 'TestCase' for b in node.bases):
27            return
28
29        for method in [m for m in node.methods() if isinstance(m, FunctionDef)]:
30            for expr in [b for b in method.body if isinstance(b, Expr)]:
31                # checks for violation
32                call: NodeNG = expr.value
33                if not isinstance(call, Call) or \
34                    not isinstance(call.func, Attribute):
35                    continue
36                name: str = call.func.attrname
37                if not call.args:
38                    continue
39                call_replacement: str
40                if name in self._BOOLEAN_ASSERTIONS:
41                    arg: NodeNG = call.args[0]
42                    if not isinstance(arg, Compare):
43                        continue
44                    op: str = arg.ops[0][0]
45                    if op == '==':
46                        call_replacement = 'assertEqual'
47                    elif op == '!=':
48                        call_replacement = 'assertNotEqual'
49                    elif op == 'is':
50                        call_replacement = 'assertIs'
51                    elif op == 'is not':
52                        call_replacement = 'assertIsNot'
53                    else:
54                        continue
55                elif name in self._EQUALITY_ASSERTIONS:
56                    args: list[NodeNG] = call.args
57                    if any(self._is_const(arg, True) for arg in args):
58                        call_replacement = 'assertTrue'
59                    elif any(self._is_const(arg, False) for arg in args):
60                        call_replacement = 'assertFalse'
61                    elif any(self._is_const(arg, None) for arg in args):
62                        call_replacement = 'assertIsNone'
63                    else:
64                        continue
65                else:
66                    continue
67                self.add_message(self._MSG, node=call.func, args=call_replacement)
68
69    @staticmethod
70    def _is_const(node: NodeNG, value: object) -> bool:
71        return isinstance(node, Const) and \
72            node.value is value
name: str = 'complicated-assertion'

Name of the provider.

msgs: dict[str, tuple[str, str, str]] = {'E6152': ("Use assertion '%s'.", 'complicated.assertion', 'https://github.com/hanggrian/rulebook')}
def visit_classdef(self, node: astroid.nodes.scoped_nodes.scoped_nodes.ClassDef) -> None:
24    def visit_classdef(self, node: ClassDef) -> None:
25        # find built-in tests
26        if not any(isinstance(b, Name) and b.name == 'TestCase' for b in node.bases):
27            return
28
29        for method in [m for m in node.methods() if isinstance(m, FunctionDef)]:
30            for expr in [b for b in method.body if isinstance(b, Expr)]:
31                # checks for violation
32                call: NodeNG = expr.value
33                if not isinstance(call, Call) or \
34                    not isinstance(call.func, Attribute):
35                    continue
36                name: str = call.func.attrname
37                if not call.args:
38                    continue
39                call_replacement: str
40                if name in self._BOOLEAN_ASSERTIONS:
41                    arg: NodeNG = call.args[0]
42                    if not isinstance(arg, Compare):
43                        continue
44                    op: str = arg.ops[0][0]
45                    if op == '==':
46                        call_replacement = 'assertEqual'
47                    elif op == '!=':
48                        call_replacement = 'assertNotEqual'
49                    elif op == 'is':
50                        call_replacement = 'assertIs'
51                    elif op == 'is not':
52                        call_replacement = 'assertIsNot'
53                    else:
54                        continue
55                elif name in self._EQUALITY_ASSERTIONS:
56                    args: list[NodeNG] = call.args
57                    if any(self._is_const(arg, True) for arg in args):
58                        call_replacement = 'assertTrue'
59                    elif any(self._is_const(arg, False) for arg in args):
60                        call_replacement = 'assertFalse'
61                    elif any(self._is_const(arg, None) for arg in args):
62                        call_replacement = 'assertIsNone'
63                    else:
64                        continue
65                else:
66                    continue
67                self.add_message(self._MSG, node=call.func, args=call_replacement)
class ComplicatedAssignmentChecker(rulebook_pylint.checkers.rulebook_checkers.RulebookChecker):
14class ComplicatedAssignmentChecker(RulebookChecker):
15    """See detail: https://hanggrian.github.io/rulebook/rules/#complicated-assignment"""
16    _MSG: str = 'complicated.assignment'
17
18    _SHORTHAND_OPERATIONS: frozenset[str] = frozenset(['+', '-', '*', '/', '%'])
19
20    name: str = 'complicated-assignment'
21    msgs: dict[str, tuple[str, str, str]] = Messages.of(_MSG)
22
23    def visit_assign(self, node: Assign) -> None:
24        # skip destructuring
25        if len(node.targets) != 1:
26            return
27
28        # checks for violation
29        identifier: NodeNG = node.targets[0]
30        if not isinstance(identifier, AssignName):
31            return
32        if not isinstance(node.value, BinOp):
33            return
34        bin_op: BinOp = self._deepest_bin_op(node.value)
35        if bin_op.op not in self._SHORTHAND_OPERATIONS:
36            return
37        if not isinstance(bin_op.left, Name) or \
38            bin_op.left.name != identifier.name:
39            return
40        self.add_message(self._MSG, node=node, args=bin_op.op + '=')
41
42    @staticmethod
43    def _deepest_bin_op(node: BinOp) -> BinOp:
44        current: BinOp = node
45        while isinstance(current.left, BinOp):
46            current = current.left
47        return current
name: str = 'complicated-assignment'

Name of the provider.

msgs: dict[str, tuple[str, str, str]] = {'E6153': ("Use assignment operator '%s'.", 'complicated.assignment', 'https://github.com/hanggrian/rulebook')}
def visit_assign(self, node: astroid.nodes.node_classes.Assign) -> None:
23    def visit_assign(self, node: Assign) -> None:
24        # skip destructuring
25        if len(node.targets) != 1:
26            return
27
28        # checks for violation
29        identifier: NodeNG = node.targets[0]
30        if not isinstance(identifier, AssignName):
31            return
32        if not isinstance(node.value, BinOp):
33            return
34        bin_op: BinOp = self._deepest_bin_op(node.value)
35        if bin_op.op not in self._SHORTHAND_OPERATIONS:
36            return
37        if not isinstance(bin_op.left, Name) or \
38            bin_op.left.name != identifier.name:
39            return
40        self.add_message(self._MSG, node=node, args=bin_op.op + '=')
class ConfusingAssertionChecker(rulebook_pylint.checkers.rulebook_checkers.RulebookChecker):
15class ConfusingAssertionChecker(RulebookChecker):
16    """See detail: https://hanggrian.github.io/rulebook/rules/#confusing-assertion"""
17    _MSG: str = 'confusing.assertion'
18
19    _ASSERT_CALLS: dict[str, str] = \
20        two_way_dict(('assertTrue', 'assertFalse'))
21
22    name: str = 'confusing-assertion'
23    msgs: dict[str, tuple[str, str, str]] = Messages.of(_MSG)
24
25    def visit_classdef(self, node: ClassDef) -> None:
26        # find built-in tests
27        if not any(isinstance(b, Name) and b.name == 'TestCase' for b in node.bases):
28            return
29
30        for method in [m for m in node.methods() if isinstance(m, FunctionDef)]:
31            for expr in [b for b in method.body if isinstance(b, Expr)]:
32                # find inverted assert function
33                call: NodeNG = expr.value
34                if not isinstance(call, Call) or \
35                    not isinstance(call.func, Attribute):
36                    continue
37                call_replacement: str | None = self._ASSERT_CALLS.get(call.func.attrname, None)
38                if call_replacement is None:
39                    continue
40
41                # checks for violation
42                if not call.args:
43                    continue
44                arg: NodeNG = call.args[0]
45                if not isinstance(arg, UnaryOp) or \
46                    arg.op != 'not':
47                    continue
48                self.add_message(self._MSG, node=call.func, args=call_replacement)
name: str = 'confusing-assertion'

Name of the provider.

msgs: dict[str, tuple[str, str, str]] = {'E6154': ("Omit negation and replace assertion with '%s'.", 'confusing.assertion', 'https://github.com/hanggrian/rulebook')}
def visit_classdef(self, node: astroid.nodes.scoped_nodes.scoped_nodes.ClassDef) -> None:
25    def visit_classdef(self, node: ClassDef) -> None:
26        # find built-in tests
27        if not any(isinstance(b, Name) and b.name == 'TestCase' for b in node.bases):
28            return
29
30        for method in [m for m in node.methods() if isinstance(m, FunctionDef)]:
31            for expr in [b for b in method.body if isinstance(b, Expr)]:
32                # find inverted assert function
33                call: NodeNG = expr.value
34                if not isinstance(call, Call) or \
35                    not isinstance(call.func, Attribute):
36                    continue
37                call_replacement: str | None = self._ASSERT_CALLS.get(call.func.attrname, None)
38                if call_replacement is None:
39                    continue
40
41                # checks for violation
42                if not call.args:
43                    continue
44                arg: NodeNG = call.args[0]
45                if not isinstance(arg, UnaryOp) or \
46                    arg.op != 'not':
47                    continue
48                self.add_message(self._MSG, node=call.func, args=call_replacement)
class DuplicateBlankLineChecker(rulebook_pylint.checkers.rulebook_checkers.RulebookFileChecker):
14class DuplicateBlankLineChecker(RulebookFileChecker):
15    """See detail: https://hanggrian.github.io/rulebook/rules/#duplicate-blank-line"""
16    _MSG: str = 'duplicate.blank.line'
17
18    name: str = 'duplicate-blank-line'
19    msgs: dict[str, tuple[str, str, str]] = Messages.of(_MSG)
20
21    def process_module(self, node: Module) -> None:
22        # checks for violation
23        counter: int = 0
24        with node.stream() as stream:
25            for (i, line) in enumerate(stream.readlines()):
26                counter = counter + 1 if not line.strip() else 0
27                if counter < 3:
28                    continue
29                self.add_message(self._MSG, line=i + 1)
name: str = 'duplicate-blank-line'

Name of the provider.

msgs: dict[str, tuple[str, str, str]] = {'E6155': ('Remove consecutive blank line.', 'duplicate.blank.line', 'https://github.com/hanggrian/rulebook')}
def process_module(self, node: astroid.nodes.scoped_nodes.scoped_nodes.Module) -> None:
21    def process_module(self, node: Module) -> None:
22        # checks for violation
23        counter: int = 0
24        with node.stream() as stream:
25            for (i, line) in enumerate(stream.readlines()):
26                counter = counter + 1 if not line.strip() else 0
27                if counter < 3:
28                    continue
29                self.add_message(self._MSG, line=i + 1)

Process a module.

The module's content is accessible via astroid.stream

class DuplicateBlankLineInBlockCommentChecker(rulebook_pylint.checkers.rulebook_checkers.RulebookChecker):
14class DuplicateBlankLineInBlockCommentChecker(RulebookChecker):
15    """See detail: https://hanggrian.github.io/rulebook/rules/#duplicate-blank-line-in-block-comment"""
16    _MSG: str = 'duplicate.blank.line.in.block.comment'
17
18    name: str = 'duplicate-blank-line-in-block-comment'
19    msgs: dict[str, tuple[str, str, str]] = Messages.of(_MSG)
20
21    def visit_module(self, node: Module) -> None:
22        self._process(node.doc_node)
23
24    def visit_classdef(self, node: ClassDef) -> None:
25        self._process(node.doc_node)
26
27    def visit_functiondef(self, node: FunctionDef) -> None:
28        self._process(node.doc_node)
29
30    def _process(self, docstring: Const | None) -> None:
31        # checks for violation
32        if docstring is None or \
33            '\n\n\n' not in docstring.value:
34            return
35        self.add_message(self._MSG, node=docstring)
name: str = 'duplicate-blank-line-in-block-comment'

Name of the provider.

msgs: dict[str, tuple[str, str, str]] = {'E6156': ('Remove consecutive blank line in \'"""\'.', 'duplicate.blank.line.in.block.comment', 'https://github.com/hanggrian/rulebook')}
def visit_module(self, node: astroid.nodes.scoped_nodes.scoped_nodes.Module) -> None:
21    def visit_module(self, node: Module) -> None:
22        self._process(node.doc_node)
def visit_classdef(self, node: astroid.nodes.scoped_nodes.scoped_nodes.ClassDef) -> None:
24    def visit_classdef(self, node: ClassDef) -> None:
25        self._process(node.doc_node)
def visit_functiondef(self, node: astroid.nodes.scoped_nodes.scoped_nodes.FunctionDef) -> None:
27    def visit_functiondef(self, node: FunctionDef) -> None:
28        self._process(node.doc_node)
class DuplicateBlankLineInCommentChecker(rulebook_pylint.checkers.rulebook_checkers.RulebookTokenChecker):
16class DuplicateBlankLineInCommentChecker(RulebookTokenChecker):
17    """See detail: https://hanggrian.github.io/rulebook/rules/#duplicate-blank-line-in-comment"""
18    _MSG: str = 'duplicate.blank.line.in.comment'
19
20    name: str = 'duplicate-blank-line-in-comment'
21    msgs: dict[str, tuple[str, str, str]] = Messages.of(_MSG)
22
23    def process_tokens(self, tokens: list[TokenInfo]) -> None:
24        last_empty_token: TokenInfo | None = None
25        # checks for violation
26        for token in [t for t in tokens if t.type == COMMENT and is_comment_empty(t)]:
27            if last_empty_token is None:
28                last_empty_token = token
29                continue
30            if last_empty_token.start[0] + 1 == token.start[0]:
31                self.add_message(self._MSG, line=token.start[0], col_offset=token.start[1])
32
33            # keep previous token for comparison
34            last_empty_token = token
name: str = 'duplicate-blank-line-in-comment'

Name of the provider.

msgs: dict[str, tuple[str, str, str]] = {'E6157': ("Remove consecutive blank line after '#'.", 'duplicate.blank.line.in.comment', 'https://github.com/hanggrian/rulebook')}
def process_tokens(self, tokens: list[tokenize.TokenInfo]) -> None:
23    def process_tokens(self, tokens: list[TokenInfo]) -> None:
24        last_empty_token: TokenInfo | None = None
25        # checks for violation
26        for token in [t for t in tokens if t.type == COMMENT and is_comment_empty(t)]:
27            if last_empty_token is None:
28                last_empty_token = token
29                continue
30            if last_empty_token.start[0] + 1 == token.start[0]:
31                self.add_message(self._MSG, line=token.start[0], col_offset=token.start[1])
32
33            # keep previous token for comparison
34            last_empty_token = token

Should be overridden by subclasses.

class DuplicateSpaceChecker(rulebook_pylint.checkers.rulebook_checkers.RulebookTokenChecker):
15class DuplicateSpaceChecker(RulebookTokenChecker):
16    """See detail: https://hanggrian.github.io/rulebook/rules/#duplicate-space"""
17    _MSG: str = 'duplicate.space'
18
19    name: str = 'duplicate-space'
20    msgs: dict[str, tuple[str, str, str]] = Messages.of(_MSG)
21
22    def process_tokens(self, tokens: list[TokenInfo]) -> None:
23        # fstring_flag: bool = False
24        for i, token in enumerate(tokens):
25            # get last token to compare
26            last_token: TokenInfo = tokens[i - 1]
27
28            # FSTRING_END unavailable on 3.11
29            # if last_token.type == FSTRING_END:
30            #     fstring_flag = False
31
32            # checks for violation
33            # if fstring_flag or not self._is_duplicate_space(token, last_token):
34            if not self._is_duplicate_space(token, last_token):
35                continue
36            self.add_message(
37                self._MSG,
38                line=last_token.start[0],
39                col_offset=last_token.start[1],
40            )
41
42            # if token.type == FSTRING_START:
43            #     fstring_flag = True
44
45    @staticmethod
46    def _is_duplicate_space(token: TokenInfo, last_token: TokenInfo) -> bool:
47        if any(
48            t in (NEWLINE, NL, INDENT, DEDENT, ENDMARKER)
49            for t in (token.type, last_token.type)
50        ):
51            return False
52        return token.start[1] - last_token.end[1] > 2 \
53            if token.type == COMMENT \
54            else token.start[1] - last_token.end[1] > 1
name: str = 'duplicate-space'

Name of the provider.

msgs: dict[str, tuple[str, str, str]] = {'E6158': ('Remove consecutive whitespace.', 'duplicate.space', 'https://github.com/hanggrian/rulebook')}
def process_tokens(self, tokens: list[tokenize.TokenInfo]) -> None:
22    def process_tokens(self, tokens: list[TokenInfo]) -> None:
23        # fstring_flag: bool = False
24        for i, token in enumerate(tokens):
25            # get last token to compare
26            last_token: TokenInfo = tokens[i - 1]
27
28            # FSTRING_END unavailable on 3.11
29            # if last_token.type == FSTRING_END:
30            #     fstring_flag = False
31
32            # checks for violation
33            # if fstring_flag or not self._is_duplicate_space(token, last_token):
34            if not self._is_duplicate_space(token, last_token):
35                continue
36            self.add_message(
37                self._MSG,
38                line=last_token.start[0],
39                col_offset=last_token.start[1],
40            )
41
42            # if token.type == FSTRING_START:
43            #     fstring_flag = True

Should be overridden by subclasses.

class GenericNameChecker(rulebook_pylint.checkers.rulebook_checkers.RulebookChecker):
15class GenericNameChecker(RulebookChecker):
16    """See detail: https://hanggrian.github.io/rulebook/rules/#generic-name"""
17    _MSG: str = 'generic.name'
18
19    name: str = 'generic-name'
20    msgs: dict[str, tuple[str, str, str]] = Messages.of(_MSG)
21
22    def visit_assign(self, node: Assign) -> None:
23        # only target TypeVar declaration
24        if not isinstance(node.value, Call) or \
25            not isinstance(node.value.func, Name) or \
26            node.value.func.name != 'TypeVar':
27            return
28
29        # checks for violation
30        target: AssignName | None = get_assignname(node)
31        if target is None:
32            return
33        if len(target.name) == 1 and \
34            target.name[0].isupper():
35            return
36        self.add_message(self._MSG, node=target)
name: str = 'generic-name'

Name of the provider.

msgs: dict[str, tuple[str, str, str]] = {'E6159': ('Use single uppercase letter.', 'generic.name', 'https://github.com/hanggrian/rulebook')}
def visit_assign(self, node: astroid.nodes.node_classes.Assign) -> None:
22    def visit_assign(self, node: Assign) -> None:
23        # only target TypeVar declaration
24        if not isinstance(node.value, Call) or \
25            not isinstance(node.value.func, Name) or \
26            node.value.func.name != 'TypeVar':
27            return
28
29        # checks for violation
30        target: AssignName | None = get_assignname(node)
31        if target is None:
32            return
33        if len(target.name) == 1 and \
34            target.name[0].isupper():
35            return
36        self.add_message(self._MSG, node=target)
class InnerClassPositionChecker(rulebook_pylint.checkers.rulebook_checkers.RulebookChecker):
14class InnerClassPositionChecker(RulebookChecker):
15    """See detail: https://hanggrian.github.io/rulebook/rules/#inner-class-position"""
16    _MSG: str = 'inner.class.position'
17
18    name: str = 'inner-class-position'
19    msgs: dict[str, tuple[str, str, str]] = Messages.of(_MSG)
20
21    def visit_classdef(self, node: ClassDef) -> None:
22        # consider only inner class
23        if node.parent and \
24            not isinstance(node.parent, ClassDef):
25            return
26
27        next2: NodeNG | None = node
28        while next2 is not None:
29            next2 = next2.next_sibling()
30
31            # checks for violation
32            if isinstance(next2, (FunctionDef, Assign, AssignName)):
33                self.add_message(self._MSG, node=node)
34                return
name: str = 'inner-class-position'

Name of the provider.

msgs: dict[str, tuple[str, str, str]] = {'E6160': ('Move inner class to the bottom.', 'inner.class.position', 'https://github.com/hanggrian/rulebook')}
def visit_classdef(self, node: astroid.nodes.scoped_nodes.scoped_nodes.ClassDef) -> None:
21    def visit_classdef(self, node: ClassDef) -> None:
22        # consider only inner class
23        if node.parent and \
24            not isinstance(node.parent, ClassDef):
25            return
26
27        next2: NodeNG | None = node
28        while next2 is not None:
29            next2 = next2.next_sibling()
30
31            # checks for violation
32            if isinstance(next2, (FunctionDef, Assign, AssignName)):
33                self.add_message(self._MSG, node=node)
34                return
class InternalErrorChecker(rulebook_pylint.checkers.rulebook_checkers.RulebookChecker):
14class InternalErrorChecker(RulebookChecker):
15    """See detail: https://hanggrian.github.io/rulebook/rules/#internal-error"""
16    _MSG: str = 'internal.error'
17
18    name: str = 'internal-error'
19    msgs: dict[str, tuple[str, str, str]] = Messages.of(_MSG)
20
21    def visit_classdef(self, node: ClassDef) -> None:
22        # checks for violation
23        for base in [
24            n for n in node.bases
25            if isinstance(n, Name) and \
26               n.name == 'BaseException'
27        ]:
28            self.add_message(self._MSG, node=base)
name: str = 'internal-error'

Name of the provider.

msgs: dict[str, tuple[str, str, str]] = {'E6161': ("Extend from class 'Exception'.", 'internal.error', 'https://github.com/hanggrian/rulebook')}
def visit_classdef(self, node: astroid.nodes.scoped_nodes.scoped_nodes.ClassDef) -> None:
21    def visit_classdef(self, node: ClassDef) -> None:
22        # checks for violation
23        for base in [
24            n for n in node.bases
25            if isinstance(n, Name) and \
26               n.name == 'BaseException'
27        ]:
28            self.add_message(self._MSG, node=base)
class LonelyCaseChecker(rulebook_pylint.checkers.rulebook_checkers.RulebookChecker):
14class LonelyCaseChecker(RulebookChecker):
15    """See detail: https://hanggrian.github.io/rulebook/rules/#lonely-case"""
16    _MSG: str = 'lonely.case'
17
18    name: str = 'lonely-case'
19    msgs: dict[str, tuple[str, str, str]] = Messages.of(_MSG)
20
21    def visit_match(self, node: Match) -> None:
22        # checks for violation
23        if len(node.cases) > 1:
24            return
25        self.add_message(self._MSG, node=node)
name: str = 'lonely-case'

Name of the provider.

msgs: dict[str, tuple[str, str, str]] = {'E6162': ("Replace 'match' with 'if' condition.", 'lonely.case', 'https://github.com/hanggrian/rulebook')}
def visit_match(self, node: astroid.nodes.node_classes.Match) -> None:
21    def visit_match(self, node: Match) -> None:
22        # checks for violation
23        if len(node.cases) > 1:
24            return
25        self.add_message(self._MSG, node=node)
class LowercaseHexadecimalChecker(rulebook_pylint.checkers.rulebook_checkers.RulebookChecker):
14class LowercaseHexadecimalChecker(RulebookChecker):
15    """See detail: https://hanggrian.github.io/rulebook/rules/#lowercase-hexadecimal"""
16    _MSG: str = 'lowercase.hexadecimal'
17
18    name: str = 'lowercase-hexadecimal'
19    msgs: dict[str, tuple[str, str, str]] = Messages.of(_MSG)
20
21    def visit_const(self, node: Const) -> None:
22        # checks for violation
23        if not isinstance(node.value, int):
24            return
25        line: bytes = node.root().stream().readlines()[node.fromlineno - 1]
26        value: str = line.decode('UTF-8')[node.col_offset: node.end_col_offset]
27        if not value.lower().startswith('0x'):
28            return
29        value_replacement: str = value.lower()
30        if value == value_replacement:
31            return
32        self.add_message(self._MSG, node=node, args=value_replacement)
name: str = 'lowercase-hexadecimal'

Name of the provider.

msgs: dict[str, tuple[str, str, str]] = {'E6163': ("Use hexadecimal '%s'.", 'lowercase.hexadecimal', 'https://github.com/hanggrian/rulebook')}
def visit_const(self, node: astroid.nodes.node_classes.Const) -> None:
21    def visit_const(self, node: Const) -> None:
22        # checks for violation
23        if not isinstance(node.value, int):
24            return
25        line: bytes = node.root().stream().readlines()[node.fromlineno - 1]
26        value: str = line.decode('UTF-8')[node.col_offset: node.end_col_offset]
27        if not value.lower().startswith('0x'):
28            return
29        value_replacement: str = value.lower()
30        if value == value_replacement:
31            return
32        self.add_message(self._MSG, node=node, args=value_replacement)
class MeaninglessWordChecker(rulebook_pylint.checkers.rulebook_checkers.RulebookChecker):
17class MeaninglessWordChecker(RulebookChecker):
18    """See detail: https://hanggrian.github.io/rulebook/rules/#meaningless-word"""
19    _MSG_ALL: str = 'meaningless.word.all'
20    _MSG_UTIL: str = 'meaningless.word.util'
21
22    _UTILITY_CLASS_NAMES: frozenset[str] = frozenset(['Util', 'Utility'])
23    _TITLE_CASE_REGEX: Pattern = \
24        re(
25            r'((^[a-z]+)|([0-9]+)|([A-Z]{1}[a-z]+)|' +
26            r'([A-Z]+(?=([A-Z][a-z])|($)|([0-9]))))',
27        )
28
29    name: str = 'meaningless-word'
30    msgs: dict[str, tuple[str, str, str]] = Messages.of(_MSG_ALL, _MSG_UTIL)
31    options: Options = (
32        MEANINGLESS_WORDS_OPTION,
33    )
34
35    _words: list[str]
36
37    def open(self) -> None:
38        self._words = self.linter.config.rulebook_meaningless_words
39
40    def visit_classdef(self, node: ClassDef) -> None:
41        # checks for violation
42        words: list[str] = [match[0] for match in self._TITLE_CASE_REGEX.findall(node.name)]
43        if not words or \
44            words[-1] not in self._words:
45            return
46        word: str = words[-1]
47        if word in self._UTILITY_CLASS_NAMES:
48            self.add_message(
49                self._MSG_UTIL,
50                node=node,
51                args=node.name[:node.name.index(word)] + 's',
52                col_offset= \
53                    node.col_offset + 6 \
54                        if node.col_offset is not None \
55                        else None,
56            )
57            return
58        self.add_message(
59            self._MSG_ALL,
60            node=node,
61            args=word,
62                col_offset= \
63                    node.col_offset + 6 \
64                        if node.col_offset is not None \
65                        else None,
66        )
name: str = 'meaningless-word'

Name of the provider.

msgs: dict[str, tuple[str, str, str]] = {'E6164': ("Avoid meaningless word '%s'.", 'meaningless.word.all', 'https://github.com/hanggrian/rulebook'), 'E6165': ("Rename utility class to '%s'.", 'meaningless.word.util', 'https://github.com/hanggrian/rulebook')}
options: 'Options' = (('rulebook-meaningless-words', {'default': ('Util', 'Utility', 'Helper', 'Manager', 'Wrapper'), 'type': 'csv', 'metavar': '<comma-separated values>', 'help': 'A set of banned names.'}),)

Options provided by this provider.

def open(self) -> None:
37    def open(self) -> None:
38        self._words = self.linter.config.rulebook_meaningless_words

Called before visiting project (i.e. set of modules).

def visit_classdef(self, node: astroid.nodes.scoped_nodes.scoped_nodes.ClassDef) -> None:
40    def visit_classdef(self, node: ClassDef) -> None:
41        # checks for violation
42        words: list[str] = [match[0] for match in self._TITLE_CASE_REGEX.findall(node.name)]
43        if not words or \
44            words[-1] not in self._words:
45            return
46        word: str = words[-1]
47        if word in self._UTILITY_CLASS_NAMES:
48            self.add_message(
49                self._MSG_UTIL,
50                node=node,
51                args=node.name[:node.name.index(word)] + 's',
52                col_offset= \
53                    node.col_offset + 6 \
54                        if node.col_offset is not None \
55                        else None,
56            )
57            return
58        self.add_message(
59            self._MSG_ALL,
60            node=node,
61            args=word,
62                col_offset= \
63                    node.col_offset + 6 \
64                        if node.col_offset is not None \
65                        else None,
66        )
class MemberOrderChecker(rulebook_pylint.checkers.rulebook_checkers.RulebookChecker):
16class MemberOrderChecker(RulebookChecker):
17    """See detail: https://hanggrian.github.io/rulebook/rules/#member-order"""
18    _MSG: str = 'member.order'
19
20    name: str = 'member-order'
21    msgs: dict[str, tuple[str, str, str]] = Messages.of(_MSG)
22    options: Options = (
23        MEMBER_ORDER_OPTION,
24    )
25
26    _member_order: list[str]
27    _property_position: int
28    _constructor_position: int
29    _function_position: int
30    _static_position: int
31
32    def open(self) -> None:
33        self._member_order = self.linter.config.rulebook_member_order
34        self._property_position = self._member_order.index('property')
35        self._constructor_position = self._member_order.index('constructor')
36        self._function_position = self._member_order.index('function')
37        self._static_position = self._member_order.index('static')
38
39    def visit_classdef(self, node: ClassDef) -> None:
40        # in Python, static members have are annotated
41        last_child: Assign | AssignName | FunctionDef | None = None
42        for child in [
43            n for n in node.values()
44            if isinstance(n, (Assign, AssignName, FunctionDef))
45        ]:
46            # checks for violation
47            if last_child is not None and \
48                self._get_member_position(last_child) > self._get_member_position(child):
49                self.add_message(
50                    self._MSG,
51                    node=child,
52                    args=(
53                        self._get_member_argument(child),
54                        self._get_member_argument(last_child),
55                    ),
56                )
57
58            last_child = child
59
60    def _get_member_position(self, node: Assign | AssignName | FunctionDef) -> int:
61        if isinstance(node, Assign):
62            return self._property_position
63        if isinstance(node, AssignName):
64            return self._property_position
65        if has_decorator(node, 'staticmethod'):
66            return self._static_position
67        return self._constructor_position \
68            if node.name == '__init__' \
69            else self._function_position
70
71    @staticmethod
72    def _get_member_argument(node: Assign | AssignName | FunctionDef) -> str:
73        if isinstance(node, Assign):
74            return 'property'
75        if isinstance(node, AssignName):
76            return 'property'
77        if has_decorator(node, 'staticmethod'):
78            return 'static member'
79        return 'constructor' \
80            if node.name == '__init__' \
81            else 'function'
name: str = 'member-order'

Name of the provider.

msgs: dict[str, tuple[str, str, str]] = {'E6166': ("Arrange '%s' before '%s'.", 'member.order', 'https://github.com/hanggrian/rulebook')}
options: 'Options' = (('rulebook-member-order', {'default': ('property', 'constructor', 'function', 'static'), 'type': 'csv', 'metavar': '<comma-separated values>', 'help': 'The structure of a class body.'}),)

Options provided by this provider.

def open(self) -> None:
32    def open(self) -> None:
33        self._member_order = self.linter.config.rulebook_member_order
34        self._property_position = self._member_order.index('property')
35        self._constructor_position = self._member_order.index('constructor')
36        self._function_position = self._member_order.index('function')
37        self._static_position = self._member_order.index('static')

Called before visiting project (i.e. set of modules).

def visit_classdef(self, node: astroid.nodes.scoped_nodes.scoped_nodes.ClassDef) -> None:
39    def visit_classdef(self, node: ClassDef) -> None:
40        # in Python, static members have are annotated
41        last_child: Assign | AssignName | FunctionDef | None = None
42        for child in [
43            n for n in node.values()
44            if isinstance(n, (Assign, AssignName, FunctionDef))
45        ]:
46            # checks for violation
47            if last_child is not None and \
48                self._get_member_position(last_child) > self._get_member_position(child):
49                self.add_message(
50                    self._MSG,
51                    node=child,
52                    args=(
53                        self._get_member_argument(child),
54                        self._get_member_argument(last_child),
55                    ),
56                )
57
58            last_child = child
class MemberSeparatorChecker(rulebook_pylint.checkers.rulebook_checkers.RulebookFileChecker):
15class MemberSeparatorChecker(RulebookFileChecker):
16    """See detail: https://hanggrian.github.io/rulebook/rules/#member-separator"""
17    _MSG: str = 'member.separator'
18
19    name: str = 'member-separator'
20    msgs: dict[str, tuple[str, str, str]] = Messages.of(_MSG)
21
22    def visit_classdef(self, node: ClassDef) -> None:
23        # collect members
24        members: list[NodeNG] = [n for n in node.body if isinstance(n, (Assign, FunctionDef))]
25
26        for (i, member) in enumerate(members):
27            if i == 0:
28                continue
29            last_member: NodeNG = members[i - 1]
30
31            # single-line fields can be joined
32            if isinstance(last_member, Assign) and \
33                isinstance(member, Assign):
34                continue
35            key: str
36            last_body: NodeNG
37            if isinstance(last_member, FunctionDef):
38                key = 'constructor' if last_member.name == '__init__' else 'function'
39                last_body = last_member.body[-1]
40            else:
41                key = 'property'
42                last_body = last_member
43
44            # checks for violation
45            if last_body.end_lineno != get_fromlineno_before(self.lines, member, last_body):
46                continue
47            self.add_message(
48                self._MSG,
49                args=key,
50                line=last_body.lineno,
51                end_lineno=last_body.end_lineno,
52                col_offset=last_body.col_offset,
53                end_col_offset=last_body.end_col_offset,
54            )
name: str = 'member-separator'

Name of the provider.

msgs: dict[str, tuple[str, str, str]] = {'E6167': ("Add blank line after '%s'.", 'member.separator', 'https://github.com/hanggrian/rulebook')}
def visit_classdef(self, node: astroid.nodes.scoped_nodes.scoped_nodes.ClassDef) -> None:
22    def visit_classdef(self, node: ClassDef) -> None:
23        # collect members
24        members: list[NodeNG] = [n for n in node.body if isinstance(n, (Assign, FunctionDef))]
25
26        for (i, member) in enumerate(members):
27            if i == 0:
28                continue
29            last_member: NodeNG = members[i - 1]
30
31            # single-line fields can be joined
32            if isinstance(last_member, Assign) and \
33                isinstance(member, Assign):
34                continue
35            key: str
36            last_body: NodeNG
37            if isinstance(last_member, FunctionDef):
38                key = 'constructor' if last_member.name == '__init__' else 'function'
39                last_body = last_member.body[-1]
40            else:
41                key = 'property'
42                last_body = last_member
43
44            # checks for violation
45            if last_body.end_lineno != get_fromlineno_before(self.lines, member, last_body):
46                continue
47            self.add_message(
48                self._MSG,
49                args=key,
50                line=last_body.lineno,
51                end_lineno=last_body.end_lineno,
52                col_offset=last_body.col_offset,
53                end_col_offset=last_body.end_col_offset,
54            )
class NamedImportOrderChecker(rulebook_pylint.checkers.rulebook_checkers.RulebookChecker):
14class NamedImportOrderChecker(RulebookChecker):
15    """See detail: https://hanggrian.github.io/rulebook/rules/#named-import-order"""
16    _MSG: str = 'named.import.order'
17
18    name: str = 'named-import-order'
19    msgs: dict[str, tuple[str, str, str]] = Messages.of(_MSG)
20
21    def visit_importfrom(self, node: ImportFrom) -> None:
22        last_name: str | None = None
23        for name in node.names:
24            import_name: str = name[0]
25
26            # checks for violation
27            if last_name is not None and \
28                last_name > import_name:
29                self.add_message(
30                    self._MSG,
31                    node=node,
32                    args=(import_name, last_name),
33                )
34
35            last_name = import_name
name: str = 'named-import-order'

Name of the provider.

msgs: dict[str, tuple[str, str, str]] = {'E6168': ("Import '%s' before '%s'.", 'named.import.order', 'https://github.com/hanggrian/rulebook')}
def visit_importfrom(self, node: astroid.nodes.node_classes.ImportFrom) -> None:
21    def visit_importfrom(self, node: ImportFrom) -> None:
22        last_name: str | None = None
23        for name in node.names:
24            import_name: str = name[0]
25
26            # checks for violation
27            if last_name is not None and \
28                last_name > import_name:
29                self.add_message(
30                    self._MSG,
31                    node=node,
32                    args=(import_name, last_name),
33                )
34
35            last_name = import_name
class NestedIfElseChecker(rulebook_pylint.checkers.rulebook_checkers.RulebookChecker):
15class NestedIfElseChecker(RulebookChecker):
16    """See detail: https://hanggrian.github.io/rulebook/rules/#nested-if-else"""
17    _MSG_INVERT: str = 'nested.if.else.invert'
18    _MSG_LIFT: str = 'nested.if.else.lift'
19
20    name: str = 'nested-if-else'
21    msgs: dict[str, tuple[str, str, str]] = Messages.of(_MSG_INVERT, _MSG_LIFT)
22
23    def visit_for(self, node: For) -> None:
24        self._process(node.body)
25
26    def visit_while(self, node: While) -> None:
27        self._process(node.body)
28
29    def visit_functiondef(self, node: FunctionDef) -> None:
30        self._process(node.body)
31
32    def _process(self, body: list[NodeNG]) -> None:
33        # get last if
34        if2: If | None = None
35        children: list[NodeNG] = body.copy()
36        children.reverse()
37        for child in children:
38            if isinstance(child, If):
39                if2 = child
40                break
41            return
42        if if2 is None:
43            return
44
45        # checks for violation
46        else2: list[NodeNG] = if2.orelse
47
48        if else2:
49            else_first_child: NodeNG = else2[0]
50            if self._else_has_if(else2):
51                return
52            if self._has_multiple_lines(else2):
53                self.add_message(self._MSG_LIFT, node=else_first_child)
54            return
55        if has_jump_statement(if2):
56            return
57        if self._has_multiple_lines(if2.body):
58            self.add_message(self._MSG_INVERT, node=if2)
59
60    @staticmethod
61    def _else_has_if(nodes: list[NodeNG]) -> bool:
62        return any(isinstance(node, If) for node in nodes)
63
64    @staticmethod
65    def _has_multiple_lines(nodes: list[NodeNG]) -> bool:
66        length: int = len(nodes)
67        if length == 1:
68            return is_multiline(nodes[0])
69        return length > 1
name: str = 'nested-if-else'

Name of the provider.

msgs: dict[str, tuple[str, str, str]] = {'E6169': ("Invert 'if' condition.", 'nested.if.else.invert', 'https://github.com/hanggrian/rulebook'), 'E6170': ("Lift 'else' and add 'return' in 'if' block.", 'nested.if.else.lift', 'https://github.com/hanggrian/rulebook')}
def visit_for(self, node: astroid.nodes.node_classes.For) -> None:
23    def visit_for(self, node: For) -> None:
24        self._process(node.body)
def visit_while(self, node: astroid.nodes.node_classes.While) -> None:
26    def visit_while(self, node: While) -> None:
27        self._process(node.body)
def visit_functiondef(self, node: astroid.nodes.scoped_nodes.scoped_nodes.FunctionDef) -> None:
29    def visit_functiondef(self, node: FunctionDef) -> None:
30        self._process(node.body)
class ParameterWrapChecker(rulebook_pylint.checkers.rulebook_checkers.RulebookChecker):
14class ParameterWrapChecker(RulebookChecker):
15    """See detail: https://hanggrian.github.io/rulebook/rules/#parameter-wrap"""
16    _MSG: str = 'parameter.wrap'
17
18    name: str = 'parameter-wrap'
19    msgs: dict[str, tuple[str, str, str]] = Messages.of(_MSG)
20
21    def visit_functiondef(self, node: FunctionDef) -> None:
22        args: list[AssignName] | None = node.args.args
23        if args is None:
24            return
25        self._process(args)
26
27    def visit_call(self, node: Call) -> None:
28        self._process(node.args)
29
30    def _process(self, parameters: list[NodeNG]) -> None:
31        # target multiline parameters
32        if not parameters or \
33            parameters[0].lineno == parameters[-1].end_lineno:
34            return
35
36        # checks for violation
37        for i, parameter in enumerate(parameters):
38            if i == 0 or \
39                parameters[i - 1].end_lineno != parameter.lineno:
40                continue
41            self.add_message(self._MSG, node=parameter)
name: str = 'parameter-wrap'

Name of the provider.

msgs: dict[str, tuple[str, str, str]] = {'E6171': ('Break each parameter into newline.', 'parameter.wrap', 'https://github.com/hanggrian/rulebook')}
def visit_functiondef(self, node: astroid.nodes.scoped_nodes.scoped_nodes.FunctionDef) -> None:
21    def visit_functiondef(self, node: FunctionDef) -> None:
22        args: list[AssignName] | None = node.args.args
23        if args is None:
24            return
25        self._process(args)
def visit_call(self, node: astroid.nodes.node_classes.Call) -> None:
27    def visit_call(self, node: Call) -> None:
28        self._process(node.args)
class ParenthesesClipChecker(rulebook_pylint.checkers.rulebook_checkers.RulebookTokenChecker):
15class ParenthesesClipChecker(RulebookTokenChecker):
16    """See detail: https://hanggrian.github.io/rulebook/rules/#parentheses-clip"""
17    _MSG: str = 'parentheses.clip'
18
19    _PARENTHESES: dict[str, str] = {
20        '{': '}',
21        '(': ')',
22        '[': ']',
23    }
24    _OPENING_PARENTHESES: frozenset[str] = frozenset(['{', '(', '['])
25
26    name: str = 'parentheses-clip'
27    msgs: dict[str, tuple[str, str, str]] = Messages.of(_MSG)
28
29    def process_tokens(self, tokens: list[TokenInfo]) -> None:
30        for i, token in enumerate(tokens):
31            # find opening parenthesis
32            if token.type != OP or \
33                token.string not in self._OPENING_PARENTHESES:
34                continue
35            self._process(tokens, i, token)
36
37    def _process(self, tokens: list[TokenInfo], i: int, token: TokenInfo) -> None:
38        j: int = i + 1
39        end_parenthesis: str = self._PARENTHESES[token.string]
40
41        # compare position when there is only whitespace between parentheses
42        if j < len(tokens):
43            next_token: TokenInfo = tokens[j]
44            if next_token.type == OP and \
45                next_token.string == end_parenthesis:
46                # checks for violation
47                if token.end[1] != next_token.start[1]:
48                    self.add_message(
49                        self._MSG,
50                        line=token.start[0],
51                        col_offset=token.start[1],
52                        end_lineno=next_token.end[0],
53                        end_col_offset=next_token.end[1],
54                        args=token.string + end_parenthesis,
55                    )
56                return
57
58        # otherwise iterate to determine newline
59        has_newline: bool = False
60        while j < len(tokens):
61            curr_token: TokenInfo = tokens[j]
62            # checks for violation
63            if curr_token.type == NL:
64                has_newline = True
65            elif curr_token.type == OP and \
66                curr_token.string == end_parenthesis:
67                if has_newline:
68                    self.add_message(
69                        self._MSG,
70                        line=token.start[0],
71                        col_offset=token.start[1],
72                        end_lineno=curr_token.end[0],
73                        end_col_offset=curr_token.end[1],
74                        args=token.string + end_parenthesis,
75                    )
76                    return
77            else:
78                return
79            j += 1
name: str = 'parentheses-clip'

Name of the provider.

msgs: dict[str, tuple[str, str, str]] = {'E6172': ("Convert into '%s'.", 'parentheses.clip', 'https://github.com/hanggrian/rulebook')}
def process_tokens(self, tokens: list[tokenize.TokenInfo]) -> None:
29    def process_tokens(self, tokens: list[TokenInfo]) -> None:
30        for i, token in enumerate(tokens):
31            # find opening parenthesis
32            if token.type != OP or \
33                token.string not in self._OPENING_PARENTHESES:
34                continue
35            self._process(tokens, i, token)

Should be overridden by subclasses.

class ParenthesesTrimChecker(rulebook_pylint.checkers.rulebook_checkers.RulebookTokenChecker):
15class ParenthesesTrimChecker(RulebookTokenChecker):
16    """See detail: https://hanggrian.github.io/rulebook/rules/#parentheses-trim"""
17    _MSG_FIRST: str = 'parentheses.trim.first'
18    _MSG_LAST: str = 'parentheses.trim.last'
19
20    _OPENING_PARENTHESES: frozenset[str] = frozenset(['(', '[', '{'])
21    _CLOSING_PARENTHESES: frozenset[str] = frozenset([')', ']', '}'])
22
23    name: str = 'parentheses-trim'
24    msgs: dict[str, tuple[str, str, str]] = Messages.of(_MSG_FIRST, _MSG_LAST)
25
26    def process_tokens(self, tokens: list[TokenInfo]) -> None:
27        for i, token in enumerate(tokens):
28            # find opening and closing parentheses
29            if token.type != OP:
30                continue
31            if token.string in self._OPENING_PARENTHESES:
32                # checks for violation
33                if i + 2 >= len(tokens):
34                    continue
35                next_token: TokenInfo = tokens[i + 1]
36                next_token2: TokenInfo = tokens[i + 2]
37                if next_token.type != NL or \
38                    next_token2.type != NL:
39                    continue
40                self.add_message(
41                    self._MSG_FIRST,
42                    line=next_token2.start[0],
43                    col_offset=next_token2.start[1],
44                    args=token.string,
45                )
46
47            # checks for violation
48            if token.string not in self._CLOSING_PARENTHESES:
49                continue
50            if i - 2 < 0:
51                continue
52            prev_token: TokenInfo = tokens[i - 1]
53            prev_token2: TokenInfo = tokens[i - 2]
54            if prev_token.type != NL or \
55                prev_token2.type != NL:
56                continue
57            self.add_message(
58                self._MSG_LAST,
59                line=prev_token.start[0],
60                col_offset=prev_token.start[1],
61                args=token.string,
62            )
name: str = 'parentheses-trim'

Name of the provider.

msgs: dict[str, tuple[str, str, str]] = {'E6173': ("Remove blank line after '%s'.", 'parentheses.trim.first', 'https://github.com/hanggrian/rulebook'), 'E6174': ("Remove blank line before '%s'.", 'parentheses.trim.last', 'https://github.com/hanggrian/rulebook')}
def process_tokens(self, tokens: list[tokenize.TokenInfo]) -> None:
26    def process_tokens(self, tokens: list[TokenInfo]) -> None:
27        for i, token in enumerate(tokens):
28            # find opening and closing parentheses
29            if token.type != OP:
30                continue
31            if token.string in self._OPENING_PARENTHESES:
32                # checks for violation
33                if i + 2 >= len(tokens):
34                    continue
35                next_token: TokenInfo = tokens[i + 1]
36                next_token2: TokenInfo = tokens[i + 2]
37                if next_token.type != NL or \
38                    next_token2.type != NL:
39                    continue
40                self.add_message(
41                    self._MSG_FIRST,
42                    line=next_token2.start[0],
43                    col_offset=next_token2.start[1],
44                    args=token.string,
45                )
46
47            # checks for violation
48            if token.string not in self._CLOSING_PARENTHESES:
49                continue
50            if i - 2 < 0:
51                continue
52            prev_token: TokenInfo = tokens[i - 1]
53            prev_token2: TokenInfo = tokens[i - 2]
54            if prev_token.type != NL or \
55                prev_token2.type != NL:
56                continue
57            self.add_message(
58                self._MSG_LAST,
59                line=prev_token.start[0],
60                col_offset=prev_token.start[1],
61                args=token.string,
62            )

Should be overridden by subclasses.

class RedundantDefaultChecker(rulebook_pylint.checkers.rulebook_checkers.RulebookChecker):
15class RedundantDefaultChecker(RulebookChecker):
16    """See detail: https://hanggrian.github.io/rulebook/rules/#redundant-default"""
17    _MSG: str = 'redundant.default'
18
19    name: str = 'redundant-default'
20    msgs: dict[str, tuple[str, str, str]] = Messages.of(_MSG)
21
22    def visit_match(self, node: Match) -> None:
23        # skip no default
24        if not node.cases:
25            return
26        default: MatchCase = node.cases[-1]
27        if not isinstance(default.pattern, MatchAs) or \
28            default.pattern.name:
29            return
30
31        # checks for violation
32        if not all(has_jump_statement(node) for node in node.cases[:-1]):
33            return
34        self.add_message(self._MSG, node=default)
name: str = 'redundant-default'

Name of the provider.

msgs: dict[str, tuple[str, str, str]] = {'E6175': ("Omit redundant 'case _' condition.", 'redundant.default', 'https://github.com/hanggrian/rulebook')}
def visit_match(self, node: astroid.nodes.node_classes.Match) -> None:
22    def visit_match(self, node: Match) -> None:
23        # skip no default
24        if not node.cases:
25            return
26        default: MatchCase = node.cases[-1]
27        if not isinstance(default.pattern, MatchAs) or \
28            default.pattern.name:
29            return
30
31        # checks for violation
32        if not all(has_jump_statement(node) for node in node.cases[:-1]):
33            return
34        self.add_message(self._MSG, node=default)
class TodoCommentChecker(rulebook_pylint.checkers.rulebook_checkers.RulebookFileChecker):
18class TodoCommentChecker(RulebookFileChecker):
19    """See detail: https://hanggrian.github.io/rulebook/rules/#todo-comment"""
20    _MSG_KEYWORD: str = 'todo.comment.keyword'
21    _MSG_SEPARATOR: str = 'todo.comment.separator'
22
23    _KEYWORD_REGEX = regex(r'\b(?i:fixme|todo)(?<!FIXME|TODO)\b')
24    _SEPARATOR_REGEX = regex(r'\b(todo|fixme)\S', IGNORECASE)
25
26    name: str = 'todo-comment'
27    msgs: dict[str, tuple[str, str, str]] = Messages.of(_MSG_KEYWORD, _MSG_SEPARATOR)
28
29    def process_module(self, node: Module) -> None:
30        with node.stream() as stream:
31            for i, line in enumerate(stream):
32                # obtain comment content
33                line_no: int = i + 1
34                parts: list[bytes] = line.split(b'#', 1)
35                if len(parts) < 2:
36                    continue
37                comment_content: str = decode(parts[1])
38
39                # checks for violation
40                match = self._KEYWORD_REGEX.search(comment_content)
41                if match is not None:
42                    self.add_message(self._MSG_KEYWORD, line=line_no, args=match.group(0))
43                match = self._SEPARATOR_REGEX.search(comment_content)
44                if match is not None:
45                    self.add_message(self._MSG_SEPARATOR, line=line_no, args=match.group(0)[-1])
name: str = 'todo-comment'

Name of the provider.

msgs: dict[str, tuple[str, str, str]] = {'E6176': ("Capitalize keyword '%s'.", 'todo.comment.keyword', 'https://github.com/hanggrian/rulebook'), 'E6177': ("Omit separator '%s'.", 'todo.comment.separator', 'https://github.com/hanggrian/rulebook')}
def process_module(self, node: astroid.nodes.scoped_nodes.scoped_nodes.Module) -> None:
29    def process_module(self, node: Module) -> None:
30        with node.stream() as stream:
31            for i, line in enumerate(stream):
32                # obtain comment content
33                line_no: int = i + 1
34                parts: list[bytes] = line.split(b'#', 1)
35                if len(parts) < 2:
36                    continue
37                comment_content: str = decode(parts[1])
38
39                # checks for violation
40                match = self._KEYWORD_REGEX.search(comment_content)
41                if match is not None:
42                    self.add_message(self._MSG_KEYWORD, line=line_no, args=match.group(0))
43                match = self._SEPARATOR_REGEX.search(comment_content)
44                if match is not None:
45                    self.add_message(self._MSG_SEPARATOR, line=line_no, args=match.group(0)[-1])

Process a module.

The module's content is accessible via astroid.stream

class TrailingCommaChecker(rulebook_pylint.checkers.rulebook_checkers.RulebookTokenChecker):
15class TrailingCommaChecker(RulebookTokenChecker):
16    """See detail: https://hanggrian.github.io/rulebook/rules/#trailing-comma"""
17    _MSG_SINGLE: str = 'trailing.comma.single'
18    _MSG_MULTI: str = 'trailing.comma.multi'
19
20    _OPENING_PARENTHESES: frozenset[str] = frozenset(['(', '[', '{'])
21    _CLOSING_PARENTHESES: frozenset[str] = frozenset([')', ']', '}'])
22
23    name: str = 'trailing-comma'
24    msgs: dict[str, tuple[str, str, str]] = Messages.of(_MSG_SINGLE, _MSG_MULTI)
25
26    def process_tokens(self, tokens: list[TokenInfo]) -> None:
27        # filter out comments
28        tokens = [t for t in tokens if t.type != COMMENT]
29
30        token: TokenInfo
31        for i, token in enumerate(tokens):
32            # find closing parenthesis
33            if token.type != OP or \
34                token.string not in self._CLOSING_PARENTHESES:
35                continue
36
37            # skip sole generator like `any(...)`
38            if self._is_sole_generator(tokens, i):
39                continue
40
41            # checks for violation
42            prev_token: TokenInfo = tokens[i - 1]
43            prev_token2: TokenInfo = tokens[i - 2]
44            if prev_token.type == OP and \
45                prev_token.string == ',':
46                self.add_message(
47                    self._MSG_SINGLE,
48                    line=prev_token.start[0],
49                    col_offset=prev_token.end[1],
50                )
51                continue
52            if prev_token.type != NL:
53                continue
54            if prev_token2.type == OP and \
55                prev_token2.string == ',':
56                continue
57
58            self.add_message(
59                self._MSG_MULTI,
60                line=prev_token2.start[0],
61                col_offset=prev_token2.end[1],
62            )
63
64    def _is_sole_generator(self, tokens: list[TokenInfo], close_index: int) -> bool:
65        nesting: int = 0
66        has_for: bool = False
67        has_comma_at_root: bool = False
68        for i in range(close_index - 1, -1, -1):
69            token: TokenInfo = tokens[i]
70            if token.type == OP:
71                if token.string in self._CLOSING_PARENTHESES:
72                    nesting += 1
73                elif token.string in self._OPENING_PARENTHESES:
74                    if nesting == 0:
75                        return has_for and not has_comma_at_root
76                    nesting -= 1
77                elif token.string == ',' and \
78                    nesting == 0:
79                    if i != close_index - 1:
80                        has_comma_at_root = True
81            elif token.type == NAME and \
82                token.string == 'for' and \
83                nesting == 0:
84                has_for = True
85                has_comma_at_root = False
86        return False
name: str = 'trailing-comma'

Name of the provider.

msgs: dict[str, tuple[str, str, str]] = {'E6178': ('Remove trailing comma.', 'trailing.comma.single', 'https://github.com/hanggrian/rulebook'), 'E6179': ('Put trailing comma.', 'trailing.comma.multi', 'https://github.com/hanggrian/rulebook')}
def process_tokens(self, tokens: list[tokenize.TokenInfo]) -> None:
26    def process_tokens(self, tokens: list[TokenInfo]) -> None:
27        # filter out comments
28        tokens = [t for t in tokens if t.type != COMMENT]
29
30        token: TokenInfo
31        for i, token in enumerate(tokens):
32            # find closing parenthesis
33            if token.type != OP or \
34                token.string not in self._CLOSING_PARENTHESES:
35                continue
36
37            # skip sole generator like `any(...)`
38            if self._is_sole_generator(tokens, i):
39                continue
40
41            # checks for violation
42            prev_token: TokenInfo = tokens[i - 1]
43            prev_token2: TokenInfo = tokens[i - 2]
44            if prev_token.type == OP and \
45                prev_token.string == ',':
46                self.add_message(
47                    self._MSG_SINGLE,
48                    line=prev_token.start[0],
49                    col_offset=prev_token.end[1],
50                )
51                continue
52            if prev_token.type != NL:
53                continue
54            if prev_token2.type == OP and \
55                prev_token2.string == ',':
56                continue
57
58            self.add_message(
59                self._MSG_MULTI,
60                line=prev_token2.start[0],
61                col_offset=prev_token2.end[1],
62            )

Should be overridden by subclasses.

class UnnecessaryAbstractChecker(rulebook_pylint.checkers.rulebook_checkers.RulebookChecker):
16class UnnecessaryAbstractChecker(RulebookChecker):
17    """See detail: https://hanggrian.github.io/rulebook/rules/#unnecessary-abstract"""
18    _MSG: str = 'unnecessary.abstract'
19
20    name: str = 'unnecessary-abstract'
21    msgs: dict[str, tuple[str, str, str]] = Messages.of(_MSG)
22
23    def visit_classdef(self, node: ClassDef) -> None:
24        # skip non-abstract class
25        if not any(isinstance(n, Name) and n.name == 'ABC' for n in node.bases):
26            return
27
28        # checks for violation
29        if len(node.bases) > 1 or \
30            any(
31                isinstance(n, FunctionDef) and
32                has_decorator(n, 'abstractmethod')
33                for n in node.body
34            ):
35            return
36        base: NodeNG | Proxy = node.bases[0]
37        self.add_message(self._MSG, node=base)
name: str = 'unnecessary-abstract'

Name of the provider.

msgs: dict[str, tuple[str, str, str]] = {'E6180': ("Omit 'abstract' modifier.", 'unnecessary.abstract', 'https://github.com/hanggrian/rulebook')}
def visit_classdef(self, node: astroid.nodes.scoped_nodes.scoped_nodes.ClassDef) -> None:
23    def visit_classdef(self, node: ClassDef) -> None:
24        # skip non-abstract class
25        if not any(isinstance(n, Name) and n.name == 'ABC' for n in node.bases):
26            return
27
28        # checks for violation
29        if len(node.bases) > 1 or \
30            any(
31                isinstance(n, FunctionDef) and
32                has_decorator(n, 'abstractmethod')
33                for n in node.body
34            ):
35            return
36        base: NodeNG | Proxy = node.bases[0]
37        self.add_message(self._MSG, node=base)
class UnnecessaryBlankLineAfterColonChecker(rulebook_pylint.checkers.rulebook_checkers.RulebookTokenChecker):
15class UnnecessaryBlankLineAfterColonChecker(RulebookTokenChecker):
16    """See detail: https://hanggrian.github.io/rulebook/rules/#unnecessary-blank-line-after-colon"""
17    _MSG: str = 'unnecessary.blank.line.after.colon'
18
19    name: str = 'unnecessary-blank-line-after-colon'
20    msgs: dict[str, tuple[str, str, str]] = Messages.of(_MSG)
21
22    def process_tokens(self, tokens: list[TokenInfo]) -> None:
23        for i, token in enumerate(tokens):
24            # target colon operator
25            if token.type != OP or \
26                token.string != ':':
27                continue
28
29            # checks for violation
30            if i + 2 >= len(tokens):
31                continue
32            next_token: TokenInfo = tokens[i + 1]
33            next_token2: TokenInfo = tokens[i + 2]
34            if next_token.type != NEWLINE or \
35                next_token2.type != NL:
36                continue
37            self.add_message(self._MSG, line=next_token2.start[0], col_offset=next_token2.start[1])
name: str = 'unnecessary-blank-line-after-colon'

Name of the provider.

msgs: dict[str, tuple[str, str, str]] = {'E6181': ("Remove blank line after ':'.", 'unnecessary.blank.line.after.colon', 'https://github.com/hanggrian/rulebook')}
def process_tokens(self, tokens: list[tokenize.TokenInfo]) -> None:
22    def process_tokens(self, tokens: list[TokenInfo]) -> None:
23        for i, token in enumerate(tokens):
24            # target colon operator
25            if token.type != OP or \
26                token.string != ':':
27                continue
28
29            # checks for violation
30            if i + 2 >= len(tokens):
31                continue
32            next_token: TokenInfo = tokens[i + 1]
33            next_token2: TokenInfo = tokens[i + 2]
34            if next_token.type != NEWLINE or \
35                next_token2.type != NL:
36                continue
37            self.add_message(self._MSG, line=next_token2.start[0], col_offset=next_token2.start[1])

Should be overridden by subclasses.

class UnnecessaryContinueChecker(rulebook_pylint.checkers.rulebook_checkers.RulebookChecker):
14class UnnecessaryContinueChecker(RulebookChecker):
15    """See detail: https://hanggrian.github.io/rulebook/rules/#unnecessary-continue"""
16    _MSG: str = 'unnecessary.continue'
17
18    name: str = 'unnecessary-continue'
19    msgs: dict[str, tuple[str, str, str]] = Messages.of(_MSG)
20
21    def visit_for(self, node: For) -> None:
22        self._process(node.body)
23
24    def visit_while(self, node: While) -> None:
25        self._process(node.body)
26
27    def _process(self, body: list[NodeNG]) -> None:
28        # checks for violation
29        if not body:
30            return
31        continue2: NodeNG = body[-1]
32        if not isinstance(continue2, Continue):
33            return
34        self.add_message(self._MSG, node=continue2)
name: str = 'unnecessary-continue'

Name of the provider.

msgs: dict[str, tuple[str, str, str]] = {'E6182': ("Last 'continue' is not needed.", 'unnecessary.continue', 'https://github.com/hanggrian/rulebook')}
def visit_for(self, node: astroid.nodes.node_classes.For) -> None:
21    def visit_for(self, node: For) -> None:
22        self._process(node.body)
def visit_while(self, node: astroid.nodes.node_classes.While) -> None:
24    def visit_while(self, node: While) -> None:
25        self._process(node.body)
class UnnecessaryLeadingBlankLineChecker(rulebook_pylint.checkers.rulebook_checkers.RulebookTokenChecker):
15class UnnecessaryLeadingBlankLineChecker(RulebookTokenChecker):
16    """See detail: https://hanggrian.github.io/rulebook/rules/#unnecessary-leading-blank-line"""
17    _MSG: str = 'unnecessary.leading.blank.line'
18
19    name: str = 'unnecessary-leading-blank-line'
20    msgs: dict[str, tuple[str, str, str]] = Messages.of(_MSG)
21
22    def process_tokens(self, tokens: list[TokenInfo]) -> None:
23        for token in [t for t in tokens if t.type != ENCODING]:
24            # checks for violation
25            if token.type == NL:
26                self.add_message(self._MSG, line=1)
27            return
name: str = 'unnecessary-leading-blank-line'

Name of the provider.

msgs: dict[str, tuple[str, str, str]] = {'E6183': ('Remove blank line at the beginning.', 'unnecessary.leading.blank.line', 'https://github.com/hanggrian/rulebook')}
def process_tokens(self, tokens: list[tokenize.TokenInfo]) -> None:
22    def process_tokens(self, tokens: list[TokenInfo]) -> None:
23        for token in [t for t in tokens if t.type != ENCODING]:
24            # checks for violation
25            if token.type == NL:
26                self.add_message(self._MSG, line=1)
27            return

Should be overridden by subclasses.

def initialize(linter: pylint.lint.pylinter.PyLinter) -> None:
51def initialize(linter: PyLinter) -> None:
52    register_plugins(linter, __path__[0])