0
# AST Visitors
1
2
Low-level AST visitor classes for custom analysis and integration. Provides the foundation for all radon analysis capabilities using the visitor pattern to traverse Python Abstract Syntax Trees (AST) and extract code metrics.
3
4
## Capabilities
5
6
### Base Visitor Classes
7
8
Foundation classes providing common functionality for AST traversal and analysis.
9
10
```python { .api }
11
class CodeVisitor(ast.NodeVisitor):
12
"""
13
Base AST visitor class with common functionality.
14
15
Provides factory methods and utilities for creating visitors
16
from source code or AST nodes.
17
"""
18
19
@classmethod
20
def from_code(cls, code, **kwargs):
21
"""
22
Create visitor instance and analyze source code.
23
24
Parameters:
25
- code (str): Python source code to analyze
26
- **kwargs: Arguments passed to visitor constructor
27
28
Returns:
29
CodeVisitor: Configured visitor instance after analysis
30
"""
31
32
@classmethod
33
def from_ast(cls, ast_node, **kwargs):
34
"""
35
Create visitor instance and analyze AST node.
36
37
Parameters:
38
- ast_node: Python AST node to analyze
39
- **kwargs: Arguments passed to visitor constructor
40
41
Returns:
42
CodeVisitor: Configured visitor instance after analysis
43
"""
44
45
@staticmethod
46
def get_name(obj):
47
"""
48
Extract name from AST node safely.
49
50
Parameters:
51
- obj: AST node object
52
53
Returns:
54
str: Node name or '<unknown>' if no name attribute
55
"""
56
57
def code2ast(source):
58
"""
59
Convert source code string to AST object.
60
61
Retained for backwards compatibility. Equivalent to ast.parse().
62
63
Parameters:
64
- source (str): Python source code
65
66
Returns:
67
ast.AST: Parsed AST tree
68
"""
69
```
70
71
### Complexity Analysis Visitor
72
73
Specialized visitor for calculating cyclomatic complexity of Python code.
74
75
```python { .api }
76
class ComplexityVisitor(CodeVisitor):
77
"""
78
AST visitor for cyclomatic complexity analysis.
79
80
Traverses AST nodes and calculates McCabe's cyclomatic complexity
81
for functions, methods, and classes.
82
"""
83
84
def __init__(self, to_method=False, classname=None, off=True, no_assert=False):
85
"""
86
Initialize complexity visitor.
87
88
Parameters:
89
- to_method (bool): Treat top-level functions as methods
90
- classname (str): Class name for method analysis context
91
- off (bool): Include decorator complexity in calculations
92
- no_assert (bool): Exclude assert statements from complexity
93
"""
94
95
# Properties
96
@property
97
def functions_complexity(self):
98
"""List of complexities for all functions/methods found."""
99
100
@property
101
def classes_complexity(self):
102
"""List of complexities for all classes found."""
103
104
@property
105
def total_complexity(self):
106
"""Total complexity across all functions and classes."""
107
108
@property
109
def blocks(self):
110
"""List of all Function and Class objects found."""
111
112
@property
113
def max_line(self):
114
"""Maximum line number encountered during analysis."""
115
116
# AST visit methods
117
def visit_FunctionDef(self, node):
118
"""Visit function definition and calculate complexity."""
119
120
def visit_AsyncFunctionDef(self, node):
121
"""Visit async function definition."""
122
123
def visit_ClassDef(self, node):
124
"""Visit class definition and analyze methods."""
125
126
def visit_Assert(self, node):
127
"""Visit assert statement (if not excluded)."""
128
```
129
130
### Halstead Metrics Visitor
131
132
Specialized visitor for calculating Halstead software science metrics.
133
134
```python { .api }
135
class HalsteadVisitor(CodeVisitor):
136
"""
137
AST visitor for Halstead metrics analysis.
138
139
Counts operators and operands to calculate software science metrics
140
including volume, difficulty, effort, and estimated bugs.
141
"""
142
143
def __init__(self, context=None):
144
"""
145
Initialize Halstead visitor.
146
147
Parameters:
148
- context: Analysis context (optional)
149
"""
150
151
# Properties
152
@property
153
def distinct_operators(self):
154
"""Set of distinct operators found in the code."""
155
156
@property
157
def distinct_operands(self):
158
"""Set of distinct operands found in the code."""
159
160
# AST visit methods for operators
161
def visit_BinOp(self, node):
162
"""Visit binary operation (e.g., +, -, *, /)."""
163
164
def visit_UnaryOp(self, node):
165
"""Visit unary operation (e.g., not, -, +)."""
166
167
def visit_Compare(self, node):
168
"""Visit comparison operation (e.g., <, >, ==)."""
169
170
def visit_BoolOp(self, node):
171
"""Visit boolean operation (and, or)."""
172
173
def visit_AugAssign(self, node):
174
"""Visit augmented assignment (+=, -=, etc.)."""
175
176
def visit_Name(self, node):
177
"""Visit name reference (variables, functions)."""
178
179
def visit_Num(self, node):
180
"""Visit numeric literal."""
181
182
def visit_Str(self, node):
183
"""Visit string literal."""
184
```
185
186
### Data Types for Analysis Results
187
188
Named tuples representing analyzed code structures.
189
190
```python { .api }
191
# Function/method representation
192
Function = namedtuple('Function', [
193
'name', # Function/method name
194
'lineno', # Starting line number
195
'col_offset', # Column offset
196
'endline', # Ending line number
197
'is_method', # Boolean: is this a method?
198
'classname', # Class name if method, None if function
199
'closures', # List of nested functions
200
'complexity' # Cyclomatic complexity score
201
])
202
203
# Additional Function properties:
204
@property
205
def letter(self):
206
"""Return 'M' for methods, 'F' for functions."""
207
208
@property
209
def fullname(self):
210
"""Return full name including class for methods."""
211
212
# Class representation
213
Class = namedtuple('Class', [
214
'name', # Class name
215
'lineno', # Starting line number
216
'col_offset', # Column offset
217
'endline', # Ending line number
218
'methods', # List of Method objects
219
'inner_classes', # List of nested Class objects
220
'real_complexity' # Class complexity score
221
])
222
223
# Additional Class properties:
224
@property
225
def complexity(self):
226
"""Average complexity of class methods plus one."""
227
228
@property
229
def fullname(self):
230
"""Return class name (for consistency with Function)."""
231
```
232
233
### Utility Constants
234
235
Helper functions and constants for working with analysis results.
236
237
```python { .api }
238
# Attribute getter functions
239
GET_COMPLEXITY = operator.attrgetter('complexity')
240
GET_REAL_COMPLEXITY = operator.attrgetter('real_complexity')
241
NAMES_GETTER = operator.attrgetter('name', 'asname')
242
GET_ENDLINE = operator.attrgetter('endline')
243
```
244
245
## Usage Examples
246
247
### Basic ComplexityVisitor Usage
248
249
```python
250
from radon.visitors import ComplexityVisitor
251
import ast
252
253
code = '''
254
class Calculator:
255
def add(self, a, b):
256
return a + b
257
258
def complex_divide(self, a, b):
259
if b == 0:
260
raise ValueError("Division by zero")
261
elif isinstance(a, str) or isinstance(b, str):
262
raise TypeError("Invalid type")
263
else:
264
return a / b
265
266
def standalone_function(x):
267
if x > 0:
268
return x * 2
269
else:
270
return 0
271
'''
272
273
# Method 1: Using class methods
274
visitor = ComplexityVisitor.from_code(code)
275
276
print(f"Total complexity: {visitor.total_complexity}")
277
print(f"Number of functions: {len(visitor.functions_complexity)}")
278
print(f"Number of classes: {len(visitor.classes_complexity)}")
279
280
# Method 2: Manual AST parsing
281
ast_tree = ast.parse(code)
282
manual_visitor = ComplexityVisitor()
283
manual_visitor.visit(ast_tree)
284
285
print("\nDetailed analysis:")
286
for block in manual_visitor.blocks:
287
print(f"{block.letter} {block.fullname}: {block.complexity}")
288
```
289
290
### Custom ComplexityVisitor Configuration
291
292
```python
293
from radon.visitors import ComplexityVisitor
294
295
class_code = '''
296
class DataProcessor:
297
@property
298
def status(self):
299
return self._status
300
301
def process(self, data):
302
assert data is not None
303
if not data:
304
return []
305
return [item.upper() for item in data if isinstance(item, str)]
306
'''
307
308
# Standard analysis
309
standard = ComplexityVisitor.from_code(class_code)
310
311
# Treat as standalone methods
312
as_methods = ComplexityVisitor.from_code(
313
class_code,
314
to_method=True,
315
classname="DataProcessor"
316
)
317
318
# Exclude assert statements
319
no_asserts = ComplexityVisitor.from_code(
320
class_code,
321
no_assert=True
322
)
323
324
print("Standard analysis:")
325
for block in standard.blocks:
326
print(f" {block.fullname}: {block.complexity}")
327
328
print("\nAs methods:")
329
for block in as_methods.blocks:
330
print(f" {block.fullname}: {block.complexity}")
331
332
print("\nExcluding asserts:")
333
for block in no_asserts.blocks:
334
print(f" {block.fullname}: {block.complexity}")
335
```
336
337
### HalsteadVisitor Usage
338
339
```python
340
from radon.visitors import HalsteadVisitor
341
import ast
342
343
code = '''
344
def calculate_interest(principal, rate, time):
345
if rate <= 0 or time <= 0:
346
return 0
347
return principal * (1 + rate) ** time
348
'''
349
350
# Analyze Halstead metrics
351
visitor = HalsteadVisitor.from_code(code)
352
353
print(f"Distinct operators: {len(visitor.distinct_operators)}")
354
print(f"Distinct operands: {len(visitor.distinct_operands)}")
355
print(f"Operators found: {sorted(visitor.distinct_operators)}")
356
print(f"Operands found: {sorted(visitor.distinct_operands)}")
357
358
# Manual visitor usage
359
ast_tree = ast.parse(code)
360
manual_visitor = HalsteadVisitor()
361
manual_visitor.visit(ast_tree)
362
363
print(f"\nManual analysis:")
364
print(f"Total operators: {len(list(manual_visitor.distinct_operators))}")
365
print(f"Total operands: {len(list(manual_visitor.distinct_operands))}")
366
```
367
368
### Working with Function and Class Objects
369
370
```python
371
from radon.visitors import ComplexityVisitor, GET_COMPLEXITY
372
import operator
373
374
code = '''
375
class WebService:
376
def __init__(self, url):
377
self.url = url
378
379
def get_data(self, endpoint):
380
if not endpoint:
381
raise ValueError("Endpoint required")
382
return f"{self.url}/{endpoint}"
383
384
def post_data(self, endpoint, data):
385
if not endpoint or not data:
386
raise ValueError("Endpoint and data required")
387
return f"POST {self.url}/{endpoint}"
388
389
class Config:
390
def __init__(self, timeout=30):
391
self.timeout = timeout
392
393
def helper_function():
394
pass
395
'''
396
397
visitor = ComplexityVisitor.from_code(code)
398
399
# Analyze functions vs methods
400
functions = [block for block in visitor.blocks if isinstance(block, type(visitor.blocks[0])) and not block.is_method]
401
methods = [block for block in visitor.blocks if isinstance(block, type(visitor.blocks[0])) and block.is_method]
402
403
print("Functions:")
404
for func in functions:
405
print(f" {func.name} (line {func.lineno}): {func.complexity}")
406
407
print("\nMethods:")
408
for method in methods:
409
print(f" {method.fullname} (line {method.lineno}): {method.complexity}")
410
411
# Use utility functions
412
complexities = [GET_COMPLEXITY(block) for block in visitor.blocks]
413
print(f"\nComplexities: {complexities}")
414
print(f"Max complexity: {max(complexities)}")
415
print(f"Average complexity: {sum(complexities) / len(complexities):.2f}")
416
```
417
418
### Extending Visitors for Custom Analysis
419
420
```python
421
from radon.visitors import ComplexityVisitor
422
import ast
423
424
class DetailedComplexityVisitor(ComplexityVisitor):
425
"""Extended complexity visitor with additional metrics."""
426
427
def __init__(self, *args, **kwargs):
428
super().__init__(*args, **kwargs)
429
self.loop_count = 0
430
self.condition_count = 0
431
self.exception_count = 0
432
433
def visit_For(self, node):
434
self.loop_count += 1
435
return super().visit_For(node)
436
437
def visit_While(self, node):
438
self.loop_count += 1
439
return super().visit_While(node)
440
441
def visit_If(self, node):
442
self.condition_count += 1
443
return super().visit_If(node)
444
445
def visit_Try(self, node):
446
self.exception_count += 1
447
return super().visit_Try(node)
448
449
code = '''
450
def process_data(items):
451
results = []
452
try:
453
for item in items:
454
if isinstance(item, dict):
455
if 'value' in item:
456
results.append(item['value'])
457
elif isinstance(item, list):
458
for sub_item in item:
459
if sub_item is not None:
460
results.append(sub_item)
461
except Exception as e:
462
return []
463
return results
464
'''
465
466
visitor = DetailedComplexityVisitor.from_code(code)
467
468
print(f"Complexity: {visitor.total_complexity}")
469
print(f"Loops: {visitor.loop_count}")
470
print(f"Conditions: {visitor.condition_count}")
471
print(f"Exception handlers: {visitor.exception_count}")
472
```
473
474
## Advanced Usage Patterns
475
476
### Analyzing Code Fragments
477
478
```python
479
from radon.visitors import ComplexityVisitor, code2ast
480
481
# Analyze code fragments without full function definitions
482
fragment = '''
483
if condition:
484
for item in items:
485
if item.valid:
486
process(item)
487
else:
488
skip(item)
489
else:
490
handle_empty()
491
'''
492
493
# Wrap in function for analysis
494
wrapped = f"def fragment():\n" + "\n".join(f" {line}" for line in fragment.split('\n'))
495
496
ast_tree = code2ast(wrapped)
497
visitor = ComplexityVisitor()
498
visitor.visit(ast_tree)
499
500
print(f"Fragment complexity: {visitor.total_complexity}")
501
```
502
503
### Batch Analysis with Visitors
504
505
```python
506
from radon.visitors import ComplexityVisitor
507
import os
508
509
def analyze_project(directory):
510
"""Analyze all Python files in a directory."""
511
total_complexity = 0
512
total_functions = 0
513
514
for filename in os.listdir(directory):
515
if filename.endswith('.py'):
516
try:
517
with open(os.path.join(directory, filename)) as f:
518
code = f.read()
519
520
visitor = ComplexityVisitor.from_code(code)
521
file_complexity = visitor.total_complexity
522
file_functions = len(visitor.blocks)
523
524
total_complexity += file_complexity
525
total_functions += file_functions
526
527
print(f"{filename}: {file_complexity} complexity, {file_functions} functions")
528
529
except Exception as e:
530
print(f"Error analyzing {filename}: {e}")
531
532
if total_functions > 0:
533
avg_complexity = total_complexity / total_functions
534
print(f"\nProject summary:")
535
print(f" Total complexity: {total_complexity}")
536
print(f" Total functions: {total_functions}")
537
print(f" Average complexity: {avg_complexity:.2f}")
538
539
# Usage:
540
# analyze_project('./src')
541
```
542
543
## Error Handling
544
545
The visitor classes handle various edge cases:
546
547
- **Empty code**: Returns empty analysis results
548
- **Syntax errors**: Propagates AST parsing exceptions
549
- **Invalid AST nodes**: Safely handles missing attributes
550
- **Circular references**: Prevents infinite recursion in nested structures
551
552
## Integration with Higher-Level APIs
553
554
The visitor classes serve as the foundation for radon's higher-level APIs:
555
556
```python
557
# High-level functions use visitors internally
558
from radon.complexity import cc_visit
559
from radon.metrics import h_visit
560
561
# These functions create and use visitors automatically
562
blocks = cc_visit(code) # Uses ComplexityVisitor internally
563
halstead = h_visit(code) # Uses HalsteadVisitor internally
564
565
# You can access the same functionality directly
566
from radon.visitors import ComplexityVisitor, HalsteadVisitor
567
568
complexity_visitor = ComplexityVisitor.from_code(code)
569
halstead_visitor = HalsteadVisitor.from_code(code)
570
```
571
572
This low-level access allows for custom analysis, extended functionality, and integration with other static analysis tools.