• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pyta-uoft / pyta / 26365916656

24 May 2026 03:57PM UTC coverage: 90.533% (-0.3%) from 90.843%
26365916656

Pull #1339

github

web-flow
Merge e407981e7 into cbf34b3c5
Pull Request #1339: Modify SnapshotTracer to generate JSON

3548 of 3919 relevant lines covered (90.53%)

17.57 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.25
/packages/python-ta/src/python_ta/checkers/invalid_name_checker.py
1
"""Checker used for identifying names that don't conform to Python naming conventions."""
2

3
from __future__ import annotations
20✔
4

5
import re
20✔
6
from typing import TYPE_CHECKING, Optional
20✔
7

8
from astroid import nodes
20✔
9
from pylint.checkers import BaseChecker, utils
20✔
10
from pylint.checkers.base.name_checker.checker import _redefines_import
20✔
11
from pylint.checkers.utils import only_required_for_messages
20✔
12

13
from python_ta.utils import _is_in_main
20✔
14

15
if TYPE_CHECKING:
16
    from pylint.lint import PyLinter
17

18
# Bad variable names.
19
BAD_NAMES = {"l", "I", "O"}
20✔
20

21
# Set a limit in name length to keep certain variable names short.
22
VAR_NAME_LENGTHS = {
20✔
23
    "module": 30,
24
    "constant": 30,
25
    "class": 30,
26
    "function": 30,
27
    "method": 30,
28
    "attribute": 30,
29
    "argument": 30,
30
    "variable": 30,
31
    "class attribute": 30,
32
    "class constant": 30,
33
    "type variable": 20,
34
    "type alias": 20,
35
}
36

37
TYPE_VAR_QNAME = frozenset(
20✔
38
    (
39
        "typing.TypeVar",
40
        "typing_extensions.TypeVar",
41
    )
42
)
43

44

45
def _is_in_snake_case(name: str) -> bool:
20✔
46
    """Returns whether `name` is in snake_case.
47

48
    `name` is in snake_case if:
49
      - `name` starts with a lowercase letter or an underscore (to denote private fields) followed
50
        by a lowercase letter,
51
      - each word is separated by an underscore, and
52
      - each word is in lowercase.
53
    """
54
    pattern = "(_?[a-z][a-z0-9_]*)$"
20✔
55

56
    return re.match(pattern, name) is not None
20✔
57

58

59
def _is_in_pascal_case(name: str) -> bool:
20✔
60
    """Returns whether `name` is in PascalCase.
61

62
    `name` is in PascalCase if:
63
      - `name` starts with an uppercase letter or an underscore (to denote private fields) followed
64
        by an uppercase letter.
65
      - each word has its first character capitalized, and
66
      - there is no whitespace, underscore, or punctuation between words.
67
    """
68
    pattern = "(_?[A-Z][a-zA-Z0-9]*)$"
20✔
69

70
    return re.match(pattern, name) is not None
20✔
71

72

73
def _is_in_upper_case_with_underscores(name: str) -> bool:
20✔
74
    """Returns whether `name` is in UPPER_CASE_WITH_UNDERSCORES.
75

76
    `name` is in `UPPER_CASE_WITH_UNDERSCORES` if:
77
      - each word is in uppercase, and
78
      - words are separated by an underscore.
79
    """
80
    pattern = "(_?[A-Z][A-Z0-9_]*)$"
20✔
81

82
    return re.match(pattern, name) is not None
20✔
83

84

85
def _parse_name(name: str) -> tuple[str, list[str] | None, str]:
20✔
86
    """Extracts the prefix, words, and suffix from `name`."""
87
    name_match = re.match(r"(_*)(.*?)(_*)$", name)
20✔
88
    if not name_match:
20✔
89
        return "", None, ""
×
90
    prefix, core, suffix = name_match.groups()
20✔
91
    prefix = "_" if prefix else ""
20✔
92
    if core and core[0].isdigit():
20✔
93
        return "", None, ""
×
94
    core = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", core)
