src/eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsFunctionVisitor.py

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

eric ide

mercurial