mirror of
https://gitlab.com/bramw/baserow.git
synced 2024-11-27 01:37:53 +00:00
174 lines
5.0 KiB
Python
174 lines
5.0 KiB
Python
from functools import partial
|
|
|
|
try:
|
|
from functools import cached_property # only present in python >= 3.8
|
|
except ImportError:
|
|
from backports.cached_property import cached_property
|
|
|
|
import ast
|
|
from tokenize import COMMENT, TokenInfo, generate_tokens
|
|
from typing import Any, Dict, Generator, Iterable, List, Optional, Tuple, Type, Union
|
|
|
|
import pycodestyle
|
|
|
|
try:
|
|
from flake8.engine import pep8 as stdin_utils
|
|
except ImportError:
|
|
from flake8 import utils as stdin_utils
|
|
|
|
|
|
DocstringType = Union[ast.Constant, ast.Str]
|
|
ERR_MSG = "X1 - Baserow plugin: missing empty line after docstring"
|
|
|
|
|
|
class Token:
|
|
def __init__(self, token: TokenInfo):
|
|
self.token = token
|
|
|
|
@property # noqa: A003
|
|
def type(self) -> int:
|
|
return self.token[0]
|
|
|
|
@property
|
|
def start(self) -> Tuple[int, int]:
|
|
return self.token[2]
|
|
|
|
@property
|
|
def start_row(self) -> int:
|
|
return self.start[0]
|
|
|
|
@property
|
|
def col_offset(self) -> int:
|
|
return self.start[1]
|
|
|
|
|
|
class FunctionNodeHelper:
|
|
def __init__(self, node: ast.FunctionDef, comments: Dict[int, Token]):
|
|
self.function_node = node
|
|
self.comments = comments
|
|
|
|
@cached_property
|
|
def docstring(self) -> Optional[DocstringType]:
|
|
if not self.function_node.body:
|
|
return None
|
|
|
|
first_node = self.function_node.body[0]
|
|
if isinstance(first_node, ast.Expr) and isinstance(
|
|
first_node.value, (ast.Constant, ast.Str)
|
|
):
|
|
return first_node.value
|
|
|
|
return None
|
|
|
|
@cached_property
|
|
def docstring_end_lineno(self) -> int:
|
|
docstring = self.docstring
|
|
return (
|
|
docstring.end_lineno
|
|
if hasattr(docstring, "end_lineno")
|
|
else docstring.lineno
|
|
)
|
|
|
|
@cached_property
|
|
def element_after_docstring(self) -> Optional[Union[Token, ast.AST]]:
|
|
"""
|
|
Returns a node (comment or AST node) if it is in the line immediately after
|
|
the docstring, otherwise returns None.
|
|
"""
|
|
|
|
dostring_end_lineno = self.docstring_end_lineno
|
|
comment = self.comments.get(dostring_end_lineno + 1, None)
|
|
if comment is not None:
|
|
return comment
|
|
|
|
function_node = self.function_node
|
|
second_node = function_node.body[1] if len(function_node.body) > 1 else None
|
|
if second_node and second_node.lineno == dostring_end_lineno + 1:
|
|
return second_node
|
|
|
|
return None
|
|
|
|
|
|
def missing_empty_line_after_docstring(
|
|
node: ast.FunctionDef,
|
|
comments: Dict[int, Token],
|
|
) -> List[Tuple[int, int, str]]:
|
|
"""
|
|
Check if there is at least one empty line after the docstring.
|
|
|
|
NOTE: ast in python3.7 see docstrings as ast.Str and has no end_lineno attr,
|
|
while in python3.10 it has end_lineno attr and is a ast.Constant.
|
|
|
|
:param node: The function node to check.
|
|
:return: A list of errors (if any) in the form [(line_no, column_no, error_msg)].
|
|
"""
|
|
|
|
function_helper = FunctionNodeHelper(node, comments)
|
|
if function_helper.docstring is None:
|
|
return []
|
|
|
|
elem = function_helper.element_after_docstring
|
|
if elem is None:
|
|
return []
|
|
|
|
return [(function_helper.docstring_end_lineno, elem.col_offset, ERR_MSG)]
|
|
|
|
|
|
class Visitor(ast.NodeVisitor):
|
|
def __init__(self, tokens) -> None:
|
|
self.tokens = tokens
|
|
self.errors: List[Tuple[int, int, str]] = []
|
|
|
|
def visit_FunctionDef(self, node: ast.FunctionDef) -> None:
|
|
self.errors += missing_empty_line_after_docstring(node, self.tokens)
|
|
self.generic_visit(node)
|
|
|
|
|
|
class Plugin:
|
|
name = "flake8_baserow_docstring"
|
|
version = "0.1.0"
|
|
|
|
_tokens = None
|
|
|
|
def __init__(
|
|
self,
|
|
tree: ast.AST,
|
|
filename: str = None,
|
|
lines: Iterable[str] = None,
|
|
file_tokens: Iterable[TokenInfo] = None,
|
|
):
|
|
self._tree = tree
|
|
self.filename = "stdin" if filename in ("stdin", "-", None) else filename
|
|
if lines:
|
|
if isinstance(lines, str):
|
|
lines = lines.splitlines(True)
|
|
self.lines = tuple(lines)
|
|
self._tokens = file_tokens
|
|
|
|
@cached_property
|
|
def lines(self) -> Tuple[str, ...]:
|
|
if self.filename == "stdin":
|
|
return stdin_utils.stdin_get_value().splitlines(True)
|
|
return pycodestyle.readlines(self.filename)
|
|
|
|
@cached_property
|
|
def tokens(self) -> Dict[int, Token]:
|
|
if self._tokens is not None:
|
|
tokens = self._tokens
|
|
else:
|
|
getter = partial(next, iter(self.lines))
|
|
tokens = generate_tokens(getter) # type: ignore
|
|
comments = []
|
|
for tkn in tokens:
|
|
token = Token(tkn)
|
|
if token.type == COMMENT:
|
|
comments.append(token)
|
|
return {comment.start_row: comment for comment in comments}
|
|
|
|
def run(self) -> Generator[Tuple[int, int, str, Type[Any]], None, None]:
|
|
visitor = Visitor(self.tokens)
|
|
visitor.visit(self._tree)
|
|
|
|
for line, col, msg in visitor.errors:
|
|
yield line, col, msg, type(self)
|