mirror of
https://github.com/davidhalter/django-stubs.git
synced 2025-12-07 04:34:29 +08:00
89 lines
3.5 KiB
Python
89 lines
3.5 KiB
Python
import os
|
|
from typing import Optional
|
|
|
|
import libcst
|
|
from libcst import Annotation, BaseExpression, FunctionDef, Name, Subscript
|
|
from libcst.metadata import SyntacticPositionProvider
|
|
|
|
BASE_DIR = 'django-stubs'
|
|
|
|
fpath = os.path.join(BASE_DIR, 'core', 'checks', 'model_checks.pyi')
|
|
with open(fpath, 'r') as f:
|
|
contents = f.read()
|
|
|
|
|
|
tree = libcst.parse_module(contents)
|
|
|
|
|
|
class TypeAnnotationsAnalyzer(libcst.CSTVisitor):
|
|
METADATA_DEPENDENCIES = (SyntacticPositionProvider,)
|
|
|
|
def __init__(self, fpath: str):
|
|
super().__init__()
|
|
self.fpath = fpath
|
|
|
|
def get_node_location(self, node: FunctionDef) -> str:
|
|
start_line = self.get_metadata(SyntacticPositionProvider, node).start.line
|
|
return f'{self.fpath}:{start_line}'
|
|
|
|
def show_error_for_node(self, node: FunctionDef, error_message: str):
|
|
print(self.get_node_location(node), error_message)
|
|
|
|
def check_subscripted_annotation(self, annotation: BaseExpression) -> Optional[str]:
|
|
if isinstance(annotation, Subscript):
|
|
if isinstance(annotation.value, Name):
|
|
error_message = self.check_concrete_class_usage(annotation.value)
|
|
if error_message:
|
|
return error_message
|
|
|
|
if annotation.value.value == 'Union':
|
|
for slice_param in annotation.slice:
|
|
if isinstance(slice_param.slice.value, Name):
|
|
error_message = self.check_concrete_class_usage(annotation.value)
|
|
if error_message:
|
|
return error_message
|
|
|
|
def check_concrete_class_usage(self, name_node: Name) -> Optional[str]:
|
|
if name_node.value == 'List':
|
|
return (f'Concrete class {name_node.value!r} used for an iterable annotation. '
|
|
f'Use abstract collection (Iterable, Collection, Sequence) instead')
|
|
|
|
def visit_FunctionDef(self, node: FunctionDef) -> Optional[bool]:
|
|
params_node = node.params
|
|
for param_node in [*params_node.params, *params_node.default_params]:
|
|
param_name = param_node.name.value
|
|
annotation_node = param_node.annotation # type: Annotation
|
|
if annotation_node is not None:
|
|
annotation = annotation_node.annotation
|
|
if annotation.value == 'None':
|
|
self.show_error_for_node(node, f'"None" type annotation used for parameter {param_name!r}')
|
|
continue
|
|
|
|
error_message = self.check_subscripted_annotation(annotation)
|
|
if error_message is not None:
|
|
self.show_error_for_node(node, error_message)
|
|
continue
|
|
|
|
if node.returns is not None:
|
|
return_annotation = node.returns.annotation
|
|
if isinstance(return_annotation, Subscript) and return_annotation.value.value == 'Union':
|
|
self.show_error_for_node(node, 'Union is return type annotation')
|
|
|
|
return False
|
|
|
|
|
|
for dirpath, dirnames, filenames in os.walk(BASE_DIR):
|
|
for filename in filenames:
|
|
fpath = os.path.join(dirpath, filename)
|
|
# skip all other checks for now, low priority
|
|
if not fpath.startswith(('django-stubs/db', 'django-stubs/views', 'django-stubs/apps',
|
|
'django-stubs/http', 'django-stubs/contrib/postgres')):
|
|
continue
|
|
|
|
with open(fpath, 'r') as f:
|
|
contents = f.read()
|
|
|
|
tree = libcst.MetadataWrapper(libcst.parse_module(contents))
|
|
analyzer = TypeAnnotationsAnalyzer(fpath)
|
|
tree.visit(analyzer)
|