|
1 '''This module contains all high-level helpers function that allow to work with |
|
2 Cyclomatic Complexity |
|
3 ''' |
|
4 |
|
5 import math |
|
6 from radon.visitors import GET_COMPLEXITY, ComplexityVisitor, code2ast |
|
7 |
|
8 |
|
9 # sorted_block ordering functions |
|
10 SCORE = lambda block: -GET_COMPLEXITY(block) |
|
11 LINES = lambda block: block.lineno |
|
12 ALPHA = lambda block: block.name |
|
13 |
|
14 |
|
15 def cc_rank(cc): |
|
16 r'''Rank the complexity score from A to F, where A stands for the simplest |
|
17 and best score and F the most complex and worst one: |
|
18 |
|
19 ============= ===================================================== |
|
20 1 - 5 A (low risk - simple block) |
|
21 6 - 10 B (low risk - well structured and stable block) |
|
22 11 - 20 C (moderate risk - slightly complex block) |
|
23 21 - 30 D (more than moderate risk - more complex block) |
|
24 31 - 40 E (high risk - complex block, alarming) |
|
25 41+ F (very high risk - error-prone, unstable block) |
|
26 ============= ===================================================== |
|
27 |
|
28 Here *block* is used in place of function, method or class. |
|
29 |
|
30 The formula used to convert the score into an index is the following: |
|
31 |
|
32 .. math:: |
|
33 |
|
34 \text{rank} = \left \lceil \dfrac{\text{score}}{10} \right \rceil |
|
35 - H(5 - \text{score}) |
|
36 |
|
37 where ``H(s)`` stands for the Heaviside Step Function. |
|
38 The rank is then associated to a letter (0 = A, 5 = F). |
|
39 ''' |
|
40 if cc < 0: |
|
41 raise ValueError('Complexity must be a non-negative value') |
|
42 return chr(min(int(math.ceil(cc / 10.) or 1) - (1, 0)[5 - cc < 0], 5) + 65) |
|
43 |
|
44 |
|
45 def average_complexity(blocks): |
|
46 '''Compute the average Cyclomatic complexity from the given blocks. |
|
47 Blocks must be either :class:`~radon.visitors.Function` or |
|
48 :class:`~radon.visitors.Class`. If the block list is empty, then 0 is |
|
49 returned. |
|
50 ''' |
|
51 size = len(blocks) |
|
52 if size == 0: |
|
53 return 0 |
|
54 return sum((GET_COMPLEXITY(block) for block in blocks), .0) / len(blocks) |
|
55 |
|
56 |
|
57 def sorted_results(blocks, order=SCORE): |
|
58 '''Given a ComplexityVisitor instance, returns a list of sorted blocks |
|
59 with respect to complexity. A block is a either |
|
60 :class:`~radon.visitors.Function` object or a |
|
61 :class:`~radon.visitors.Class` object. |
|
62 The blocks are sorted in descending order from the block with the highest |
|
63 complexity. |
|
64 |
|
65 The optional `order` parameter indicates how to sort the blocks. It can be: |
|
66 |
|
67 * `LINES`: sort by line numbering; |
|
68 * `ALPHA`: sort by name (from A to Z); |
|
69 * `SCORE`: sorty by score (descending). |
|
70 |
|
71 Default is `SCORE`. |
|
72 ''' |
|
73 return sorted(blocks, key=order) |
|
74 |
|
75 |
|
76 def add_closures(blocks): |
|
77 '''Process a list of blocks by adding all closures as top-level blocks.''' |
|
78 new_blocks = [] |
|
79 for block in blocks: |
|
80 new_blocks.append(block) |
|
81 if 'closures' not in block._fields: |
|
82 continue |
|
83 for closure in block.closures: |
|
84 named = closure._replace(name=block.name + '.' + closure.name) |
|
85 new_blocks.append(named) |
|
86 return new_blocks |
|
87 |
|
88 |
|
89 def cc_visit(code, **kwargs): |
|
90 '''Visit the given code with :class:`~radon.visitors.ComplexityVisitor`. |
|
91 All the keyword arguments are directly passed to the visitor. |
|
92 ''' |
|
93 return cc_visit_ast(code2ast(code), **kwargs) |
|
94 |
|
95 |
|
96 def cc_visit_ast(ast_node, **kwargs): |
|
97 '''Visit the AST node with :class:`~radon.visitors.ComplexityVisitor`. All |
|
98 the keyword arguments are directly passed to the visitor. |
|
99 ''' |
|
100 return ComplexityVisitor.from_ast(ast_node, **kwargs).blocks |
|
101 |
|
102 |
|
103 class Flake8Checker(object): |
|
104 '''Entry point for the Flake8 tool.''' |
|
105 |
|
106 name = 'radon' |
|
107 _code = 'R701' |
|
108 _error_tmpl = 'R701: %r is too complex (%d)' |
|
109 no_assert = False |
|
110 max_cc = -1 |
|
111 |
|
112 def __init__(self, tree, filename): |
|
113 '''Accept the AST tree and a filename (unused).''' |
|
114 self.tree = tree |
|
115 |
|
116 version = property(lambda self: __import__('radon').__version__) |
|
117 |
|
118 @classmethod |
|
119 def add_options(cls, parser): # pragma: no cover |
|
120 '''Add custom options to the global parser.''' |
|
121 parser.add_option('--radon-max-cc', default=-1, action='store', |
|
122 type='int', help='Radon complexity threshold') |
|
123 parser.add_option('--radon-no-assert', dest='no_assert', |
|
124 action='store_true', default=False, |
|
125 help='Radon will ignore assert statements') |
|
126 parser.config_options.append('radon-max-cc') |
|
127 parser.config_options.append('radon-no-assert') |
|
128 |
|
129 @classmethod |
|
130 def parse_options(cls, options): # pragma: no cover |
|
131 '''Save actual options as class attributes.''' |
|
132 cls.max_cc = options.radon_max_cc |
|
133 cls.no_assert = options.no_assert |
|
134 |
|
135 def run(self): |
|
136 '''Run the ComplexityVisitor over the AST tree.''' |
|
137 if self.max_cc < 0: |
|
138 if not self.no_assert: |
|
139 return |
|
140 self.max_cc = 10 |
|
141 visitor = ComplexityVisitor.from_ast(self.tree, |
|
142 no_assert=self.no_assert) |
|
143 for block in visitor.blocks: |
|
144 if block.complexity > self.max_cc: |
|
145 text = self._error_tmpl % (block.name, block.complexity) |
|
146 yield block.lineno, 0, text, type(self) |