23 "A101", "A102", |
23 "A101", "A102", |
24 |
24 |
25 ## Return Annotations |
25 ## Return Annotations |
26 "A201", "A202", "A203", "A204", "A205", "A206", |
26 "A201", "A202", "A203", "A204", "A205", "A206", |
27 |
27 |
|
28 ## Annotation Coverage |
|
29 "A881", |
|
30 |
|
31 ## Annotation Complexity |
|
32 "A891", |
|
33 |
28 ## Syntax Error |
34 ## Syntax Error |
29 "A999", |
35 "A999", |
30 ] |
36 ] |
31 |
37 |
32 def __init__(self, source, filename, select, ignore, expected, repeat): |
38 def __init__(self, source, filename, select, ignore, expected, repeat, |
|
39 args): |
33 """ |
40 """ |
34 Constructor |
41 Constructor |
35 |
42 |
36 @param source source code to be checked |
43 @param source source code to be checked |
37 @type list of str |
44 @type list of str |
43 @type list of str |
50 @type list of str |
44 @param expected list of expected codes |
51 @param expected list of expected codes |
45 @type list of str |
52 @type list of str |
46 @param repeat flag indicating to report each occurrence of a code |
53 @param repeat flag indicating to report each occurrence of a code |
47 @type bool |
54 @type bool |
|
55 @param args dictionary of arguments for the annotation checks |
|
56 @type dict |
48 """ |
57 """ |
49 self.__select = tuple(select) |
58 self.__select = tuple(select) |
50 self.__ignore = ('',) if select else tuple(ignore) |
59 self.__ignore = ('',) if select else tuple(ignore) |
51 self.__expected = expected[:] |
60 self.__expected = expected[:] |
52 self.__repeat = repeat |
61 self.__repeat = repeat |
53 self.__filename = filename |
62 self.__filename = filename |
54 self.__source = source[:] |
63 self.__source = source[:] |
|
64 self.__args = args |
55 |
65 |
56 # statistics counters |
66 # statistics counters |
57 self.counters = {} |
67 self.counters = {} |
58 |
68 |
59 # collection of detected errors |
69 # collection of detected errors |
63 ( |
73 ( |
64 self.__checkFunctionAnnotations, |
74 self.__checkFunctionAnnotations, |
65 ("A001", "A002", "A003", "A101", "A102", |
75 ("A001", "A002", "A003", "A101", "A102", |
66 "A201", "A202", "A203", "A204", "A205", "A206",) |
76 "A201", "A202", "A203", "A204", "A205", "A206",) |
67 ), |
77 ), |
|
78 (self.__checkAnnotationsCoverage, ("A881",)), |
|
79 (self.__checkAnnotationComplexity, ("A891",)), |
68 ] |
80 ] |
|
81 |
|
82 self.__defaultArgs = { |
|
83 "MinimumCoverage": 75, # % of type annotation coverage |
|
84 "MaximumComplexity": 3, |
|
85 } |
69 |
86 |
70 self.__checkers = [] |
87 self.__checkers = [] |
71 for checker, codes in checkersWithCodes: |
88 for checker, codes in checkersWithCodes: |
72 if any(not (code and self.__ignoreCode(code)) |
89 if any(not (code and self.__ignoreCode(code)) |
73 for code in codes): |
90 for code in codes): |
176 for issue in visitor.issues: |
193 for issue in visitor.issues: |
177 node = issue[0] |
194 node = issue[0] |
178 reason = issue[1] |
195 reason = issue[1] |
179 params = issue[2:] |
196 params = issue[2:] |
180 self.__error(node.lineno - 1, node.col_offset, reason, *params) |
197 self.__error(node.lineno - 1, node.col_offset, reason, *params) |
|
198 |
|
199 def __checkAnnotationsCoverage(self): |
|
200 """ |
|
201 Private method to check for function annotation coverage. |
|
202 """ |
|
203 minAnnotationsCoverage = self.__args.get( |
|
204 "MinimumCoverage", self.__defaultArgs["MinimumCoverage"]) |
|
205 if minAnnotationsCoverage == 0: |
|
206 # 0 means it is switched off |
|
207 return |
|
208 |
|
209 functionDefs = [ |
|
210 f for f in ast.walk(self.__tree) |
|
211 if isinstance(f, (ast.AsyncFunctionDef, ast.FunctionDef)) |
|
212 ] |
|
213 if not functionDefs: |
|
214 # no functions/methods at all |
|
215 return |
|
216 |
|
217 functionDefAnnotationsInfo = [ |
|
218 hasTypeAnnotations(f) for f in functionDefs |
|
219 ] |
|
220 annotationsCoverage = int( |
|
221 len(list(filter(None, functionDefAnnotationsInfo))) / |
|
222 len(functionDefAnnotationsInfo) * 100 |
|
223 ) |
|
224 if annotationsCoverage < minAnnotationsCoverage: |
|
225 self.__error(0, 0, "A881", annotationsCoverage) |
|
226 |
|
227 def __checkAnnotationComplexity(self): |
|
228 """ |
|
229 Private method to check the type annotation complexity. |
|
230 """ |
|
231 maxAnnotationComplexity = self.__args.get( |
|
232 "MaximumComplexity", self.__defaultArgs["MaximumComplexity"]) |
|
233 typeAnnotations = [] |
|
234 |
|
235 functionDefs = [ |
|
236 f for f in ast.walk(self.__tree) |
|
237 if isinstance(f, (ast.AsyncFunctionDef, ast.FunctionDef)) |
|
238 ] |
|
239 for functionDef in functionDefs: |
|
240 typeAnnotations += list(filter( |
|
241 None, [a.annotation for a in functionDef.args.args])) |
|
242 if functionDef.returns: |
|
243 typeAnnotations.append(functionDef.returns) |
|
244 typeAnnotations += [a.annotation for a in ast.walk(self.__tree) |
|
245 if isinstance(a, ast.AnnAssign) and a.annotation] |
|
246 for annotation in typeAnnotations: |
|
247 complexity = getAnnotationComplexity(annotation) |
|
248 if complexity > maxAnnotationComplexity: |
|
249 self.__error(annotation.lineno - 1, annotation.col_offset, |
|
250 "A891", complexity, maxAnnotationComplexity) |
181 |
251 |
182 |
252 |
183 class FunctionVisitor(ast.NodeVisitor): |
253 class FunctionVisitor(ast.NodeVisitor): |
184 """ |
254 """ |
185 Class implementing a node visitor to check function annotations. |
255 Class implementing a node visitor to check function annotations. |
277 self.__classifyArgumentError( |
347 self.__classifyArgumentError( |
278 arg, argType, classMethodType) |
348 arg, argType, classMethodType) |
279 |
349 |
280 # check function return annotation |
350 # check function return annotation |
281 if not node.returns: |
351 if not node.returns: |
282 lineno = node.body[0].lineno |
352 lineno = node.lineno |
283 colOffset = self.__sourceLines[lineno - 1].find(":") + 1 |
353 colOffset = self.__sourceLines[lineno - 1].rfind(":") + 1 |
284 self.__classifyReturnError(classMethodType, visibilityType, |
354 self.__classifyReturnError(classMethodType, visibilityType, |
285 lineno, colOffset) |
355 lineno, colOffset) |
286 |
356 |
287 def __classifyReturnError(self, methodType, visibilityType, lineno, |
357 def __classifyReturnError(self, methodType, visibilityType, lineno, |
288 colOffset): |
358 colOffset): |
344 elif argType == "vararg": |
414 elif argType == "vararg": |
345 self.issues.append((argNode, "A002", argNode.arg)) |
415 self.issues.append((argNode, "A002", argNode.arg)) |
346 else: |
416 else: |
347 # args and kwonlyargs |
417 # args and kwonlyargs |
348 self.issues.append((argNode, "A001", argNode.arg)) |
418 self.issues.append((argNode, "A001", argNode.arg)) |
|
419 |
|
420 ###################################################################### |
|
421 ## some utility functions below |
|
422 ###################################################################### |
|
423 |
|
424 |
|
425 def hasTypeAnnotations(funcNode): |
|
426 """ |
|
427 Function to check for type annotations. |
|
428 |
|
429 @param funcNode reference to the function definition node to be checked |
|
430 @type ast.AsyncFunctionDef or ast.FunctionDef |
|
431 @return flag indicating the presence of type annotations |
|
432 @rtype bool |
|
433 """ |
|
434 hasReturnAnnotation = funcNode.returns is not None |
|
435 hasArgsAnnotations = any(a for a in funcNode.args.args |
|
436 if a.annotation is not None) |
|
437 hasKwargsAnnotations = (funcNode.args and |
|
438 funcNode.args.kwarg and |
|
439 funcNode.args.kwarg.annotation is not None) |
|
440 hasKwonlyargsAnnotations = any(a for a in funcNode.args.kwonlyargs |
|
441 if a.annotation is not None) |
|
442 |
|
443 return any((hasReturnAnnotation, hasArgsAnnotations, hasKwargsAnnotations, |
|
444 hasKwonlyargsAnnotations)) |
|
445 |
|
446 |
|
447 def getAnnotationComplexity(annotationNode): |
|
448 """ |
|
449 Function to determine the annotation complexity. |
|
450 |
|
451 @param annotationNode reference to the node to determine the annotation |
|
452 complexity for |
|
453 @type ast.AST |
|
454 @return annotation complexity |
|
455 @rtype = int |
|
456 """ |
|
457 if isinstance(annotationNode, ast.Str): |
|
458 annotationNode = ast.parse(annotationNode.s).body[0].value |
|
459 if isinstance(annotationNode, ast.Subscript): |
|
460 return 1 + getAnnotationComplexity(annotationNode.slice.value) |
|
461 if isinstance(annotationNode, ast.Tuple): |
|
462 return max(getAnnotationComplexity(n) for n in annotationNode.elts) |
|
463 return 1 |