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

branch
maintenance
changeset 8273
698ae46f40a4
parent 8258
82b608e352ec
equal deleted inserted replaced
8190:fb0ef164f536 8273:698ae46f40a4
5 5
6 """ 6 """
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
11 import ast
10 import sys 12 import sys
11 import ast 13 from functools import lru_cache
12 14
13 import AstUtilities 15 import AstUtilities
14 16
15 17 from .AnnotationsEnums import AnnotationType, ClassDecoratorType, FunctionType
16 class AnnotationsChecker(object): 18 from .AnnotationsCheckerDefaults import AnnotationsCheckerDefaultArgs
19
20
21 class AnnotationsChecker:
17 """ 22 """
18 Class implementing a checker for function type annotations. 23 Class implementing a checker for function type annotations.
19 """ 24 """
20 Codes = [ 25 Codes = [
21 ## Function Annotations 26 ## Function Annotations
25 "A101", "A102", 30 "A101", "A102",
26 31
27 ## Return Annotations 32 ## Return Annotations
28 "A201", "A202", "A203", "A204", "A205", "A206", 33 "A201", "A202", "A203", "A204", "A205", "A206",
29 34
35 ## Mixed kind of annotations
36 "A301",
37
30 ## Annotation Coverage 38 ## Annotation Coverage
31 "A881", 39 "A881",
32 40
33 ## Annotation Complexity 41 ## Annotation Complexity
34 "A891", 42 "A891", "A892",
35
36 ## Syntax Error
37 "A999",
38 ] 43 ]
39 44
40 def __init__(self, source, filename, select, ignore, expected, repeat, 45 def __init__(self, source, filename, tree, select, ignore, expected,
41 args): 46 repeat, args):
42 """ 47 """
43 Constructor 48 Constructor
44 49
45 @param source source code to be checked 50 @param source source code to be checked
46 @type list of str 51 @type list of str
47 @param filename name of the source file 52 @param filename name of the source file
48 @type str 53 @type str
54 @param tree AST tree of the source code
55 @type ast.Module
49 @param select list of selected codes 56 @param select list of selected codes
50 @type list of str 57 @type list of str
51 @param ignore list of codes to be ignored 58 @param ignore list of codes to be ignored
52 @type list of str 59 @type list of str
53 @param expected list of expected codes 60 @param expected list of expected codes
61 self.__ignore = ('',) if select else tuple(ignore) 68 self.__ignore = ('',) if select else tuple(ignore)
62 self.__expected = expected[:] 69 self.__expected = expected[:]
63 self.__repeat = repeat 70 self.__repeat = repeat
64 self.__filename = filename 71 self.__filename = filename
65 self.__source = source[:] 72 self.__source = source[:]
73 self.__tree = copy.deepcopy(tree)
66 self.__args = args 74 self.__args = args
67 75
68 # statistics counters 76 # statistics counters
69 self.counters = {} 77 self.counters = {}
70 78
73 81
74 checkersWithCodes = [ 82 checkersWithCodes = [
75 ( 83 (
76 self.__checkFunctionAnnotations, 84 self.__checkFunctionAnnotations,
77 ("A001", "A002", "A003", "A101", "A102", 85 ("A001", "A002", "A003", "A101", "A102",
78 "A201", "A202", "A203", "A204", "A205", "A206",) 86 "A201", "A202", "A203", "A204", "A205", "A206",
87 "A301", )
79 ), 88 ),
80 (self.__checkAnnotationsCoverage, ("A881",)), 89 (self.__checkAnnotationsCoverage, ("A881",)),
81 (self.__checkAnnotationComplexity, ("A891",)), 90 (self.__checkAnnotationComplexity, ("A891", "A892")),
82 ] 91 ]
83
84 self.__defaultArgs = {
85 "MinimumCoverage": 75, # % of type annotation coverage
86 "MaximumComplexity": 3,
87 }
88 92
89 self.__checkers = [] 93 self.__checkers = []
90 for checker, codes in checkersWithCodes: 94 for checker, codes in checkersWithCodes:
91 if any(not (code and self.__ignoreCode(code)) 95 if any(not (code and self.__ignoreCode(code))
92 for code in codes): 96 for code in codes):
139 "code": code, 143 "code": code,
140 "args": args, 144 "args": args,
141 } 145 }
142 ) 146 )
143 147
144 def __reportInvalidSyntax(self):
145 """
146 Private method to report a syntax error.
147 """
148 exc_type, exc = sys.exc_info()[:2]
149 if len(exc.args) > 1:
150 offset = exc.args[1]
151 if len(offset) > 2:
152 offset = offset[1:3]
153 else:
154 offset = (1, 0)
155 self.__error(offset[0] - 1, offset[1] or 0,
156 'A999', exc_type.__name__, exc.args[0])
157
158 def __generateTree(self):
159 """
160 Private method to generate an AST for our source.
161
162 @return generated AST
163 @rtype ast.Module
164 """
165 return ast.parse("".join(self.__source), self.__filename)
166
167 def run(self): 148 def run(self):
168 """ 149 """
169 Public method to check the given source against annotation issues. 150 Public method to check the given source against annotation issues.
170 """ 151 """
171 if not self.__filename: 152 if not self.__filename:
174 155
175 if not self.__checkers: 156 if not self.__checkers:
176 # don't do anything, if no codes were selected 157 # don't do anything, if no codes were selected
177 return 158 return
178 159
179 try:
180 self.__tree = self.__generateTree()
181 except (SyntaxError, TypeError):
182 self.__reportInvalidSyntax()
183 return
184
185 for check in self.__checkers: 160 for check in self.__checkers:
186 check() 161 check()
187 162
163 #######################################################################
164 ## Annotations
165 ##
166 ## adapted from: flake8-annotations v2.6.2
167 #######################################################################
168
188 def __checkFunctionAnnotations(self): 169 def __checkFunctionAnnotations(self):
189 """ 170 """
190 Private method to check for function annotation issues. 171 Private method to check for function annotation issues.
191 """ 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
192 visitor = FunctionVisitor(self.__source) 198 visitor = FunctionVisitor(self.__source)
193 visitor.visit(self.__tree) 199 visitor.visit(self.__tree)
194 for issue in visitor.issues: 200
195 node = issue[0] 201 # Keep track of the last encountered function decorated by
196 reason = issue[1] 202 # `typing.overload`, if any. Per the `typing` module documentation,
197 params = issue[2:] 203 # a series of overload-decorated definitions must be followed by
198 self.__error(node.lineno - 1, node.col_offset, reason, *params) 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 #######################################################################
199 400
200 def __checkAnnotationsCoverage(self): 401 def __checkAnnotationsCoverage(self):
201 """ 402 """
202 Private method to check for function annotation coverage. 403 Private method to check for function annotation coverage.
203 """ 404 """
204 minAnnotationsCoverage = self.__args.get( 405 minAnnotationsCoverage = self.__args.get(
205 "MinimumCoverage", self.__defaultArgs["MinimumCoverage"]) 406 "MinimumCoverage",
407 AnnotationsCheckerDefaultArgs["MinimumCoverage"])
206 if minAnnotationsCoverage == 0: 408 if minAnnotationsCoverage == 0:
207 # 0 means it is switched off 409 # 0 means it is switched off
208 return 410 return
209 411
210 functionDefs = [ 412 functionDefs = [
214 if not functionDefs: 416 if not functionDefs:
215 # no functions/methods at all 417 # no functions/methods at all
216 return 418 return
217 419
218 functionDefAnnotationsInfo = [ 420 functionDefAnnotationsInfo = [
219 hasTypeAnnotations(f) for f in functionDefs 421 self.__hasTypeAnnotations(f) for f in functionDefs
220 ] 422 ]
221 annotationsCoverage = int( 423 annotationsCoverage = int(
222 len(list(filter(None, functionDefAnnotationsInfo))) / 424 len(list(filter(None, functionDefAnnotationsInfo))) /
223 len(functionDefAnnotationsInfo) * 100 425 len(functionDefAnnotationsInfo) * 100
224 ) 426 )
225 if annotationsCoverage < minAnnotationsCoverage: 427 if annotationsCoverage < minAnnotationsCoverage:
226 self.__error(0, 0, "A881", annotationsCoverage) 428 self.__error(0, 0, "A881", annotationsCoverage)
227 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
228 def __checkAnnotationComplexity(self): 457 def __checkAnnotationComplexity(self):
229 """ 458 """
230 Private method to check the type annotation complexity. 459 Private method to check the type annotation complexity.
231 """ 460 """
232 maxAnnotationComplexity = self.__args.get( 461 maxAnnotationComplexity = self.__args.get(
233 "MaximumComplexity", self.__defaultArgs["MaximumComplexity"]) 462 "MaximumComplexity",
463 AnnotationsCheckerDefaultArgs["MaximumComplexity"])
464 maxAnnotationLength = self.__args.get(
465 "MaximumLength", AnnotationsCheckerDefaultArgs["MaximumLength"])
234 typeAnnotations = [] 466 typeAnnotations = []
235 467
236 functionDefs = [ 468 functionDefs = [
237 f for f in ast.walk(self.__tree) 469 f for f in ast.walk(self.__tree)
238 if isinstance(f, (ast.AsyncFunctionDef, ast.FunctionDef)) 470 if isinstance(f, (ast.AsyncFunctionDef, ast.FunctionDef))
243 if functionDef.returns: 475 if functionDef.returns:
244 typeAnnotations.append(functionDef.returns) 476 typeAnnotations.append(functionDef.returns)
245 typeAnnotations += [a.annotation for a in ast.walk(self.__tree) 477 typeAnnotations += [a.annotation for a in ast.walk(self.__tree)
246 if isinstance(a, ast.AnnAssign) and a.annotation] 478 if isinstance(a, ast.AnnAssign) and a.annotation]
247 for annotation in typeAnnotations: 479 for annotation in typeAnnotations:
248 complexity = getAnnotationComplexity(annotation) 480 complexity = self.__getAnnotationComplexity(annotation)
249 if complexity > maxAnnotationComplexity: 481 if complexity > maxAnnotationComplexity:
250 self.__error(annotation.lineno - 1, annotation.col_offset, 482 self.__error(annotation.lineno - 1, annotation.col_offset,
251 "A891", complexity, maxAnnotationComplexity) 483 "A891", complexity, maxAnnotationComplexity)
252 484
253 485 annotationLength = self.__getAnnotationLength(annotation)
254 class FunctionVisitor(ast.NodeVisitor): 486 if annotationLength > maxAnnotationLength:
255 """ 487 self.__error(annotation.lineno - 1, annotation.col_offset,
256 Class implementing a node visitor to check function annotations. 488 "A892", annotationLength, maxAnnotationLength)
257 489
258 Note: this class is modelled after flake8-annotations checker. 490 def __getAnnotationComplexity(self, annotationNode, defaultComplexity=1):
259 """ 491 """
260 def __init__(self, sourceLines): 492 Private method to determine the annotation complexity.
261 """ 493
262 Constructor 494 @param annotationNode reference to the node to determine the annotation
263 495 complexity for
264 @param sourceLines lines of source code 496 @type ast.AST
265 @type list of str 497 @param defaultComplexity default complexity value
266 """ 498 @type int
267 super(FunctionVisitor, self).__init__() 499 @return annotation complexity
268 500 @rtype = int
269 self.__sourceLines = sourceLines 501 """
270 502 if AstUtilities.isString(annotationNode):
271 self.issues = [] 503 try:
272 504 annotationNode = ast.parse(annotationNode.s).body[0].value
273 def visit_FunctionDef(self, node): 505 except (SyntaxError, IndexError):
274 """ 506 return defaultComplexity
275 Public method to handle a function or method definition. 507 if isinstance(annotationNode, ast.Subscript):
276 508 if sys.version_info >= (3, 9):
277 @param node reference to the node to be processed 509 return (defaultComplexity +
278 @type ast.FunctionDef 510 self.__getAnnotationComplexity(annotationNode.slice))
279 """
280 self.__checkFunctionNode(node)
281 self.generic_visit(node)
282
283 def visit_AsyncFunctionDef(self, node):
284 """
285 Public method to handle an async function or method definition.
286
287 @param node reference to the node to be processed
288 @type ast.AsyncFunctionDef
289 """
290 self.__checkFunctionNode(node)
291 self.generic_visit(node)
292
293 def visit_ClassDef(self, node):
294 """
295 Public method to handle class definitions.
296
297 @param node reference to the node to be processed
298 @type ast.ClassDef
299 """
300 methodNodes = [
301 childNode for childNode in node.body
302 if isinstance(childNode, (ast.FunctionDef, ast.AsyncFunctionDef))
303 ]
304 for methodNode in methodNodes:
305 self.__checkFunctionNode(methodNode, classMethod=True)
306
307 def __checkFunctionNode(self, node, classMethod=False):
308 """
309 Private method to check an individual function definition node.
310
311 @param node reference to the node to be processed
312 @type ast.FunctionDef or ast.AsyncFunctionDef
313 @param classMethod flag indicating a class method
314 @type bool
315 """
316 if node.name.startswith("__") and node.name.endswith("__"):
317 visibilityType = "special"
318 elif node.name.startswith("__"):
319 visibilityType = "private"
320 elif node.name.startswith("_"):
321 visibilityType = "protected"
322 else:
323 visibilityType = "public"
324
325 if classMethod:
326 decorators = [
327 decorator.id for decorator in node.decorator_list
328 if isinstance(decorator, ast.Name)
329 ]
330 if "classmethod" in decorators:
331 classMethodType = "decorator"
332 elif "staticmethod" in decorators:
333 classMethodType = "staticmethod"
334 else: 511 else:
335 classMethodType = "" 512 return (
336 else: 513 defaultComplexity +
337 classMethodType = "function" 514 self.__getAnnotationComplexity(annotationNode.slice.value)
338 515 )
339 # check argument annotations 516 if isinstance(annotationNode, ast.Tuple):
340 for argType in ("args", "vararg", "kwonlyargs", "kwarg"): 517 return max(
341 args = node.args.__getattribute__(argType) 518 (self.__getAnnotationComplexity(n)
342 if args: 519 for n in annotationNode.elts),
343 if not isinstance(args, list): 520 default=defaultComplexity
344 args = [args] 521 )
345 522 return defaultComplexity
346 for arg in args: 523
347 if not arg.annotation: 524 def __getAnnotationLength(self, annotationNode):
348 self.__classifyArgumentError( 525 """
349 arg, argType, classMethodType) 526 Private method to determine the annotation length.
350 527
351 # check function return annotation 528 @param annotationNode reference to the node to determine the annotation
352 if not node.returns: 529 length for
353 lineno = node.lineno 530 @type ast.AST
354 colOffset = self.__sourceLines[lineno - 1].rfind(":") + 1 531 @return annotation length
355 self.__classifyReturnError(classMethodType, visibilityType, 532 @rtype = int
356 lineno, colOffset) 533 """
357 534 if AstUtilities.isString(annotationNode):
358 def __classifyReturnError(self, methodType, visibilityType, lineno, 535 try:
359 colOffset): 536 annotationNode = ast.parse(annotationNode.s).body[0].value
360 """ 537 except (SyntaxError, IndexError):
361 Private method to classify and record a return annotation issue. 538 return 0
362 539 if isinstance(annotationNode, ast.Subscript):
363 @param methodType type of method/function the argument belongs to 540 try:
364 @type str 541 if sys.version_info >= (3, 9):
365 @param visibilityType visibility of the function 542 return len(annotationNode.slice.elts)
366 @type str 543 else:
367 @param lineno line number 544 return len(annotationNode.slice.value.elts)
368 @type int 545 except AttributeError:
369 @param colOffset column number 546 return 0
370 @type int 547 return 0
371 """
372 # create a dummy AST node to report line and column
373 node = ast.AST()
374 node.lineno = lineno
375 node.col_offset = colOffset
376
377 # now classify the issue
378 if methodType == "classmethod":
379 self.issues.append((node, "A206"))
380 elif methodType == "staticmethod":
381 self.issues.append((node, "A205"))
382 elif visibilityType == "special":
383 self.issues.append((node, "A204"))
384 elif visibilityType == "private":
385 self.issues.append((node, "A203"))
386 elif visibilityType == "protected":
387 self.issues.append((node, "A202"))
388 else:
389 self.issues.append((node, "A201"))
390
391 def __classifyArgumentError(self, argNode, argType, methodType):
392 """
393 Private method to classify and record an argument annotation issue.
394
395 @param argNode reference to the argument node
396 @type ast.arguments
397 @param argType type of the argument node
398 @type str
399 @param methodType type of method/function the argument belongs to
400 @type str
401 """
402 # check class method issues
403 if methodType != "function":
404 if argNode.arg in ("cls", "self"):
405 if methodType == "classmethod":
406 self.issues.append((argNode, "A102"))
407 return
408 elif methodType != "staticmethod":
409 self.issues.append((argNode, "A101"))
410 return
411
412 # check all other arguments
413 if argType == "kwarg":
414 self.issues.append((argNode, "A003", argNode.arg))
415 elif argType == "vararg":
416 self.issues.append((argNode, "A002", argNode.arg))
417 else:
418 # args and kwonlyargs
419 self.issues.append((argNode, "A001", argNode.arg))
420
421 ######################################################################
422 ## some utility functions below
423 ######################################################################
424
425
426 def hasTypeAnnotations(funcNode):
427 """
428 Function to check for type annotations.
429
430 @param funcNode reference to the function definition node to be checked
431 @type ast.AsyncFunctionDef or ast.FunctionDef
432 @return flag indicating the presence of type annotations
433 @rtype bool
434 """
435 hasReturnAnnotation = funcNode.returns is not None
436 hasArgsAnnotations = any(a for a in funcNode.args.args
437 if a.annotation is not None)
438 hasKwargsAnnotations = (funcNode.args and
439 funcNode.args.kwarg and
440 funcNode.args.kwarg.annotation is not None)
441 hasKwonlyargsAnnotations = any(a for a in funcNode.args.kwonlyargs
442 if a.annotation is not None)
443
444 return any((hasReturnAnnotation, hasArgsAnnotations, hasKwargsAnnotations,
445 hasKwonlyargsAnnotations))
446
447
448 def getAnnotationComplexity(annotationNode):
449 """
450 Function to determine the annotation complexity.
451
452 @param annotationNode reference to the node to determine the annotation
453 complexity for
454 @type ast.AST
455 @return annotation complexity
456 @rtype = int
457 """
458 if AstUtilities.isString(annotationNode):
459 annotationNode = ast.parse(annotationNode.s).body[0].value
460 if isinstance(annotationNode, ast.Subscript):
461 return 1 + getAnnotationComplexity(annotationNode.slice.value)
462 if isinstance(annotationNode, ast.Tuple):
463 return max(getAnnotationComplexity(n) for n in annotationNode.elts)
464 return 1

eric ide

mercurial