pre-commit-hooks/pre_commit_hooks/check_builtin_literals.py

106 lines
2.9 KiB
Python
Raw Permalink Normal View History

from __future__ import annotations
import argparse
import ast
from collections.abc import Sequence
2020-02-06 03:10:42 +08:00
from typing import NamedTuple
BUILTIN_TYPES = {
'complex': '0j',
'dict': '{}',
'float': '0.0',
'int': '0',
'list': '[]',
'str': "''",
'tuple': '()',
}
2020-02-06 03:10:42 +08:00
class Call(NamedTuple):
name: str
line: int
column: int
2019-02-12 11:56:15 +08:00
class Visitor(ast.NodeVisitor):
2020-02-06 03:10:42 +08:00
def __init__(
self,
ignore: Sequence[str] | None = None,
2020-02-06 03:10:42 +08:00
allow_dict_kwargs: bool = True,
) -> None:
self.builtin_type_calls: list[Call] = []
self.ignore = set(ignore) if ignore else set()
self.allow_dict_kwargs = allow_dict_kwargs
2020-02-06 03:10:42 +08:00
def _check_dict_call(self, node: ast.Call) -> bool:
return self.allow_dict_kwargs and bool(node.keywords)
2020-02-06 03:10:42 +08:00
def visit_Call(self, node: ast.Call) -> None:
2018-05-18 08:14:25 +08:00
if not isinstance(node.func, ast.Name):
# Ignore functions that are object attributes (`foo.bar()`).
# Assume that if the user calls `builtins.list()`, they know what
# they're doing.
return
if node.func.id not in set(BUILTIN_TYPES).difference(self.ignore):
return
if node.func.id == 'dict' and self._check_dict_call(node):
return
elif node.args:
return
self.builtin_type_calls.append(
2019-02-12 11:56:15 +08:00
Call(node.func.id, node.lineno, node.col_offset),
)
2020-02-06 03:10:42 +08:00
def check_file(
filename: str,
ignore: Sequence[str] | None = None,
2020-02-06 03:10:42 +08:00
allow_dict_kwargs: bool = True,
) -> list[Call]:
2018-06-18 15:00:38 +08:00
with open(filename, 'rb') as f:
tree = ast.parse(f.read(), filename=filename)
2019-02-12 11:56:15 +08:00
visitor = Visitor(ignore=ignore, allow_dict_kwargs=allow_dict_kwargs)
visitor.visit(tree)
return visitor.builtin_type_calls
def parse_ignore(value: str) -> set[str]:
return set(value.split(','))
def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*')
parser.add_argument('--ignore', type=parse_ignore, default=set())
mutex = parser.add_mutually_exclusive_group(required=False)
mutex.add_argument('--allow-dict-kwargs', action='store_true')
2019-02-12 11:56:15 +08:00
mutex.add_argument(
'--no-allow-dict-kwargs',
dest='allow_dict_kwargs', action='store_false',
)
mutex.set_defaults(allow_dict_kwargs=True)
args = parser.parse_args(argv)
rc = 0
for filename in args.filenames:
2019-02-12 11:56:15 +08:00
calls = check_file(
filename,
ignore=args.ignore,
allow_dict_kwargs=args.allow_dict_kwargs,
)
if calls:
rc = rc or 1
for call in calls:
print(
2020-02-06 03:10:42 +08:00
f'{filename}:{call.line}:{call.column}: '
f'replace {call.name}() with {BUILTIN_TYPES[call.name]}',
)
return rc
if __name__ == '__main__':
raise SystemExit(main())