or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

built-in-checkers.mdchecker-development.mdconfiguration.mdcore-linting.mdextensions.mdindex.mdmessages.mdpyreverse.mdreporters.mdtest-utilities.md

checker-development.mddocs/

0

# Checker Development

1

2

Framework for creating custom checkers that analyze specific code patterns. Pylint's checker system is built on a flexible architecture that allows developers to create specialized analysis tools for their specific coding standards and requirements.

3

4

**Note**: Pylint checkers work with AST nodes provided by the `astroid` library (pylint's dependency). References to `astroid.Node`, `astroid.parse()`, etc. in the examples below refer to this external library.

5

6

## Capabilities

7

8

### Base Checker Classes

9

10

Foundation classes that provide the interface and common functionality for all checkers.

11

12

```python { .api }

13

class BaseChecker:

14

"""

15

Abstract base class for all checkers.

16

17

Attributes:

18

name (str): Unique name identifying the checker

19

msgs (dict): Message definitions with format: {

20

'message-id': (

21

'message-text',

22

'message-symbol',

23

'description'

24

)

25

}

26

options (tuple): Configuration options for the checker

27

reports (tuple): Report definitions

28

priority (int): Checker priority (lower runs first)

29

"""

30

31

name: str

32

msgs: dict

33

options: tuple

34

reports: tuple

35

priority: int

36

37

def __init__(self, linter=None):

38

"""

39

Initialize the checker.

40

41

Args:

42

linter: PyLinter instance this checker belongs to

43

"""

44

45

def open(self):

46

"""Called before checking begins."""

47

48

def close(self):

49

"""Called after all checking is complete."""

50

```

51

52

### Specialized Base Classes

53

54

Specialized base classes for different types of code analysis.

55

56

```python { .api }

57

class BaseRawFileChecker(BaseChecker):

58

"""

59

Base class for checkers that process raw file content.

60

61

Used for checkers that need to analyze the file as text

62

rather than parsed AST (e.g., encoding, line length).

63

"""

64

65

def process_module(self, astroid_module):

66

"""

67

Process a module's raw content.

68

69

Args:

70

astroid_module: Astroid module node

71

"""

72

73

class BaseTokenChecker(BaseChecker):

74

"""

75

Base class for checkers that process tokens.

76

77

Used for checkers that analyze code at the token level

78

(e.g., formatting, whitespace, comments).

79

"""

80

81

def process_tokens(self, tokens):

82

"""

83

Process tokens from the module.

84

85

Args:

86

tokens: List of token tuples (type, string, start, end, line)

87

"""

88

```

89

90

### AST Node Checkers

91

92

Most checkers inherit from BaseChecker and implement visit methods for AST nodes.

93

94

```python { .api }

95

# Example AST checker pattern

96

class CustomChecker(BaseChecker):

97

"""Custom checker examining function definitions."""

98

99

name = 'custom'

100

msgs = {

101

'C9999': (

102

'Custom message: %s',

103

'custom-message',

104

'Description of the custom check'

105

)

106

}

107

108

def visit_functiondef(self, node):

109

"""Visit function definition nodes."""

110

# Analysis logic here

111

if some_condition:

112

self.add_message('custom-message', node=node, args=(info,))

113

114

def visit_classdef(self, node):

115

"""Visit class definition nodes."""

116

pass

117

118

def leave_functiondef(self, node):

119

"""Called when leaving function definition nodes."""

120

pass

121

```

122

123

### Message Definition System

124

125

System for defining and managing checker messages with categories and formatting.

126

127

```python { .api }

128

# Message format structure

129

MSG_FORMAT = {

130

'message-id': (

131

'message-template', # Template with %s placeholders

132

'message-symbol', # Symbolic name for the message

133

'description' # Detailed description

134

)

135

}

136

137

# Message categories

138

MESSAGE_CATEGORIES = {

139

'C': 'convention', # Coding standard violations

140

'R': 'refactor', # Refactoring suggestions

141

'W': 'warning', # Potential issues

142

'E': 'error', # Probable bugs

143

'F': 'fatal', # Errors preventing further processing

144

'I': 'info' # Informational messages

145

}

146

```

147

148

### Options System

149

150

Configuration system for checker-specific options.

151

152

```python { .api }

153

# Options format

154

OPTIONS_FORMAT = (

155

'option-name', # Command line option name

156

{

157

'default': value, # Default value

158

'type': 'string', # Type: string, int, float, choice, yn, csv

159

'metavar': '<value>',# Help text placeholder

160

'help': 'Description of the option',

161

'choices': ['a','b'] # For choice type options

162

}

163

)

164

165

# Example options definition

166

options = (

167

('max-complexity', {

168

'default': 10,

169

'type': 'int',

170

'metavar': '<int>',

171

'help': 'Maximum allowed complexity score'

172

}),

173

('ignore-patterns', {

174

'default': [],

175

'type': 'csv',

176

'metavar': '<pattern>',

177

'help': 'Comma-separated list of patterns to ignore'

178

})

179

)

180

```

181

182

### Checker Registration

183

184

Functions and patterns for registering checkers with the PyLinter.

185

186

```python { .api }

187

def register(linter):

188

"""

189

Register checker with linter.

190

191

This function is called by pylint when loading the checker.

192

193

Args:

194

linter: PyLinter instance to register with

195

"""

196

linter.register_checker(MyCustomChecker(linter))

197

198

def initialize(linter):

199

"""

200

Alternative registration function name.

201

202

Some checkers use this name instead of register().

203

"""

204

register(linter)

205

```

206

207

## Usage Examples

208

209

### Simple Custom Checker

210

211

```python

212

from pylint.checkers import BaseChecker

213

from pylint.interfaces import IAstroidChecker

214

215

class FunctionNamingChecker(BaseChecker):

216

"""Check that function names follow naming conventions."""

217

218

__implements__ = IAstroidChecker

219

220

name = 'function-naming'

221

msgs = {

222

'C9001': (

223

'Function name "%s" should start with verb',

224

'function-name-should-start-with-verb',

225

'Function names should start with an action verb'

226

)

227

}

228

229

options = (

230

('required-function-prefixes', {

231

'default': ['get', 'set', 'is', 'has', 'create', 'update', 'delete'],

232

'type': 'csv',

233

'help': 'Required prefixes for function names'

234

}),

235

)

236

237

def visit_functiondef(self, node):

238

"""Check function name starts with approved verb."""

239

func_name = node.name

240

prefixes = self.config.required_function_prefixes

241

242

if not any(func_name.startswith(prefix) for prefix in prefixes):

243

self.add_message(

244

'function-name-should-start-with-verb',

245

node=node,

246

args=(func_name,)

247

)

248

249

def register(linter):

250

"""Register the checker with pylint."""

251

linter.register_checker(FunctionNamingChecker(linter))

252

```

253

254

### Token-Based Checker

255

256

```python

257

from pylint.checkers import BaseTokenChecker

258

import tokenize

259

260

class CommentStyleChecker(BaseTokenChecker):

261

"""Check comment formatting style."""

262

263

name = 'comment-style'

264

msgs = {

265

'C9002': (

266

'Comment should have space after #',

267

'comment-no-space',

268

'Comments should have a space after the # character'

269

)

270

}

271

272

def process_tokens(self, tokens):

273

"""Process tokens to check comment formatting."""

274

for token in tokens:

275

if token.type == tokenize.COMMENT:

276

comment_text = token.string

277

if len(comment_text) > 1 and comment_text[1] != ' ':

278

self.add_message(

279

'comment-no-space',

280

line=token.start[0],

281

col_offset=token.start[1]

282

)

283

284

def register(linter):

285

linter.register_checker(CommentStyleChecker(linter))

286

```

287

288

### Raw File Checker

289

290

```python

291

from pylint.checkers import BaseRawFileChecker

292

293

class FileHeaderChecker(BaseRawFileChecker):

294

"""Check for required file headers."""

295

296

name = 'file-header'

297

msgs = {

298

'C9003': (

299

'Missing required copyright header',

300

'missing-copyright-header',

301

'All files should contain a copyright header'

302

)

303

}

304

305

options = (

306

('required-header-pattern', {

307

'default': r'# Copyright \d{4}',

308

'type': 'string',

309

'help': 'Regex pattern for required header'

310

}),

311

)

312

313

def process_module(self, astroid_module):

314

"""Check module for required header."""

315

with open(astroid_module.file, 'r', encoding='utf-8') as f:

316

content = f.read()

317

318

import re

319

pattern = self.config.required_header_pattern

320

if not re.search(pattern, content[:500]): # Check first 500 chars

321

self.add_message('missing-copyright-header', line=1)

322

323

def register(linter):

324

linter.register_checker(FileHeaderChecker(linter))

325

```

326

327

### Complex Checker with State

328

329

```python

330

from pylint.checkers import BaseChecker

331

import astroid

332

333

class VariableUsageChecker(BaseChecker):

334

"""Track variable usage patterns."""

335

336

name = 'variable-usage'

337

msgs = {

338

'W9001': (

339

'Variable "%s" assigned but never used in function',

340

'unused-function-variable',

341

'Variables should be used after assignment'

342

)

343

}

344

345

def __init__(self, linter=None):

346

super().__init__(linter)

347

self._function_vars = {}

348

self._current_function = None

349

350

def visit_functiondef(self, node):

351

"""Enter function scope."""

352

self._current_function = node.name

353

self._function_vars[node.name] = {

354

'assigned': set(),

355

'used': set()

356

}

357

358

def visit_assign(self, node):

359

"""Track variable assignments."""

360

if self._current_function:

361

for target in node.targets:

362

if isinstance(target, astroid.AssignName):

363

self._function_vars[self._current_function]['assigned'].add(

364

target.name

365

)

366

367

def visit_name(self, node):

368

"""Track variable usage."""

369

if self._current_function and isinstance(node.ctx, astroid.Load):

370

self._function_vars[self._current_function]['used'].add(node.name)

371

372

def leave_functiondef(self, node):

373

"""Check for unused variables when leaving function."""

374

if self._current_function:

375

vars_info = self._function_vars[self._current_function]

376

unused = vars_info['assigned'] - vars_info['used']

377

378

for var_name in unused:

379

self.add_message(

380

'unused-function-variable',

381

node=node,

382

args=(var_name,)

383

)

384

385

self._current_function = None

386

387

def register(linter):

388

linter.register_checker(VariableUsageChecker(linter))

389

```

390

391

## Testing Custom Checkers

392

393

```python

394

from pylint.testutils import CheckerTestCase, MessageTest

395

396

class TestFunctionNamingChecker(CheckerTestCase):

397

"""Test cases for function naming checker."""

398

399

CHECKER_CLASS = FunctionNamingChecker

400

401

def test_function_with_verb_prefix(self):

402

"""Test that functions with verb prefixes pass."""

403

code = '''

404

def get_value():

405

pass

406

407

def create_user():

408

pass

409

'''

410

with self.assertNoMessages():

411

self.walk(astroid.parse(code))

412

413

def test_function_without_verb_prefix(self):

414

"""Test that functions without verb prefixes fail."""

415

code = '''

416

def value(): # Should trigger warning

417

pass

418

'''

419

message = MessageTest(

420

'function-name-should-start-with-verb',

421

node='value',

422

args=('value',)

423

)

424

with self.assertAddsMessages(message):

425

self.walk(astroid.parse(code))

426

```