20✔
95
    core = re.sub(r"([A-Z])([A-Z][a-z])", r"\1_\2", core)
20✔
96

97
    return prefix, [word for word in core.split("_") if word], suffix
20✔
98

99

100
def _to_pascal_case(name: str) -> str | None:
20✔
101
    """Returns a PascalCase version of `name`."""
102
    prefix, words, _ = _parse_name(name)
20✔
103
    if words is None:
20✔
104
        return None
×
105

106
    return prefix + "".join(word[0].upper() + word[1:] for word in words)
20✔
107

108

109
def _to_upper_case_with_underscores(name: str) -> str | None:
20✔
110
    """Returns an UPPER_CASE_WITH_UNDERSCORES version of `name`."""
111
    prefix, words, suffix = _parse_name(name)
20✔
112
    if words is None:
20✔
113
        return None
×
114

115
    return prefix + "_".join(word.upper() for word in words) + suffix
20✔
116

117

118
def _is_bad_name(name: str) -> str:
20✔
119
    """Returns a string detailing why `name` is a bad name.
120

121
    `name` is a bad name if it is in the pre-determined collection of "bad names".
122

123
    Returns the empty string if `name` is not a bad name."""
124
    msg = ""
20✔
125

126
    if name in BAD_NAMES:
20✔
127
        msg = (
20✔
128
            f'"{name}" is a name that should be avoided. Change to something less ambiguous '
129
            f"and/or more descriptive."
130
        )
131

132
    return msg
20✔
133

134

135
def _is_within_name_length(node_type: str, name: str) -> str:
20✔
136
    """Returns a string saying that `name` exceeds the character limit for that variable name type.
137

138
    Returns the empty string if `name` is within the name length limit."""
139
    msg = ""
20✔
140
    name_length_limit = VAR_NAME_LENGTHS[node_type]
20✔
141

142
    if len(name) > name_length_limit:
20✔
143
        msg = (
20✔
144
            f'{node_type.capitalize()} name "{name}" exceeds the limit of {name_length_limit} '
145
            f"characters."
146
        )
147

148
    return msg
20✔
149

150

151
def _ignore_name(name: str, pattern: re.Pattern) -> bool:
20✔
152
    """Returns whether name matches any of the regular expressions provided in patterns"""
153
    return pattern.pattern and pattern.match(name) is not None
20✔
154

155

156
def _check_module_name(_node_type: str, name: str) -> list[str]:
20✔
157
    """Returns a list of strings, each detailing how `name` violates Python naming conventions for
158
    module names.
159

160
    Returns an empty list if `name` is a valid module name."""
161
    error_msgs = []
20✔
162

163
    if not _is_in_snake_case(name):
20✔
164
        error_msgs.append(
20✔
165
            f'Module name "{name}" should be in snake_case format. Modules should be all-lowercase '
166
            f"names, with each name separated by underscores."
167
        )
168

169
    return error_msgs
20✔
170

171

172
def _check_const_name(node_type: str, name: str) -> list[str]:
20✔
173
    """Returns a list of strings, each detailing how `name` violates Python naming conventions for
174
    constant and class constant names and provides a suggested correction.
175

176
    Returns an empty list if `name` is a valid (global or class) constant name."""
177
    error_msgs = []
20✔
178

179
    if not _is_in_upper_case_with_underscores(name):
20✔
180
        suggested_name = _to_upper_case_with_underscores(name)
20✔
181
        msg = f'{node_type.capitalize()} name "{name}" should be in UPPER_CASE_WITH_UNDERSCORES format. '
20✔
182
        if suggested_name:
20✔
183
            msg += f'Suggested fix: "{suggested_name}". '
20✔
184
        msg += (
20✔
185
            "Constants should be all-uppercase words with each word separated by an "
186
            "underscore. A single leading underscore can be used to denote a private constant."
187
        )
188
        if node_type == "class constant":
20✔
189
            msg += " A double leading underscore invokes Python's name-mangling rules."
20✔
190
        error_msgs.append(msg)
20✔
191

192
    return error_msgs
20✔
193

194

