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
```