eric7/Plugins/CheckerPlugins/CodeStyleChecker/Annotations/AnnotationsChecker.py

branch
eric7
changeset 8312
800c432b34c8
parent 8258
82b608e352ec
child 8773
3dd81b827455
equal deleted inserted replaced
8311:4e8b98454baa 8312:800c432b34c8
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2019 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a checker for function type annotations.
8 """
9
10 import copy
11 import ast
12 import sys
13 from functools import lru_cache
14
15 import AstUtilities
16
17 from .AnnotationsEnums import AnnotationType, ClassDecoratorType, FunctionType
18 from .AnnotationsCheckerDefaults import AnnotationsCheckerDefaultArgs
19
20
21 class AnnotationsChecker:
22 """
23 Class implementing a checker for function type annotations.
24 """
25 Codes = [
26 ## Function Annotations
27 "A001", "A002", "A003",
28
29 ## Method Annotations
30 "A101", "A102",
31
32 ## Return Annotations
33 "A201", "A202", "A203", "A204", "A205", "A206",
34
35 ## Mixed kind of annotations
36 "A301",
37
38 ## Annotation Coverage
39 "A881",
40
41 ## Annotation Complexity
42 "A891", "A892",
43 ]
44
45 def __init__(self, source, filename, tree, select, ignore, expected,
46 repeat, args):
47 """
48 Constructor
49
50 @param source source code to be checked
51 @type list of str
52 @param filename name of the source file
53 @type str
54 @param tree AST tree of the source code
55 @type ast.Module
56 @param select list of selected codes
57 @type list of str
58 @param ignore list of codes to be ignored
59 @type list of str
60 @param expected list of expected codes
61 @type list of str
62 @param repeat flag indicating to report each occurrence of a code
63 @type bool
64 @param args dictionary of arguments for the annotation checks
65 @type dict
66 """
67 self.__select = tuple(select)
68 self.__ignore = ('',) if select else tuple(ignore)
69 self.__expected = expected[:]
70 self.__repeat = repeat
71 self.__filename = filename
72 self.__source = source[:]
73 self.__tree = copy.deepcopy(tree)
74 self.__args = args
75
76 # statistics counters
77 self.counters = {}
78
79 # collection of detected errors
80 self.errors = []
81
82 checkersWithCodes = [
83 (
84 self.__checkFunctionAnnotations,
85 ("A001", "A002", "A003", "A101", "A102",
86 "A201", "A202", "A203", "A204", "A205", "A206",
87 "A301", )
88 ),
89 (self.__checkAnnotationsCoverage, ("A881",)),
90 (self.__checkAnnotationComplexity, ("A891", "A892")),
91 ]
92
93 self.__checkers = []
94 for checker, codes in checkersWithCodes:
95 if any(not (code and self.__ignoreCode(code))
96 for code in codes):
97 self.__checkers.append(checker)
98
99 def __ignoreCode(self, code):
100 """
101 Private method to check if the message code should be ignored.
102
103 @param code message code to check for
104 @type str
105 @return flag indicating to ignore the given code
106 @rtype bool
107 """
108 return (code.startswith(self.__ignore) and
109 not code.startswith(self.__select))
110
111 def __error(self, lineNumber, offset, code, *args):
112 """
113 Private method to record an issue.
114
115 @param lineNumber line number of the issue
116 @type int
117 @param offset position within line of the issue
118 @type int
119 @param code message code
120 @type str
121 @param args arguments for the message
122 @type list
123 """
124 if self.__ignoreCode(code):
125 return
126
127 if code in self.counters:
128 self.counters[code] += 1
129 else:
130 self.counters[code] = 1
131
132 # Don't care about expected codes
133 if code in self.__expected:
134 return
135
136 if code and (self.counters[code] == 1 or self.__repeat):
137 # record the issue with one based line number
138 self.errors.append(
139 {
140 "file": self.__filename,
141 "line": lineNumber + 1,
142 "offset": offset,
143 "code": code,
144 "args": args,
145 }
146 )
147
148 def run(self):
149 """
150 Public method to check the given source against annotation issues.
151 """
152 if not self.__filename:
153 # don't do anything, if essential data is missing
154 return
155
156 if not self.__checkers:
157 # don't do anything, if no codes were selected
158 return
159
160 for check in self.__checkers:
161 check()
162
163 #######################################################################
164 ## Annotations
165 ##
166 ## adapted from: flake8-annotations v2.6.2
167 #######################################################################
168
169 def __checkFunctionAnnotations(self):
170 """
171 Private method to check for function annotation issues.
172 """
173 suppressNoneReturning = self.__args.get(
174 "SuppressNoneReturning",
175 AnnotationsCheckerDefaultArgs["SuppressNoneReturning"])
176 suppressDummyArgs = self.__args.get(
177 "SuppressDummyArgs",
178 AnnotationsCheckerDefaultArgs["SuppressDummyArgs"])
179 allowUntypedDefs = self.__args.get(
180 "AllowUntypedDefs",
181 AnnotationsCheckerDefaultArgs["AllowUntypedDefs"])
182 allowUntypedNested = self.__args.get(
183 "AllowUntypedNested",
184 AnnotationsCheckerDefaultArgs["AllowUntypedNested"])
185 mypyInitReturn = self.__args.get(
186 "MypyInitReturn",
187 AnnotationsCheckerDefaultArgs["MypyInitReturn"])
188
189 # Store decorator lists as sets for easier lookup
190 dispatchDecorators = set(self.__args.get(
191 "DispatchDecorators",
192 AnnotationsCheckerDefaultArgs["DispatchDecorators"]))
193 overloadDecorators = set(self.__args.get(
194 "OverloadDecorators",
195 AnnotationsCheckerDefaultArgs["OverloadDecorators"]))
196
197 from .AnnotationsFunctionVisitor import FunctionVisitor
198 visitor = FunctionVisitor(self.__source)
199 visitor.visit(self.__tree)
200
201 # Keep track of the last encountered function decorated by
202 # `typing.overload`, if any. Per the `typing` module documentation,
203 # a series of overload-decorated definitions must be followed by
204 # exactly one non-overload-decorated definition of the same function.
205 lastOverloadDecoratedFunctionName = None
206
207 # Iterate over the arguments with missing type hints, by function.
208 for function in visitor.functionDefinitions:
209 if (
210 function.isDynamicallyTyped() and
211 (allowUntypedDefs or
212 (function.isNested and allowUntypedNested))
213 ):
214 # Skip recording errors from dynamically typed functions
215 # or nested functions
216 continue
217
218 # Skip recording errors for configured dispatch functions, such as
219 # (by default) `functools.singledispatch` and
220 # `functools.singledispatchmethod`
221 if function.hasDecorator(dispatchDecorators):
222 continue
223
224 # Create sentinels to check for mixed hint styles
225 hasTypeComment = function.hasTypeComment
226
227 has3107Annotation = False
228 # PEP 3107 annotations are captured by the return arg
229
230 # Iterate over annotated args to detect mixing of type annotations
231 # and type comments. Emit this only once per function definition
232 for arg in function.getAnnotatedArguments():
233 if arg.hasTypeComment:
234 hasTypeComment = True
235
236 if arg.has3107Annotation:
237 has3107Annotation = True
238
239 if hasTypeComment and has3107Annotation:
240 # Short-circuit check for mixing of type comments &
241 # 3107-style annotations
242 self.__error(function.lineno - 1, function.col_offset,
243 "A301")
244 break
245
246 # Before we iterate over the function's missing annotations, check
247 # to see if it's the closing function def in a series of
248 # `typing.overload` decorated functions.
249 if lastOverloadDecoratedFunctionName == function.name:
250 continue
251
252 # If it's not, and it is overload decorated, store it for the next
253 # iteration
254 if function.hasDecorator(overloadDecorators):
255 lastOverloadDecoratedFunctionName = function.name
256
257 # Record explicit errors for arguments that are missing annotations
258 for arg in function.getMissedAnnotations():
259 if arg.argname == "return":
260 # return annotations have multiple possible short-circuit
261 # paths
262 if (
263 suppressNoneReturning and
264 not arg.hasTypeAnnotation and
265 function.hasOnlyNoneReturns
266 ):
267 # Skip recording return errors if the function has only
268 # `None` returns. This includes the case of no returns.
269 continue
270
271 if (
272 mypyInitReturn and
273 function.isClassMethod and
274 function.name == "__init__" and
275 function.getAnnotatedArguments()
276 ):
277 # Skip recording return errors for `__init__` if at
278 # least one argument is annotated
279 continue
280
281 # If the `suppressDummyArgs` flag is `True`, skip recording
282 # errors for any arguments named `_`
283 if arg.argname == "_" and suppressDummyArgs:
284 continue
285
286 self.__classifyError(function, arg)
287
288 def __classifyError(self, function, arg):
289 """
290 Private method to classify the missing type annotation based on the
291 Function & Argument metadata.
292
293 For the currently defined rules & program flow, the assumption can be
294 made that an argument passed to this method will match a linting error,
295 and will only match a single linting error
296
297 This function provides an initial classificaton, then passes relevant
298 attributes to cached helper function(s).
299
300 @param function reference to the Function object
301 @type Function
302 @param arg reference to the Argument object
303 @type Argument
304 """
305 # Check for return type
306 # All return "arguments" have an explicitly defined name "return"
307 if arg.argname == "return":
308 errorCode = self.__returnErrorClassifier(
309 function.isClassMethod, function.classDecoratorType,
310 function.functionType
311 )
312 else:
313 # Otherwise, classify function argument error
314 isFirstArg = arg == function.args[0]
315 errorCode = self.__argumentErrorClassifier(
316 function.isClassMethod, isFirstArg,
317 function.classDecoratorType, arg.annotationType,
318 )
319
320 if errorCode in ("A001", "A002", "A003"):
321 self.__error(arg.lineno - 1, arg.col_offset, errorCode,
322 arg.argname)
323 else:
324 self.__error(arg.lineno - 1, arg.col_offset, errorCode)
325
326 @lru_cache()
327 def __returnErrorClassifier(self, isClassMethod, classDecoratorType,
328 functionType):
329 """
330 Private method to classify a return type annotation issue.
331
332 @param isClassMethod flag indicating a classmethod type function
333 @type bool
334 @param classDecoratorType type of class decorator
335 @type ClassDecoratorType
336 @param functionType type of function
337 @type FunctionType
338 @return error code
339 @rtype str
340 """
341 # Decorated class methods (@classmethod, @staticmethod) have a higher
342 # priority than the rest
343 if isClassMethod:
344 if classDecoratorType == ClassDecoratorType.CLASSMETHOD:
345 return "A206"
346 elif classDecoratorType == ClassDecoratorType.STATICMETHOD:
347 return "A205"
348
349 if functionType == FunctionType.SPECIAL:
350 return "A204"
351 elif functionType == FunctionType.PRIVATE:
352 return "A203"
353 elif functionType == FunctionType.PROTECTED:
354 return "A202"
355 else:
356 return "A201"
357
358 @lru_cache()
359 def __argumentErrorClassifier(self, isClassMethod, isFirstArg,
360 classDecoratorType, annotationType):
361 """
362 Private method to classify an argument type annotation issue.
363
364 @param isClassMethod flag indicating a classmethod type function
365 @type bool
366 @param isFirstArg flag indicating the first argument
367 @type bool
368 @param classDecoratorType type of class decorator
369 @type enums.ClassDecoratorType
370 @param annotationType type of annotation
371 @type AnnotationType
372 @return error code
373 @rtype str
374 """
375 # Check for regular class methods and @classmethod, @staticmethod is
376 # deferred to final check
377 if isClassMethod and isFirstArg:
378 # The first function argument here would be an instance of self or
379 # class
380 if classDecoratorType == ClassDecoratorType.CLASSMETHOD:
381 return "A102"
382 elif classDecoratorType != ClassDecoratorType.STATICMETHOD:
383 # Regular class method
384 return "A101"
385
386 # Check for remaining codes
387 if annotationType == AnnotationType.KWARG:
388 return "A003"
389 elif annotationType == AnnotationType.VARARG:
390 return "A002"
391 else:
392 # Combine PosOnlyArgs, Args, and KwOnlyArgs
393 return "A001"
394
395 #######################################################################
396 ## Annotations Coverage
397 ##
398 ## adapted from: flake8-annotations-coverage v0.0.5
399 #######################################################################
400
401 def __checkAnnotationsCoverage(self):
402 """
403 Private method to check for function annotation coverage.
404 """
405 minAnnotationsCoverage = self.__args.get(
406 "MinimumCoverage",
407 AnnotationsCheckerDefaultArgs["MinimumCoverage"])
408 if minAnnotationsCoverage == 0:
409 # 0 means it is switched off
410 return
411
412 functionDefs = [
413 f for f in ast.walk(self.__tree)
414 if isinstance(f, (ast.AsyncFunctionDef, ast.FunctionDef))
415 ]
416 if not functionDefs:
417 # no functions/methods at all
418 return
419
420 functionDefAnnotationsInfo = [
421 self.__hasTypeAnnotations(f) for f in functionDefs
422 ]
423 annotationsCoverage = int(
424 len(list(filter(None, functionDefAnnotationsInfo))) /
425 len(functionDefAnnotationsInfo) * 100
426 )
427 if annotationsCoverage < minAnnotationsCoverage:
428 self.__error(0, 0, "A881", annotationsCoverage)
429
430 def __hasTypeAnnotations(self, funcNode):
431 """
432 Private method to check for type annotations.
433
434 @param funcNode reference to the function definition node to be checked
435 @type ast.AsyncFunctionDef or ast.FunctionDef
436 @return flag indicating the presence of type annotations
437 @rtype bool
438 """
439 hasReturnAnnotation = funcNode.returns is not None
440 hasArgsAnnotations = any(a for a in funcNode.args.args
441 if a.annotation is not None)
442 hasKwargsAnnotations = (funcNode.args and
443 funcNode.args.kwarg and
444 funcNode.args.kwarg.annotation is not None)
445 hasKwonlyargsAnnotations = any(a for a in funcNode.args.kwonlyargs
446 if a.annotation is not None)
447
448 return any((hasReturnAnnotation, hasArgsAnnotations,
449 hasKwargsAnnotations, hasKwonlyargsAnnotations))
450
451 #######################################################################
452 ## Annotations Complexity
453 ##
454 ## adapted from: flake8-annotations-complexity v0.0.6
455 #######################################################################
456
457 def __checkAnnotationComplexity(self):
458 """
459 Private method to check the type annotation complexity.
460 """
461 maxAnnotationComplexity = self.__args.get(
462 "MaximumComplexity",
463 AnnotationsCheckerDefaultArgs["MaximumComplexity"])
464 maxAnnotationLength = self.__args.get(
465 "MaximumLength", AnnotationsCheckerDefaultArgs["MaximumLength"])
466 typeAnnotations = []
467
468 functionDefs = [
469 f for f in ast.walk(self.__tree)
470 if isinstance(f, (ast.AsyncFunctionDef, ast.FunctionDef))
471 ]
472 for functionDef in functionDefs:
473 typeAnnotations += list(filter(
474 None, [a.annotation for a in functionDef.args.args]))
475 if functionDef.returns:
476 typeAnnotations.append(functionDef.returns)
477 typeAnnotations += [a.annotation for a in ast.walk(self.__tree)
478 if isinstance(a, ast.AnnAssign) and a.annotation]
479 for annotation in typeAnnotations:
480 complexity = self.__getAnnotationComplexity(annotation)
481 if complexity > maxAnnotationComplexity:
482 self.__error(annotation.lineno - 1, annotation.col_offset,
483 "A891", complexity, maxAnnotationComplexity)
484
485 annotationLength = self.__getAnnotationLength(annotation)
486 if annotationLength > maxAnnotationLength:
487 self.__error(annotation.lineno - 1, annotation.col_offset,
488 "A892", annotationLength, maxAnnotationLength)
489
490 def __getAnnotationComplexity(self, annotationNode, defaultComplexity=1):
491 """
492 Private method to determine the annotation complexity.
493
494 @param annotationNode reference to the node to determine the annotation
495 complexity for
496 @type ast.AST
497 @param defaultComplexity default complexity value
498 @type int
499 @return annotation complexity
500 @rtype = int
501 """
502 if AstUtilities.isString(annotationNode):
503 try:
504 annotationNode = ast.parse(annotationNode.s).body[0].value
505 except (SyntaxError, IndexError):
506 return defaultComplexity
507 if isinstance(annotationNode, ast.Subscript):
508 if sys.version_info >= (3, 9):
509 return (defaultComplexity +
510 self.__getAnnotationComplexity(annotationNode.slice))
511 else:
512 return (
513 defaultComplexity +
514 self.__getAnnotationComplexity(annotationNode.slice.value)
515 )
516 if isinstance(annotationNode, ast.Tuple):
517 return max(
518 (self.__getAnnotationComplexity(n)
519 for n in annotationNode.elts),
520 default=defaultComplexity
521 )
522 return defaultComplexity
523
524 def __getAnnotationLength(self, annotationNode):
525 """
526 Private method to determine the annotation length.
527
528 @param annotationNode reference to the node to determine the annotation
529 length for
530 @type ast.AST
531 @return annotation length
532 @rtype = int
533 """
534 if AstUtilities.isString(annotationNode):
535 try:
536 annotationNode = ast.parse(annotationNode.s).body[0].value
537 except (SyntaxError, IndexError):
538 return 0
539 if isinstance(annotationNode, ast.Subscript):
540 try:
541 if sys.version_info >= (3, 9):
542 return len(annotationNode.slice.elts)
543 else:
544 return len(annotationNode.slice.value.elts)
545 except AttributeError:
546 return 0
547 return 0

eric ide

mercurial