195
def _check_class_name(_node_type: str, name: str) -> list[str]:
20✔
196
    """Returns a list of strings, each detailing how `name` violates Python naming conventions for
197
    class names and provides a suggested correction.
198

199
    Returns an empty list if `name` is a valid class name."""
200
    error_msgs = []
20✔
201

202
    if not _is_in_pascal_case(name):
20✔
203
        suggested_name = _to_pascal_case(name)
20✔
204
        msg = f'Class name "{name}" should be in PascalCase format. '
20✔
205
        if suggested_name:
20✔
206
            msg += f'Suggested fix: "{suggested_name}". '
20✔
207
        msg += (
20✔
208
            "Class names should have the first letter of each word capitalized with no separation "
209
            "between each word. A single leading underscore can be used to denote a private class."
210
        )
211
        error_msgs.append(msg)
20✔
212

213
    return error_msgs
20✔
214

215

216
def _check_function_and_variable_name(node_type: str, name: str) -> list[str]:
20✔
217
    """Returns a list of strings, each detailing how `name` violates Python naming conventions for
218
    function and variable names.
219

220
    Returns an empty list if `name` is a valid function or variable name."""
221
    error_msgs = []
20✔
222

223
    if name != "_" and not _is_in_snake_case(name):
20✔
224
        error_msgs.append(
20✔
225
            f'{node_type.capitalize()} name "{name}" should be in snake_case format. '
226
            f"{node_type.capitalize()} names should be lowercase, with words "
227
            f"separated by underscores. A single leading underscore can be used to "
228
            f"denote a private {node_type}."
229
        )
230

231
    return error_msgs
20✔
232

233

234
def _check_method_and_attr_name(node_type: str, name: str) -> list[str]:
20✔
235
    """Returns a list of strings, each detailing how `name` violates Python naming conventions for
236
    method and instance or class attribute names.
237

238
    Returns an empty list if `name` is a valid method, instance, or attribute name."""
239
    error_msgs = []
20✔
240

241
    # Also consider the case of invoking Python's name mangling rules with leading dunderscores.
242
    if not (_is_in_snake_case(name) or (name.startswith("__") and _is_in_snake_case(name[2:]))):
20✔
243
        error_msgs.append(
20✔
244
            f'{node_type.capitalize()} name "{name}" should be in snake_case format. '
245
            f"{node_type.capitalize()} names should be lowercase, with words "
246
            f"separated by underscores. A single leading underscore can be used to "
247
            f"denote a private {node_type} while a double leading underscore invokes "
248
            f"Python's name-mangling rules."
249
        )
250

251
    return error_msgs
20✔
252

253

254
def _check_argument_name(_node_type: str, name: str) -> list[str]:
20✔
255
    """Returns a list of strings, each detailing how `name` violates Python naming conventions for
256
    argument names.
257

258
    Returns an empty list if `name` is a valid argument name."""
259
    error_msgs = []
20✔
260

261
    if not _is_in_snake_case(name):
20✔
262
        error_msgs.append(
20✔
263
            f'Argument name "{name}" should be in snake_case format. Argument names should be '
264
            f"lowercase, with words separated by underscores. A single leading "
265
            f"underscore can be used to indicate that the argument is not being used "
266
            f"but is still needed somehow."
267
        )
268

269
    return error_msgs
20✔
270

271

272
def _check_typevar_name(_node_type: str, name: str) -> list[str]:
20✔
273
    """Returns a list of strings, each detailing how `name` violates Python naming conventions for
274
    type variable names and provides a suggested correction.
275

276
    Returns an empty list if `name` is a valid type variable name."""
277
    error_msgs = []
20✔
278

279
    if not _is_in_pascal_case(name):
20✔
280
        suggested_name = _to_pascal_case(name)
20✔
281
        msg = f'Type variable name "{name}" should be in PascalCase format. '
20✔
282
        if suggested_name:
20✔
283
            msg += f'Suggested fix: "{suggested_name}". '
20✔
284
        msg += (
20✔
285
            "Type variable names should have the first letter of each word "
286
            "capitalized with no separation between each word."
287
        )
