or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

cli.mdcomplexity.mdhalstead.mdindex.mdmaintainability.mdraw-metrics.mdvisitors.md

visitors.mddocs/

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.