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

changeset 8244
ed8cb108b27b
parent 8236
695777f04b25
child 8246
e4b95b0ebd31
equal deleted inserted replaced
8243:cc717c2ae956 8244:ed8cb108b27b
7 Module implementing a checker for function type annotations. 7 Module implementing a checker for function type annotations.
8 """ 8 """
9 9
10 import copy 10 import copy
11 import ast 11 import ast
12 import sys
13 from functools import lru_cache
12 14
13 import AstUtilities 15 import AstUtilities
16
17 from .AnnotationsEnums import AnnotationType, ClassDecoratorType, FunctionType
14 18
15 19
16 class AnnotationsChecker: 20 class AnnotationsChecker:
17 """ 21 """
18 Class implementing a checker for function type annotations. 22 Class implementing a checker for function type annotations.
25 "A101", "A102", 29 "A101", "A102",
26 30
27 ## Return Annotations 31 ## Return Annotations
28 "A201", "A202", "A203", "A204", "A205", "A206", 32 "A201", "A202", "A203", "A204", "A205", "A206",
29 33
34 ## Mixed kind of annotations
35 "A301",
36
30 ## Annotation Coverage 37 ## Annotation Coverage
31 "A881", 38 "A881",
32 39
33 ## Annotation Complexity 40 ## Annotation Complexity
34 "A891", 41 "A891", "A892",
35 ] 42 ]
36 43
37 def __init__(self, source, filename, tree, select, ignore, expected, 44 def __init__(self, source, filename, tree, select, ignore, expected,
38 repeat, args): 45 repeat, args):
39 """ 46 """
73 80
74 checkersWithCodes = [ 81 checkersWithCodes = [
75 ( 82 (
76 self.__checkFunctionAnnotations, 83 self.__checkFunctionAnnotations,
77 ("A001", "A002", "A003", "A101", "A102", 84 ("A001", "A002", "A003", "A101", "A102",
78 "A201", "A202", "A203", "A204", "A205", "A206",) 85 "A201", "A202", "A203", "A204", "A205", "A206",
86 "A301", )
79 ), 87 ),
80 (self.__checkAnnotationsCoverage, ("A881",)), 88 (self.__checkAnnotationsCoverage, ("A881",)),
81 (self.__checkAnnotationComplexity, ("A891",)), 89 (self.__checkAnnotationComplexity, ("A891", "A892")),
82 ] 90 ]
83 91
92 # TODO: the parameters to CodeStyleCheckerDialog
84 self.__defaultArgs = { 93 self.__defaultArgs = {
94 # Annotations
95 "SuppressNoneReturning": False,
96 "SuppressDummyArgs": False,
97 "AllowUntypedDefs": False,
98 "AllowUntypedNested": False,
99 "MypyInitReturn": False,
100 "DispatchDecorators": [
101 "singledispatch",
102 "singledispatchmethod",
103 ],
104 "OverloadDecorators": ["overload"],
105
106 # Annotation Coverage
85 "MinimumCoverage": 75, # % of type annotation coverage 107 "MinimumCoverage": 75, # % of type annotation coverage
108
109 # Annotation Complexity
86 "MaximumComplexity": 3, 110 "MaximumComplexity": 3,
111 "MaximumLength": 7,
87 } 112 }
88 113
89 self.__checkers = [] 114 self.__checkers = []
90 for checker, codes in checkersWithCodes: 115 for checker, codes in checkersWithCodes:
91 if any(not (code and self.__ignoreCode(code)) 116 if any(not (code and self.__ignoreCode(code))
154 return 179 return
155 180
156 for check in self.__checkers: 181 for check in self.__checkers:
157 check() 182 check()
158 183
184 #######################################################################
185 ## Annotations
186 ##
187 ## adapted from: flake8-annotations v2.6.2
188 #######################################################################
189
159 def __checkFunctionAnnotations(self): 190 def __checkFunctionAnnotations(self):
160 """ 191 """
161 Private method to check for function annotation issues. 192 Private method to check for function annotation issues.
162 """ 193 """
194 suppressNoneReturning = self.__args.get(
195 "SuppressNoneReturning",
196 self.__defaultArgs["SuppressNoneReturning"])
197 suppressDummyArgs = self.__args.get(
198 "SuppressDummyArgs",
199 self.__defaultArgs["SuppressDummyArgs"])
200 allowUntypedDefs = self.__args.get(
201 "AllowUntypedDefs",
202 self.__defaultArgs["AllowUntypedDefs"])
203 allowUntypedNested = self.__args.get(
204 "AllowUntypedNested",
205 self.__defaultArgs["AllowUntypedNested"])
206 mypyInitReturn = self.__args.get(
207 "MypyInitReturn",
208 self.__defaultArgs["MypyInitReturn"])
209
210 # Store decorator lists as sets for easier lookup
211 dispatchDecorators = set(self.__args.get(
212 "DispatchDecorators",
213 self.__defaultArgs["DispatchDecorators"]))
214 overloadDecorators = set(self.__args.get(
215 "OverloadDecorators",
216 self.__defaultArgs["OverloadDecorators"]))
217
218 from .AnnotationsFunctionVisitor import FunctionVisitor
163 visitor = FunctionVisitor(self.__source) 219 visitor = FunctionVisitor(self.__source)
164 visitor.visit(self.__tree) 220 visitor.visit(self.__tree)
165 for issue in visitor.issues: 221
166 node = issue[0] 222 # Keep track of the last encountered function decorated by
167 reason = issue[1] 223 # `typing.overload`, if any. Per the `typing` module documentation,
168 params = issue[2:] 224 # a series of overload-decorated definitions must be followed by
169 self.__error(node.lineno - 1, node.col_offset, reason, *params) 225 # exactly one non-overload-decorated definition of the same function.
226 lastOverloadDecoratedFunctionName = None
227
228 # Iterate over the arguments with missing type hints, by function.
229 for function in visitor.functionDefinitions:
230 if (
231 function.isDynamicallyTyped() and
232 (allowUntypedDefs or
233 (function.isNested and allowUntypedNested))
234 ):
235 # Skip yielding errors from dynamically typed functions
236 # or nested functions
237 continue
238
239 # Skip yielding errors for configured dispatch functions, such as
240 # (by default) `functools.singledispatch` and
241 # `functools.singledispatchmethod`
242 if function.hasDecorator(dispatchDecorators):
243 continue
244
245 # Create sentinels to check for mixed hint styles
246 hasTypeComment = function.hasTypeComment
247
248 has3107Annotation = False
249 # PEP 3107 annotations are captured by the return arg
250
251 # Iterate over annotated args to detect mixing of type annotations
252 # and type comments. Emit this only once per function definition
253 for arg in function.getAnnotatedArguments():
254 if arg.hasTypeComment:
255 hasTypeComment = True
256
257 if arg.has3107Annotation:
258 has3107Annotation = True
259
260 if hasTypeComment and has3107Annotation:
261 # Short-circuit check for mixing of type comments &
262 # 3107-style annotations
263 self.__error(function.lineno - 1, function.col_offset,
264 "A301")
265 break
266
267 # Before we iterate over the function's missing annotations, check
268 # to see if it's the closing function def in a series of
269 # `typing.overload` decorated functions.
270 if lastOverloadDecoratedFunctionName == function.name:
271 continue
272
273 # If it's not, and it is overload decorated, store it for the next
274 # iteration
275 if function.hasDecorator(overloadDecorators):
276 lastOverloadDecoratedFunctionName = function.name
277
278 # Record explicit errors for arguments that are missing annotations
279 for arg in function.getMissedAnnotations():
280 if arg.argname == "return":
281 # return annotations have multiple possible short-circuit
282 # paths
283 if (
284 suppressNoneReturning and
285 not arg.hasTypeAnnotation and
286 function.hasOnlyNoneReturns
287 ):
288 # Skip recording return errors if the function has only
289 # `None` returns. This includes the case of no returns.
290 continue
291
292 if (
293 mypyInitReturn and
294 function.isClassMethod and
295 function.name == "__init__" and
296 function.getAnnotatedArguments()
297 ):
298 # Skip recording return errors for `__init__` if at
299 # least one argument is annotated
300 continue
301
302 # If the `suppressDummyArgs` flag is `True`, skip recording
303 # errors for any arguments named `_`
304 if arg.argname == "_" and suppressDummyArgs:
305 continue
306
307 self.__classifyError(function, arg)
308
309 def __classifyError(self, function, arg):
310 """
311 Private method to classify the missing type annotation based on the
312 Function & Argument metadata.
313
314 For the currently defined rules & program flow, the assumption can be
315 made that an argument passed to this method will match a linting error,
316 and will only match a single linting error
317
318 This function provides an initial classificaton, then passes relevant
319 attributes to cached helper function(s).
320
321 @param function reference to the Function object
322 @type Function
323 @param arg reference to the Argument object
324 @type Argument
325 """
326 # Check for return type
327 # All return "arguments" have an explicitly defined name "return"
328 if arg.argname == "return":
329 errorCode = self.__returnErrorClassifier(
330 function.isClassMethod, function.classDecoratorType,
331 function.functionType
332 )
333 else:
334 # Otherwise, classify function argument error
335 isFirstArg = arg == function.args[0]
336 errorCode = self.__argumentErrorClassifier(
337 function.isClassMethod, isFirstArg,
338 function.classDecoratorType, arg.annotationType,
339 )
340
341 if errorCode in ("A001", "A002", "A003"):
342 self.__error(arg.lineno - 1, arg.col_offset, errorCode,
343 arg.argname)
344 else:
345 self.__error(arg.lineno - 1, arg.col_offset, errorCode)
346
347 @lru_cache()
348 def __returnErrorClassifier(self, isClassMethod, classDecoratorType,
349 functionType):
350 """
351 Private method to classify a return type annotation issue.
352
353 @param isClassMethod flag indicating a classmethod type function
354 @type bool
355 @param classDecoratorType type of class decorator
356 @type ClassDecoratorType
357 @param functionType type of function
358 @type FunctionType
359 @return error code
360 @rtype str
361 """
362 # Decorated class methods (@classmethod, @staticmethod) have a higher
363 # priority than the rest
364 if isClassMethod:
365 if classDecoratorType == ClassDecoratorType.CLASSMETHOD:
366 return "A206"
367 elif classDecoratorType == ClassDecoratorType.STATICMETHOD:
368 return "A205"
369
370 if functionType == FunctionType.SPECIAL:
371 return "A204"
372 elif functionType == FunctionType.PRIVATE:
373 return "A203"
374 elif functionType == FunctionType.PROTECTED:
375 return "A202"
376 else:
377 return "A201"
378
379 @lru_cache()
380 def __argumentErrorClassifier(self, isClassMethod, isFirstArg,
381 classDecoratorType, annotationType):
382 """
383 Private method to classify an argument type annotation issue.
384
385 @param isClassMethod flag indicating a classmethod type function
386 @type bool
387 @param isFirstArg flag indicating the first argument
388 @type bool
389 @param classDecoratorType type of class decorator
390 @type enums.ClassDecoratorType
391 @param annotationType type of annotation
392 @type AnnotationType
393 @return error code
394 @rtype str
395 """
396 # Check for regular class methods and @classmethod, @staticmethod is
397 # deferred to final check
398 if isClassMethod and isFirstArg:
399 # The first function argument here would be an instance of self or
400 # class
401 if classDecoratorType == ClassDecoratorType.CLASSMETHOD:
402 return "A102"
403 elif classDecoratorType != ClassDecoratorType.STATICMETHOD:
404 # Regular class method
405 return "A101"
406
407 # Check for remaining codes
408 if annotationType == AnnotationType.KWARG:
409 return "A003"
410 elif annotationType == AnnotationType.VARARG:
411 return "A002"
412 else:
413 # Combine PosOnlyArgs, Args, and KwOnlyArgs
414 return "A001"
415
416 #######################################################################
417 ## Annotations Coverage
418 ##
419 ## adapted from: flake8-annotations-coverage v0.0.5
420 #######################################################################
170 421
171 def __checkAnnotationsCoverage(self): 422 def __checkAnnotationsCoverage(self):
172 """ 423 """
173 Private method to check for function annotation coverage. 424 Private method to check for function annotation coverage.
174 """ 425 """
185 if not functionDefs: 436 if not functionDefs:
186 # no functions/methods at all 437 # no functions/methods at all
187 return 438 return
188 439
189 functionDefAnnotationsInfo = [ 440 functionDefAnnotationsInfo = [
190 hasTypeAnnotations(f) for f in functionDefs 441 self.__hasTypeAnnotations(f) for f in functionDefs
191 ] 442 ]
192 annotationsCoverage = int( 443 annotationsCoverage = int(
193 len(list(filter(None, functionDefAnnotationsInfo))) / 444 len(list(filter(None, functionDefAnnotationsInfo))) /
194 len(functionDefAnnotationsInfo) * 100 445 len(functionDefAnnotationsInfo) * 100
195 ) 446 )
196 if annotationsCoverage < minAnnotationsCoverage: 447 if annotationsCoverage < minAnnotationsCoverage:
197 self.__error(0, 0, "A881", annotationsCoverage) 448 self.__error(0, 0, "A881", annotationsCoverage)
198 449
450 def __hasTypeAnnotations(self, funcNode):
451 """
452 Private method to check for type annotations.
453
454 @param funcNode reference to the function definition node to be checked
455 @type ast.AsyncFunctionDef or ast.FunctionDef
456 @return flag indicating the presence of type annotations
457 @rtype bool
458 """
459 hasReturnAnnotation = funcNode.returns is not None
460 hasArgsAnnotations = any(a for a in funcNode.args.args
461 if a.annotation is not None)
462 hasKwargsAnnotations = (funcNode.args and
463 funcNode.args.kwarg and
464 funcNode.args.kwarg.annotation is not None)
465 hasKwonlyargsAnnotations = any(a for a in funcNode.args.kwonlyargs
466 if a.annotation is not None)
467
468 return any((hasReturnAnnotation, hasArgsAnnotations,
469 hasKwargsAnnotations, hasKwonlyargsAnnotations))
470
471 #######################################################################
472 ## Annotations Complexity
473 ##
474 ## adapted from: flake8-annotations-complexity v0.0.6
475 #######################################################################
476
199 def __checkAnnotationComplexity(self): 477 def __checkAnnotationComplexity(self):
200 """ 478 """
201 Private method to check the type annotation complexity. 479 Private method to check the type annotation complexity.
202 """ 480 """
203 maxAnnotationComplexity = self.__args.get( 481 maxAnnotationComplexity = self.__args.get(
204 "MaximumComplexity", self.__defaultArgs["MaximumComplexity"]) 482 "MaximumComplexity", self.__defaultArgs["MaximumComplexity"])
483 # TODO: include 'MaximumLength' in CodeStyleCheckerDialog
484 maxAnnotationLength = self.__args.get(
485 "MaximumLength", self.__defaultArgs["MaximumLength"])
205 typeAnnotations = [] 486 typeAnnotations = []
206 487
207 functionDefs = [ 488 functionDefs = [
208 f for f in ast.walk(self.__tree) 489 f for f in ast.walk(self.__tree)
209 if isinstance(f, (ast.AsyncFunctionDef, ast.FunctionDef)) 490 if isinstance(f, (ast.AsyncFunctionDef, ast.FunctionDef))
214 if functionDef.returns: 495 if functionDef.returns:
215 typeAnnotations.append(functionDef.returns) 496 typeAnnotations.append(functionDef.returns)
216 typeAnnotations += [a.annotation for a in ast.walk(self.__tree) 497 typeAnnotations += [a.annotation for a in ast.walk(self.__tree)
217 if isinstance(a, ast.AnnAssign) and a.annotation] 498 if isinstance(a, ast.AnnAssign) and a.annotation]
218 for annotation in typeAnnotations: 499 for annotation in typeAnnotations:
219 complexity = getAnnotationComplexity(annotation) 500 complexity = self.__getAnnotationComplexity(annotation)
220 if complexity > maxAnnotationComplexity: 501 if complexity > maxAnnotationComplexity:
221 self.__error(annotation.lineno - 1, annotation.col_offset, 502 self.__error(annotation.lineno - 1, annotation.col_offset,
222 "A891", complexity, maxAnnotationComplexity) 503 "A891", complexity, maxAnnotationComplexity)
223 504
224 505 annotationLength = self.__getAnnotationLength(annotation)
225 class FunctionVisitor(ast.NodeVisitor): 506 if annotationLength > maxAnnotationLength:
226 """ 507 self.__error(annotation.lineno - 1, annotation.col_offset,
227 Class implementing a node visitor to check function annotations. 508 "A892", annotationLength, maxAnnotationLength)
228 509
229 Note: this class is modeled after flake8-annotations checker. 510 def __getAnnotationComplexity(self, annotationNode, defaultComplexity=1):
230 """ 511 """
231 def __init__(self, sourceLines): 512 Private method to determine the annotation complexity.
232 """ 513
233 Constructor 514 @param annotationNode reference to the node to determine the annotation
234 515 complexity for
235 @param sourceLines lines of source code 516 @type ast.AST
236 @type list of str 517 @param defaultComplexity default complexity value
237 """ 518 @type int
238 super().__init__() 519 @return annotation complexity
239 520 @rtype = int
240 self.__sourceLines = sourceLines 521 """
241 522 if AstUtilities.isString(annotationNode):
242 self.issues = [] 523 try:
243 524 annotationNode = ast.parse(annotationNode.s).body[0].value
244 def visit_FunctionDef(self, node): 525 except (SyntaxError, IndexError):
245 """ 526 return defaultComplexity
246 Public method to handle a function or method definition. 527 if isinstance(annotationNode, ast.Subscript):
247 528 if sys.version_info >= (3, 9):
248 @param node reference to the node to be processed 529 return (defaultComplexity +
249 @type ast.FunctionDef 530 self.__getAnnotationComplexity(annotationNode.slice))
250 """
251 self.__checkFunctionNode(node)
252 self.generic_visit(node)
253
254 def visit_AsyncFunctionDef(self, node):
255 """
256 Public method to handle an async function or method definition.
257
258 @param node reference to the node to be processed
259 @type ast.AsyncFunctionDef
260 """
261 self.__checkFunctionNode(node)
262 self.generic_visit(node)
263
264 def visit_ClassDef(self, node):
265 """
266 Public method to handle class definitions.
267
268 @param node reference to the node to be processed
269 @type ast.ClassDef
270 """
271 methodNodes = [
272 childNode for childNode in node.body
273 if isinstance(childNode, (ast.FunctionDef, ast.AsyncFunctionDef))
274 ]
275 for methodNode in methodNodes:
276 self.__checkFunctionNode(methodNode, classMethod=True)
277
278 def __checkFunctionNode(self, node, classMethod=False):
279 """
280 Private method to check an individual function definition node.
281
282 @param node reference to the node to be processed
283 @type ast.FunctionDef or ast.AsyncFunctionDef
284 @param classMethod flag indicating a class method
285 @type bool
286 """
287 if node.name.startswith("__") and node.name.endswith("__"):
288 visibilityType = "special"
289 elif node.name.startswith("__"):
290 visibilityType = "private"
291 elif node.name.startswith("_"):
292 visibilityType = "protected"
293 else:
294 visibilityType = "public"
295
296 if classMethod:
297 decorators = [
298 decorator.id for decorator in node.decorator_list
299 if isinstance(decorator, ast.Name)
300 ]
301 if "classmethod" in decorators:
302 classMethodType = "decorator"
303 elif "staticmethod" in decorators:
304 classMethodType = "staticmethod"
305 else: 531 else:
306 classMethodType = "" 532 return (
307 else: 533 defaultComplexity +
308 classMethodType = "function" 534 self.__getAnnotationComplexity(annotationNode.slice.value)
309 535 )
310 # check argument annotations 536 if isinstance(annotationNode, ast.Tuple):
311 for argType in ("args", "vararg", "kwonlyargs", "kwarg"): 537 return max(
312 args = node.args.__getattribute__(argType) 538 (self.__getAnnotationComplexity(n)
313 if args: 539 for n in annotationNode.elts),
314 if not isinstance(args, list): 540 default=defaultComplexity
315 args = [args] 541 )
316 542 return defaultComplexity
317 for arg in args: 543
318 if not arg.annotation: 544 def __getAnnotationLength(self, annotationNode):
319 self.__classifyArgumentError( 545 """
320 arg, argType, classMethodType) 546 Private method to determine the annotation length.
321 547
322 # check function return annotation 548 @param annotationNode reference to the node to determine the annotation
323 if not node.returns: 549 length for
324 lineno = node.lineno 550 @type ast.AST
325 colOffset = self.__sourceLines[lineno - 1].rfind(":") + 1 551 @return annotation length
326 self.__classifyReturnError(classMethodType, visibilityType, 552 @rtype = int
327 lineno, colOffset) 553 """
328 554 if AstUtilities.isString(annotationNode):
329 def __classifyReturnError(self, methodType, visibilityType, lineno, 555 try:
330 colOffset): 556 annotationNode = ast.parse(annotationNode.s).body[0].value
331 """ 557 except (SyntaxError, IndexError):
332 Private method to classify and record a return annotation issue. 558 return 0
333 559 if isinstance(annotationNode, ast.Subscript):
334 @param methodType type of method/function the argument belongs to 560 try:
335 @type str 561 if sys.version_info >= (3, 9):
336 @param visibilityType visibility of the function 562 return len(annotationNode.slice.elts)
337 @type str 563 else:
338 @param lineno line number 564 return len(annotationNode.slice.value.elts)
339 @type int 565 except AttributeError:
340 @param colOffset column number 566 return 0
341 @type int 567 return 0
342 """
343 # create a dummy AST node to report line and column
344 node = ast.AST()
345 node.lineno = lineno
346 node.col_offset = colOffset
347
348 # now classify the issue
349 if methodType == "classmethod":
350 self.issues.append((node, "A206"))
351 elif methodType == "staticmethod":
352 self.issues.append((node, "A205"))
353 elif visibilityType == "special":
354 self.issues.append((node, "A204"))
355 elif visibilityType == "private":
356 self.issues.append((node, "A203"))
357 elif visibilityType == "protected":
358 self.issues.append((node, "A202"))
359 else:
360 self.issues.append((node, "A201"))
361
362 def __classifyArgumentError(self, argNode, argType, methodType):
363 """
364 Private method to classify and record an argument annotation issue.
365
366 @param argNode reference to the argument node
367 @type ast.arguments
368 @param argType type of the argument node
369 @type str
370 @param methodType type of method/function the argument belongs to
371 @type str
372 """
373 # check class method issues
374 if methodType != "function" and argNode.arg in ("cls", "self"):
375 if methodType == "classmethod":
376 self.issues.append((argNode, "A102"))
377 return
378 elif methodType != "staticmethod":
379 self.issues.append((argNode, "A101"))
380 return
381
382 # check all other arguments
383 if argType == "kwarg":
384 self.issues.append((argNode, "A003", argNode.arg))
385 elif argType == "vararg":
386 self.issues.append((argNode, "A002", argNode.arg))
387 else:
388 # args and kwonlyargs
389 self.issues.append((argNode, "A001", argNode.arg))
390
391 ######################################################################
392 ## some utility functions below
393 ######################################################################
394
395
396 def hasTypeAnnotations(funcNode):
397 """
398 Function to check for type annotations.
399
400 @param funcNode reference to the function definition node to be checked
401 @type ast.AsyncFunctionDef or ast.FunctionDef
402 @return flag indicating the presence of type annotations
403 @rtype bool
404 """
405 hasReturnAnnotation = funcNode.returns is not None
406 hasArgsAnnotations = any(a for a in funcNode.args.args
407 if a.annotation is not None)
408 hasKwargsAnnotations = (funcNode.args and
409 funcNode.args.kwarg and
410 funcNode.args.kwarg.annotation is not None)
411 hasKwonlyargsAnnotations = any(a for a in funcNode.args.kwonlyargs
412 if a.annotation is not None)
413
414 return any((hasReturnAnnotation, hasArgsAnnotations, hasKwargsAnnotations,
415 hasKwonlyargsAnnotations))
416
417
418 def getAnnotationComplexity(annotationNode):
419 """
420 Function to determine the annotation complexity.
421
422 @param annotationNode reference to the node to determine the annotation
423 complexity for
424 @type ast.AST
425 @return annotation complexity
426 @rtype = int
427 """
428 if AstUtilities.isString(annotationNode):
429 annotationNode = ast.parse(annotationNode.s).body[0].value
430 if isinstance(annotationNode, ast.Tuple):
431 return max(getAnnotationComplexity(n) for n in annotationNode.elts)
432 return 1

eric ide

mercurial