288
        error_msgs.append(msg)
20✔
289

290
    return error_msgs
20✔
291

292

293
def _check_type_alias_name(_node_type: str, name: str) -> list[str]:
20✔
294
    """Returns a list of strings, each detailing how `name` violates Python naming conventions for
295
    type alias names and provides a suggested correction.
296

297
    Returns an empty list if `name` is a valid type alias name."""
298
    error_msgs = []
20✔
299

300
    if not _is_in_pascal_case(name):
20✔
301
        suggested_name = _to_pascal_case(name)
20✔
302
        msg = f'Type alias name "{name}" should be in PascalCase format. '
20✔
303
        if suggested_name:
20✔
304
            msg += f'Suggested fix: "{suggested_name}". '
20✔
305
        msg += (
20✔
306
            "Type alias names should have the first letter of each word "
307
            "capitalized with no separation between each word."
308
        )
309
        error_msgs.append(msg)
20✔
310

311
    return error_msgs
20✔
312

313

314
# Map each variable name type to its corresponding check
315
NAME_CHECK = {
20✔
316
    "module": _check_module_name,
317
    "constant": _check_const_name,
318
    "class": _check_class_name,
319
    "function": _check_function_and_variable_name,
320
    "method": _check_method_and_attr_name,
321
    "attribute": _check_method_and_attr_name,
322
    "argument": _check_argument_name,
323
    "variable": _check_function_and_variable_name,
324
    "class attribute": _check_method_and_attr_name,
325
    "class constant": _check_const_name,
326
    "type variable": _check_typevar_name,
327
    "type alias": _check_type_alias_name,
328
}
329

330

331
class InvalidNameChecker(BaseChecker):
20✔
332
    """A checker class to report on names that don't conform to Python naming conventions.
333

334
    For the Python naming conventions, see https://peps.python.org/pep-0008/#naming-conventions.
335
    """
336

337
    name = "naming_convention_violation"
20✔
338
    msgs = {
20✔
339
        "C9103": (
340
            "%s",
341
            "naming-convention-violation",
342
            "Used when the name doesn't conform to standard Python naming conventions.",
343
        ),
344
        "C9104": (
345
            "%s",
346
            "module-name-violation",
347
            "Used when the name doesn't conform to standard Python naming conventions.",
348
        ),
349
    }
350
    options = (
20✔
351
        (
352
            "ignore-names",
353
            {
354
                "default": "",
355
                "type": "regexp",
356
                "metavar": "<regexp>",
357
                "help": "Ignore C9103 naming convention violation for names that exactly match the pattern",
358
            },
359
        ),
360
        (
361
            "ignore-module-names",
362
            {
363
                "default": "",
364
                "type": "regexp",
365
                "metavar": "<regexp>",
366
                "help": "Ignore C9104 module name violation for module names that exactly match the pattern",
367
            },
368
        ),
369
    )
370

371
    @only_required_for_messages("module-name-violation")
20✔
372
    def visit_module(self, node: nodes.Module) -> None:
20✔
373
        """Visit a Module node to check for any name violations.
374

375
        Snippets taken from pylint.checkers.base.name_checker.checker."""
376
        if not _ignore_name(node.name, self.linter.config.ignore_module_names):
20✔
377
            self._check_name("module", node.name.split(".")[-1], node)
20✔
378

379
    @only_required_for_messages("naming-convention-violation")
20✔
380
    def visit_classdef(self, node: nodes.ClassDef) -> None:
20✔
381
        """Visit a Class node to check for any name violations.
382

383
        Taken from pylint.checkers.base.name_checker.checker."""
384
        if not _ignore_name(node.name, self.linter.config.ignore_names):
20✔
385
            self._check_name("class", node.name, node)
20✔
386

387
    @only_required_for_messages("naming-convention-violation")
20✔
388
    def visit_functiondef(self, node: nodes.FunctionDef) -> None:
20✔
389
        """Visit a FunctionDef node to check for any name violations.
390

391
        Snippets taken from pylint.checkers.base.name_checker.checker."""
392
        if node.is_method():
20✔
393
            if utils.overrides_a_method(node.parent.frame(future=True), node.name):
20✔
394
                return
20✔
395

396
        if not _ignore_name(node.name, self.linter.config.ignore_names):
20✔
397
            self._check_name("method" if node.is_method() else "function", node.name, node)
20✔
398

399
    visit_asyncfunctiondef = visit_functiondef
20✔
400

401
    @only_required_for_messages("naming-convention-violation")
20✔
402
    def visit_assignname(self, node: nodes.AssignName) -> None:
20✔
403
        """Visit an AssignName node to check for any name violations.
404

405
        Taken from pylint.checkers.base.name_checker.checker."""
406
        # Do not check this node if included in the ignore-names option
407
        if _ignore_name(node.name, self.linter.config.ignore_names):
20✔
408
            return
20✔
409

410
        frame = node.frame()
20✔
411
        assign_type = node.assign_type()
20✔
412

413
        # Check names defined in comprehensions
414
        if isinstance(assign_type, nodes.Comprehension):
20✔
415
            self._check_name("variable", node.name, node)
20✔
416

417
        # Check names defined as function arguments.
418
        elif isinstance(assign_type, nodes.Arguments):
20✔
419
            self._check_name("argument", node.name, node)
20✔
420

421
        # Check names defined in module scope
422
        elif isinstance(frame, nodes.Module) and not _is_in_main(node):
20✔
423
            # Check names defined in Assign nodes
424
            if isinstance(assign_type, nodes.Assign):
20✔
425
                inferred_assign_type = utils.safe_infer(assign_type.value)
20✔
426

427
                # Check TypeVar's and TypeAliases assigned alone or in tuple assignment
428
                if isinstance(node.parent, nodes.Assign):
20✔
429
                    if self._assigns_typevar(assign_type.value):
20✔
430
                        self._check_name("type variable", assign_type.targets[0].name, node)
20✔
431
                        return
20✔
432
                    if self._assigns_typealias(assign_type.value):
20✔
433
                        self._check_name("type alias", assign_type.targets[0].name, node)
20✔
434
                        return
20✔
435

436
                if (
20✔
437
                    isinstance(node.parent, nodes.Tuple)
438
                    and isinstance(assign_type.value, nodes.Tuple)
439
                    # protect against unbalanced tuple unpacking
440
                    and node.parent.elts.index(node) < len(assign_type.value.elts)
441
                ):
442
                    assigner = assign_type.value.elts[node.parent.elts.index(node)]
20✔
443
                    if self._assigns_typevar(assigner):
20✔
444
                        self._check_name(
20✔
445
                            "type variable",
446
                            assign_type.targets[0].elts[node.parent.elts.index(node)].name,
447
                            node,
448
                        )
449
                        return
20✔
450
                    if self._assigns_typealias(assigner):
20✔
451
                        self._check_name(
20✔
452
                            "type alias",
453
                            assign_type.targets[0].elts[node.parent.elts.index(node)].name,
454
                            node,
455
                        )
456
                        return
20✔
457

458
                # Check classes (TypeVar's are classes so they need to be excluded first)
459
                elif isinstance(inferred_assign_type, nodes.ClassDef):
20✔
460
                    self._check_name("class", node.name, node)
20✔
461

462
                # Don't emit if the name redefines an import in an ImportError except handler.
463
                elif not _redefines_import(node):
20✔
464
                    self._check_name("constant", node.name, node)
20✔
465
                else:
466
                    self._check_name("variable", node.name, node)
20✔
467

468
            # Check names defined in AnnAssign nodes
469
            elif isinstance(assign_type, nodes.AnnAssign):
20✔
470
                if utils.is_assign_name_annotated_with(node, "Final"):
20✔
471
                    self._check_name("constant", node.name, node)
20✔
472
                elif self._assigns_typealias(assign_type.annotation):
20✔
473
                    self._check_name("type alias", node.name, node)
20✔
474

475
        # Check names defined in function scopes
