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 )
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)
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.
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)
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')}
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 )
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])
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
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()
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
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
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)
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)
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)
msgs: dict[str, tuple[str, str, str]] =
{'E6156': ('Remove consecutive blank line in \'"""\'.', 'duplicate.blank.line.in.block.comment', 'https://github.com/hanggrian/rulebook')}
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
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
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)
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
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)
msgs: dict[str, tuple[str, str, str]] =
{'E6161': ("Extend from class 'Exception'.", 'internal.error', 'https://github.com/hanggrian/rulebook')}
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)
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)
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 )
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
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'
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 )
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
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
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')}
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)
msgs: dict[str, tuple[str, str, str]] =
{'E6171': ('Break each parameter into newline.', 'parameter.wrap', 'https://github.com/hanggrian/rulebook')}
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
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 )
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)
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])
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
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)
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])
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)
msgs: dict[str, tuple[str, str, str]] =
{'E6182': ("Last 'continue' is not needed.", 'unnecessary.continue', 'https://github.com/hanggrian/rulebook')}
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
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: