eric6/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsFunctionVisitor.py

changeset 8244
ed8cb108b27b
equal deleted inserted replaced
8243:cc717c2ae956 8244:ed8cb108b27b
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a node visitor for function type annotations.
8 """
9
10 #
11 # The visitor and associated classes are adapted from flake8-annotations v2.6.2
12 #
13
14 import ast
15 import itertools
16
17 from .AnnotationsEnums import AnnotationType, ClassDecoratorType, FunctionType
18
19 # The order of AST_ARG_TYPES must match Python's grammar
20 AST_ARG_TYPES = ("posonlyargs", "args", "vararg", "kwonlyargs", "kwarg")
21
22
23 class Argument:
24 """
25 Class representing a function argument.
26 """
27 def __init__(self, argname, lineno, col_offset, annotationType,
28 hasTypeAnnotation=False, has3107Annotation=False,
29 hasTypeComment=False):
30 """
31 Constructor
32
33 @param argname name of the argument
34 @type str
35 @param lineno line number
36 @type int
37 @param col_offset column number
38 @type int
39 @param annotationType type of annotation
40 @type AnnotationType
41 @param hasTypeAnnotation flag indicating the presence of a type
42 annotation (defaults to False)
43 @type bool (optional)
44 @param has3107Annotation flag indicating the presence of a PEP 3107
45 annotation (defaults to False)
46 @type bool (optional)
47 @param hasTypeComment flag indicating the presence of a type comment
48 (defaults to False)
49 @type bool (optional)
50 """
51 self.argname = argname
52 self.lineno = lineno
53 self.col_offset = col_offset
54 self.annotationType = annotationType
55 self.hasTypeAnnotation = hasTypeAnnotation
56 self.has3107Annotation = has3107Annotation
57 self.hasTypeComment = hasTypeComment
58
59 @classmethod
60 def fromNode(cls, node, annotationTypeName):
61 """
62 Class method to create an Argument object based on the given node.
63
64 @param node reference to the node to be converted
65 @type ast.arguments
66 @param annotationTypeName name of the annotation type
67 @type str
68 @return Argument object
69 @rtype Argument
70 """
71 annotationType = AnnotationType[annotationTypeName]
72 newArg = cls(node.arg, node.lineno, node.col_offset, annotationType)
73
74 newArg.hasTypeAnnotation = False
75 if node.annotation:
76 newArg.hasTypeAnnotation = True
77 newArg.has3107Annotation = True
78
79 if node.type_comment:
80 newArg.hasTypeAnnotation = True
81 newArg.hasTypeComment = True
82
83 return newArg
84
85
86 class Function:
87 """
88 Class representing a function.
89 """
90 def __init__(self, name, lineno, col_offset,
91 functionType=FunctionType.PUBLIC, isClassMethod=False,
92 classDecoratorType=None, isReturnAnnotated=False,
93 hasTypeComment=False, hasOnlyNoneReturns=True,
94 isNested=False, decoratorList=None, args=None):
95 """
96 Constructor
97
98 @param name name of the function
99 @type str
100 @param lineno line number
101 @type int
102 @param col_offset column number
103 @type int
104 @param functionType type of the function (defaults to
105 FunctionType.PUBLIC)
106 @type FunctionType (optional)
107 @param isClassMethod flag indicating a class method (defaults to False)
108 @type bool (optional)
109 @param classDecoratorType type of a function decorator
110 (defaults to None)
111 @type ClassDecoratorType or None (optional)
112 @param isReturnAnnotated flag indicating the presence of a return
113 type annotation (defaults to False)
114 @type bool (optional)
115 @param hasTypeComment flag indicating the presence of a type comment
116 (defaults to False)
117 @type bool (optional)
118 @param hasOnlyNoneReturns flag indicating only None return values
119 (defaults to True)
120 @type bool (optional)
121 @param isNested flag indicating a nested function (defaults to False)
122 @type bool (optional)
123 @param decoratorList list of decorator nodes (defaults to None)
124 @type list of ast.Attribute, ast.Call or ast.Name (optional)
125 @param args list of arguments (defaults to None)
126 @type list of Argument (optional)
127 """
128 self.name = name
129 self.lineno = lineno
130 self.col_offset = col_offset
131 self.functionType = functionType
132 self.isClassMethod = isClassMethod
133 self.classDecoratorType = classDecoratorType
134 self.isReturnAnnotated = isReturnAnnotated
135 self.hasTypeComment = hasTypeComment
136 self.hasOnlyNoneReturns = hasOnlyNoneReturns
137 self.isNested = isNested
138 self.decoratorList = decoratorList
139 self.args = args
140
141 def isFullyAnnotated(self):
142 """
143 Public method to check, if the function definition is fully type
144 annotated.
145
146 Note: self.args will always include an Argument object for return.
147
148 @return flag indicating a fully annotated function definition
149 @rtype bool
150 """
151 return all(arg.hasTypeAnnotation for arg in self.args)
152
153 def isDynamicallyTyped(self):
154 """
155 Public method to check, if a function definition is dynamically typed
156 (i.e. completely lacking hints).
157
158 @return flag indicating a dynamically typed function definition
159 @rtype bool
160 """
161 return not any(arg.hasTypeAnnotation for arg in self.args)
162
163 def getMissedAnnotations(self):
164 """
165 Public method to provide a list of arguments with missing type
166 annotations.
167
168 @return list of arguments with missing type annotations
169 @rtype list of Argument
170 """
171 return [arg for arg in self.args if not arg.hasTypeAnnotation]
172
173 def getAnnotatedArguments(self):
174 """
175 Public method to get list of arguments with type annotations.
176
177 @return list of arguments with type annotations.
178 @rtype list of Argument
179 """
180 return [arg for arg in self.args if arg.hasTypeAnnotation]
181
182 def hasDecorator(self, checkDecorators):
183 """
184 Public method to check whether the function node is decorated by any of
185 the provided decorators.
186
187 Decorator matching is done against the provided `checkDecorators` set.
188 Decorators are assumed to be either a module attribute (e.g.
189 `@typing.overload`) or name (e.g. `@overload`). For the case of a
190 module attribute, only the attribute is checked against
191 `overload_decorators`.
192
193 Note: Deeper decorator imports (e.g. `a.b.overload`) are not explicitly
194 supported.
195
196 @param checkDecorators set of decorators to check against
197 @type set of str
198 @return flag indicating the presence of any decorators
199 @rtype bool
200 """
201 for decorator in self.decoratorList:
202 # Drop to a helper to allow for simpler handling of callable
203 # decorators
204 return self.__decoratorChecker(decorator, checkDecorators)
205 else:
206 return False
207
208 def __decoratorChecker(self, decorator, checkDecorators):
209 """
210 Private method to check the provided decorator for a match against the
211 provided set of check names.
212
213 Decorators are assumed to be of the following form:
214 * `a.name` or `a.name()`
215 * `name` or `name()`
216
217 Note: Deeper imports (e.g. `a.b.name`) are not explicitly supported.
218
219 @param decorator decorator node to check
220 @type ast.Attribute, ast.Call or ast.Name
221 @param checkDecorators set of decorators to check against
222 @type set of str
223 @return flag indicating the presence of any decorators
224 @rtype bool
225 """
226 if isinstance(decorator, ast.Name):
227 # e.g. `@overload`, where `decorator.id` will be the name
228 if decorator.id in checkDecorators:
229 return True
230 elif isinstance(decorator, ast.Attribute):
231 # e.g. `@typing.overload`, where `decorator.attr` will be the name
232 if decorator.attr in checkDecorators:
233 return True
234 elif isinstance(decorator, ast.Call):
235 # e.g. `@overload()` or `@typing.overload()`, where
236 # `decorator.func` will be `ast.Name` or `ast.Attribute`,
237 # which we can check recursively
238 return self.__decoratorChecker(decorator.func, checkDecorators)
239
240 return None
241
242 @classmethod
243 def fromNode(cls, node, lines, **kwargs):
244 """
245 Class method to create a Function object from ast.FunctionDef or
246 ast.AsyncFunctionDef nodes.
247
248 Accept the source code, as a list of strings, in order to get the
249 column where the function definition ends.
250
251 With exceptions, input kwargs are passed straight through to Function's
252 __init__. The following kwargs will be overridden:
253 * function_type
254 * class_decorator_type
255 * args
256
257 @param node reference to the function definition node
258 @type ast.AsyncFunctionDef or ast.FunctionDef
259 @param lines list of source code lines
260 @type list of str
261 @keyparam **kwargs keyword arguments
262 @type dict
263 @return created Function object
264 @rtype Function
265 """
266 # Extract function types from function name
267 kwargs["functionType"] = cls.getFunctionType(node.name)
268
269 # Identify type of class method, if applicable
270 if kwargs.get("isClassMethod", False):
271 kwargs["classDecoratorType"] = cls.getClassDecoratorType(node)
272
273 # Store raw decorator list for use by property methods
274 kwargs["decoratorList"] = node.decorator_list
275
276 newFunction = cls(node.name, node.lineno, node.col_offset, **kwargs)
277
278 # Iterate over arguments by type & add
279 newFunction.args = []
280 for argType in AST_ARG_TYPES:
281 args = node.args.__getattribute__(argType)
282 if args:
283 if not isinstance(args, list):
284 args = [args]
285
286 newFunction.args.extend(
287 [Argument.fromNode(arg, argType.upper())
288 for arg in args]
289 )
290
291 # Create an Argument object for the return hint
292 defEndLineno, defEndColOffset = cls.colonSeeker(node, lines)
293 returnArg = Argument("return", defEndLineno, defEndColOffset,
294 AnnotationType.RETURN)
295 if node.returns:
296 returnArg.hasTypeAnnotation = True
297 returnArg.has3107Annotation = True
298 newFunction.isReturnAnnotated = True
299
300 newFunction.args.append(returnArg)
301
302 # Type comments in-line with input arguments are handled by the
303 # Argument class. If a function-level type comment is present, attempt
304 # to parse for any missed type hints.
305 if node.type_comment:
306 newFunction.hasTypeComment = True
307 newFunction = cls.tryTypeComment(newFunction, node)
308
309 # Check for the presence of non-`None` returns using the special-case
310 # return node visitor.
311 returnVisitor = ReturnVisitor(node)
312 returnVisitor.visit(node)
313 newFunction.hasOnlyNoneReturns = returnVisitor.hasOnlyNoneReturns
314
315 return newFunction
316
317 @staticmethod
318 def colonSeeker(node, lines):
319 """
320 Static method to find the line & column indices of the function
321 definition's closing colon.
322
323 @param node reference to the function definition node
324 @type ast.AsyncFunctionDef or ast.FunctionDef
325 @param lines list of source code lines
326 @type list of str
327 @return line and column offset of the colon
328 @rtype tuple of (int, int)
329 """
330 # Special case single line function definitions
331 if node.lineno == node.body[0].lineno:
332 return Function._singleLineColonSeeker(
333 node, lines[node.lineno - 1])
334
335 defEndLineno = node.body[0].lineno - 1
336
337 # Use str.rfind() to account for annotations on the same line,
338 # definition closure should be the last : on the line
339 defEndColOffset = lines[defEndLineno - 1].rfind(":")
340
341 return defEndLineno, defEndColOffset
342
343 @staticmethod
344 def _singleLineColonSeeker(node, line):
345 """
346 Static method to find the line & column indices of a single line
347 function definition.
348
349 @param node reference to the function definition node
350 @type ast.AsyncFunctionDef or ast.FunctionDef
351 @param line source code line
352 @type str
353 @return line and column offset of the colon
354 @rtype tuple of (int, int)
355 """
356 colStart = node.col_offset
357 colEnd = node.body[0].col_offset
358 defEndColOffset = line.rfind(":", colStart, colEnd)
359
360 return node.lineno, defEndColOffset
361
362 @staticmethod
363 def tryTypeComment(funcObj, node):
364 """
365 Static method to infer type hints from a function-level type comment.
366
367 If a function is type commented it is assumed to have a return
368 annotation, otherwise Python will fail to parse the hint.
369
370 @param funcObj reference to the Function object
371 @type Function
372 @param node reference to the function definition node
373 @type ast.AsyncFunctionDef or ast.FunctionDef
374 @return reference to the modified Function object
375 @rtype Function
376 """
377 hintTree = ast.parse(node.type_comment, "<func_type>", "func_type")
378 hintTree = Function._maybeInjectClassArgument(hintTree, funcObj)
379
380 for arg, hintComment in itertools.zip_longest(
381 funcObj.args, hintTree.argtypes
382 ):
383 if isinstance(hintComment, ast.Ellipsis):
384 continue
385
386 if arg and hintComment:
387 arg.hasTypeAnnotation = True
388 arg.hasTypeComment = True
389
390 # Return arg is always last
391 funcObj.args[-1].hasTypeAnnotation = True
392 funcObj.args[-1].hasTypeComment = True
393 funcObj.isReturnAnnotated = True
394
395 return funcObj
396
397 @staticmethod
398 def _maybeInjectClassArgument(hintTree, funcObj):
399 """
400 Static method to inject `self` or `cls` args into a type comment to
401 align with PEP 3107-style annotations.
402
403 Because PEP 484 does not describe a method to provide partial function-
404 level type comments, there is a potential for ambiguity in the context
405 of both class methods and classmethods when aligning type comments to
406 method arguments.
407
408 These two class methods, for example, should lint equivalently:
409
410 def bar(self, a):
411 # type: (int) -> int
412 ...
413
414 def bar(self, a: int) -> int
415 ...
416
417 When this example type comment is parsed by `ast` and then matched with
418 the method's arguments, it associates the `int` hint to `self` rather
419 than `a`, so a dummy hint needs to be provided in situations where
420 `self` or `class` are not hinted in the type comment in order to
421 achieve equivalent linting results to PEP-3107 style annotations.
422
423 A dummy `ast.Ellipses` constant is injected if the following criteria
424 are met:
425 1. The function node is either a class method or classmethod
426 2. The number of hinted args is at least 1 less than the number
427 of function args
428
429 @param hintTree parsed type hint node
430 @type ast.FunctionType
431 @param funcObj reference to the Function object
432 @type Function
433 @return reference to the hint node
434 @rtype ast.FunctionType
435 """
436 if not funcObj.isClassMethod:
437 # Short circuit
438 return hintTree
439
440 if (
441 funcObj.classDecoratorType != ClassDecoratorType.STATICMETHOD and
442 len(hintTree.argtypes) < (len(funcObj.args) - 1)
443 ):
444 # Subtract 1 to skip return arg
445 hintTree.argtypes = [ast.Ellipsis()] + hintTree.argtypes
446
447 return hintTree
448
449 @staticmethod
450 def getFunctionType(functionName):
451 """
452 Static method to determine the function's FunctionType from its name.
453
454 MethodType is determined by the following priority:
455 1. Special: function name prefixed & suffixed by "__"
456 2. Private: function name prefixed by "__"
457 3. Protected: function name prefixed by "_"
458 4. Public: everything else
459
460 @param functionName function name to be checked
461 @type str
462 @return type of function
463 @rtype FunctionType
464 """
465 if functionName.startswith("__") and functionName.endswith("__"):
466 return FunctionType.SPECIAL
467 elif functionName.startswith("__"):
468 return FunctionType.PRIVATE
469 elif functionName.startswith("_"):
470 return FunctionType.PROTECTED
471 else:
472 return FunctionType.PUBLIC
473
474 @staticmethod
475 def getClassDecoratorType(functionNode):
476 """
477 Static method to get the class method's decorator type from its
478 function node.
479
480 Only @classmethod and @staticmethod decorators are identified; all
481 other decorators are ignored
482
483 If @classmethod or @staticmethod decorators are not present, this
484 function will return None.
485
486 @param functionNode reference to the function definition node
487 @type ast.AsyncFunctionDef or ast.FunctionDef
488 @return class decorator type
489 @rtype ClassDecoratorType or None
490 """
491 # @classmethod and @staticmethod will show up as ast.Name objects,
492 # where callable decorators will show up as ast.Call, which we can
493 # ignore
494 decorators = [
495 decorator.id
496 for decorator in functionNode.decorator_list
497 if isinstance(decorator, ast.Name)
498 ]
499
500 if "classmethod" in decorators:
501 return ClassDecoratorType.CLASSMETHOD
502 elif "staticmethod" in decorators:
503 return ClassDecoratorType.STATICMETHOD
504 else:
505 return None
506
507
508 class FunctionVisitor(ast.NodeVisitor):
509 """
510 Class implementing a node visitor to check function annotations.
511 """
512 def __init__(self, lines):
513 """
514 Constructor
515
516 @param lines source code lines of the function
517 @type list of str
518 """
519 self.lines = lines
520 self.functionDefinitions = []
521 self.__context = []
522
523 def switchContext(self, node):
524 """
525 Public method implementing a context switcher as a generic function
526 visitor in order to track function context.
527
528 Without keeping track of context, it's challenging to reliably
529 differentiate class methods from "regular" functions, especially in the
530 case of nested classes.
531
532 @param node reference to the function definition node to be analyzed
533 @type ast.AsyncFunctionDef or ast.FunctionDef
534 """
535 if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
536 # Check for non-empty context first to prevent IndexErrors for
537 # non-nested nodes
538 if self.__context:
539 if isinstance(self.__context[-1], ast.ClassDef):
540 # Check if current context is a ClassDef node & pass the
541 # appropriate flag
542 self.functionDefinitions.append(
543 Function.fromNode(node, self.lines, isClassMethod=True)
544 )
545 elif isinstance(
546 self.__context[-1],
547 (ast.FunctionDef, ast.AsyncFunctionDef)
548 ):
549 # Check for nested function & pass the appropriate flag
550 self.functionDefinitions.append(
551 Function.fromNode(node, self.lines, isNested=True)
552 )
553 else:
554 self.functionDefinitions.append(
555 Function.fromNode(node, self.lines))
556
557 self.__context.append(node)
558 self.generic_visit(node)
559 self.__context.pop()
560
561 visit_FunctionDef = switchContext
562 visit_AsyncFunctionDef = switchContext
563 visit_ClassDef = switchContext
564
565
566 class ReturnVisitor(ast.NodeVisitor):
567 """
568 Class implementing a node visitor to check the return statements of a
569 function node.
570
571 If the function node being visited has an explicit return statement of
572 anything other than `None`, the `instance.hasOnlyNoneReturns` flag will
573 be set to `False`.
574
575 If the function node being visited has no return statement, or contains
576 only return statement(s) that explicitly return `None`, the
577 `instance.hasOnlyNoneReturns` flag will be set to `True`.
578
579 Due to the generic visiting being done, we need to keep track of the
580 context in which a non-`None` return node is found. These functions are
581 added to a set that is checked to see whether nor not the parent node is
582 present.
583 """
584 def __init__(self, parentNode):
585 """
586 Constructor
587
588 @param parentNode reference to the function definition node to be
589 analyzed
590 @type ast.AsyncFunctionDef or ast.FunctionDef
591 """
592 self.parentNode = parentNode
593 self.__context = []
594 self.__nonNoneReturnNodes = set()
595
596 @property
597 def hasOnlyNoneReturns(self):
598 """
599 Public method indicating, that the parent node isn't in the visited
600 nodes that don't return `None`.
601
602 @return flag indicating, that the parent node isn't in the visited
603 nodes that don't return `None`
604 @rtype bool
605 """
606 return self.parentNode not in self.__nonNoneReturnNodes
607
608 def visit_Return(self, node):
609 """
610 Public method to check each Return node to see if it returns anything
611 other than `None`.
612
613 If the node being visited returns anything other than `None`, its
614 parent context is added to the set of non-returning child nodes of
615 the parent node.
616
617 @param node reference to the AST Return node
618 @type ast.Return
619 """
620 if node.value is not None:
621 # In the event of an explicit `None` return (`return None`), the
622 # node body will be an instance of either `ast.Constant` (3.8+) or
623 # `ast.NameConstant`, which we need to check to see if it's
624 # actually `None`
625 if (
626 isinstance(node.value, (ast.Constant, ast.NameConstant)) and
627 node.value.value is None
628 ):
629 return
630
631 self.__nonNoneReturnNodes.add(self.__context[-1])
632
633 def switchContext(self, node):
634 """
635 Public method implementing a context switcher as a generic function
636 visitor in order to track function context.
637
638 Without keeping track of context, it's challenging to reliably
639 differentiate class methods from "regular" functions, especially in the
640 case of nested classes.
641
642 @param node reference to the function definition node to be analyzed
643 @type ast.AsyncFunctionDef or ast.FunctionDef
644 """
645 self.__context.append(node)
646 self.generic_visit(node)
647 self.__context.pop()
648
649 visit_FunctionDef = switchContext
650 visit_AsyncFunctionDef = switchContext

eric ide

mercurial