476
        elif isinstance(frame, nodes.FunctionDef):
20✔
477
            # global introduced variable aren't in the function locals
478
            if node.name in frame and node.name not in frame.argnames():
20✔
479
                if not _redefines_import(node):
20✔
480
                    self._check_name("variable", node.name, node)
20✔
481

482
        # Check names defined in class scopes
483
        elif isinstance(frame, nodes.ClassDef):
20✔
484
            if not list(frame.local_attr_ancestors(node.name)):
20✔
485
                for ancestor in frame.ancestors():
20✔
486
                    if utils.is_enum(ancestor) or utils.is_assign_name_annotated_with(
20✔
487
                        node, "Final"
488
                    ):
489
                        self._check_name("class constant", node.name, node)
20✔
490
                        break
20✔
491
                    elif utils.is_assign_name_annotated_with(node, "ClassVar"):
20✔
492
                        self._check_name("class attribute", node.name, node)
20✔
493
                        break
20✔
494
                    elif isinstance(node.parent, nodes.AnnAssign):
20✔
495
                        self._check_name("attribute", node.name, node)
20✔
496
                        break
20✔
497
                else:
498
                    self._check_name("class attribute", node.name, node)
20✔
499

500
    def _check_name(self, node_type: str, name: str, node: nodes.NodeNG) -> None:
20✔
501
        """Check for a name that violates Python naming conventions."""
502
        name_check = NAME_CHECK[node_type]
20✔
503
        error_msgs = name_check(node_type, name)
20✔
504

505
        bad_name_msg = _is_bad_name(name)
20✔
506
        if bad_name_msg:
20✔
507
            error_msgs.append(bad_name_msg)
20✔
508

509
        name_length_msg = _is_within_name_length(node_type, name)
20✔
510
        if name_length_msg:
20✔
511
            error_msgs.append(name_length_msg)
20✔
512

513
        msg_id = "naming-convention-violation" if node_type != "module" else "module-name-violation"
20✔
514
        line_no = 1 if node_type == "module" else None
20✔
515

516
        for msg in error_msgs:
20✔
517
            self.add_message(msgid=msg_id, node=node, args=msg, line=line_no)
20✔
518

519
    @staticmethod
20✔
520
    def _assigns_typevar(node: Optional[nodes.NodeNG]) -> bool:
20✔
521
        """Check if a node is assigning a TypeVar.
522

523
        Taken from pylint.checkers.base.name_checker.checker."""
524
        if isinstance(node, nodes.Call):
20✔
525
            inferred = utils.safe_infer(node.func)
20✔
526
            if isinstance(inferred, nodes.ClassDef) and inferred.qname() in TYPE_VAR_QNAME:
20✔
527
                return True
20✔
528
        return False
20✔
529

530
    @staticmethod
20✔
531
    def _assigns_typealias(node: Optional[nodes.NodeNG]) -> bool:
20✔
532
        """Check if a node is assigning a TypeAlias.
533

534
        Taken from pylint.checkers.base.name_checker.checker."""
535
        inferred = utils.safe_infer(node)
20✔
536
        if isinstance(inferred, nodes.ClassDef):
20✔
537
            qname = inferred.qname()
20✔
538
            if qname == "typing.TypeAlias":
20✔
539
                return True
12✔
540
            if qname == ".Union":
20✔
541
                # Union is a special case because it can be used as a type alias
542
                # or as a type annotation. We only want to check the former.
543
                assert node is not None
20✔
544
                return not isinstance(node.parent, nodes.AnnAssign)
20✔
545
        elif isinstance(inferred, nodes.FunctionDef):
20✔
546
            # TODO: when py3.12 is minimum, remove this condition
547
            # TypeAlias became a class in python 3.12
548
            if inferred.qname() == "typing.TypeAlias":
8✔
549
                return True
8✔
550
        return False
20✔
551

552

553
def register(linter: PyLinter) -> None:
20✔
554
    """Required method to auto-register this checker to the linter"""
555
    linter.register_checker(InvalidNameChecker(linter))
20✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc