summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPierre Sassoulas <pierre.sassoulas@gmail.com>2022-03-24 22:03:35 +0100
committerPierre Sassoulas <pierre.sassoulas@gmail.com>2022-03-24 22:40:30 +0100
commit3f11fe629a7b89d2a3b92dce09ac5818f3904cee (patch)
treedaf3488946f26b184bf8fc850ba7e8f22c3e8ba0
parent6940715ba15f81fbd7d9e8685c0a714a8b612f24 (diff)
downloadpylint-git-3f11fe629a7b89d2a3b92dce09ac5818f3904cee.tar.gz
[refactor] Create a package for the NameChecker in pylint.checker.base
-rw-r--r--pylint/checkers/base/__init__.py757
-rw-r--r--pylint/checkers/base/name_checker/__init__.py25
-rw-r--r--pylint/checkers/base/name_checker/checker.py588
-rw-r--r--pylint/checkers/base/name_checker/naming_style.py175
-rw-r--r--tests/checkers/base/unittest_base.py260
-rw-r--r--tests/checkers/base/unittest_multi_naming_style.py176
-rw-r--r--tests/checkers/base/unittest_name_preset.py99
7 files changed, 1088 insertions, 992 deletions
diff --git a/pylint/checkers/base/__init__.py b/pylint/checkers/base/__init__.py
index d4e9417ef..67c18d8d1 100644
--- a/pylint/checkers/base/__init__.py
+++ b/pylint/checkers/base/__init__.py
@@ -3,23 +3,44 @@
# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt
"""Basic checker for Python code."""
+
+__all__ = [
+ "NameChecker",
+ "NamingStyle",
+ "KNOWN_NAME_TYPES_WITH_STYLE",
+ "SnakeCaseStyle",
+ "CamelCaseStyle",
+ "UpperCaseStyle",
+ "PascalCaseStyle",
+ "AnyStyle",
+]
+
import collections
import itertools
-import re
import sys
-from typing import TYPE_CHECKING, Any, Dict, Iterator, Optional, Pattern, Tuple, cast
+from typing import TYPE_CHECKING, Any, Dict, Iterator, Optional, cast
import astroid
from astroid import nodes
-from pylint import constants, interfaces
+from pylint import interfaces
from pylint import utils as lint_utils
from pylint.checkers import utils
from pylint.checkers.base.basic_checker import _BasicChecker
from pylint.checkers.base.comparison_checker import ComparisonChecker
from pylint.checkers.base.docstring_checker import DocStringChecker
+from pylint.checkers.base.name_checker import (
+ KNOWN_NAME_TYPES_WITH_STYLE,
+ AnyStyle,
+ CamelCaseStyle,
+ NamingStyle,
+ PascalCaseStyle,
+ SnakeCaseStyle,
+ UpperCaseStyle,
+)
+from pylint.checkers.base.name_checker.checker import NameChecker
from pylint.checkers.base.pass_checker import PassChecker
-from pylint.checkers.utils import infer_all, is_property_deleter, is_property_setter
+from pylint.checkers.utils import infer_all
from pylint.reporters.ureports import nodes as reporter_nodes
from pylint.utils import LinterStats
from pylint.utils.utils import get_global_option
@@ -33,109 +54,12 @@ else:
from typing_extensions import Literal
-class NamingStyle:
- """It may seem counterintuitive that single naming style has multiple "accepted"
- forms of regular expressions, but we need to special-case stuff like dunder names in method names.
- """
-
- ANY: Pattern[str] = re.compile(".*")
- CLASS_NAME_RGX: Pattern[str] = ANY
- MOD_NAME_RGX: Pattern[str] = ANY
- CONST_NAME_RGX: Pattern[str] = ANY
- COMP_VAR_RGX: Pattern[str] = ANY
- DEFAULT_NAME_RGX: Pattern[str] = ANY
- CLASS_ATTRIBUTE_RGX: Pattern[str] = ANY
-
- @classmethod
- def get_regex(cls, name_type):
- return {
- "module": cls.MOD_NAME_RGX,
- "const": cls.CONST_NAME_RGX,
- "class": cls.CLASS_NAME_RGX,
- "function": cls.DEFAULT_NAME_RGX,
- "method": cls.DEFAULT_NAME_RGX,
- "attr": cls.DEFAULT_NAME_RGX,
- "argument": cls.DEFAULT_NAME_RGX,
- "variable": cls.DEFAULT_NAME_RGX,
- "class_attribute": cls.CLASS_ATTRIBUTE_RGX,
- "class_const": cls.CONST_NAME_RGX,
- "inlinevar": cls.COMP_VAR_RGX,
- }[name_type]
-
-
-class SnakeCaseStyle(NamingStyle):
- """Regex rules for snake_case naming style."""
-
- CLASS_NAME_RGX = re.compile(r"[^\W\dA-Z][^\WA-Z]+$")
- MOD_NAME_RGX = re.compile(r"[^\W\dA-Z][^\WA-Z]*$")
- CONST_NAME_RGX = re.compile(r"([^\W\dA-Z][^\WA-Z]*|__.*__)$")
- COMP_VAR_RGX = re.compile(r"[^\W\dA-Z][^\WA-Z]*$")
- DEFAULT_NAME_RGX = re.compile(
- r"([^\W\dA-Z][^\WA-Z]{2,}|_[^\WA-Z]*|__[^\WA-Z\d_][^\WA-Z]+__)$"
- )
- CLASS_ATTRIBUTE_RGX = re.compile(r"([^\W\dA-Z][^\WA-Z]{2,}|__.*__)$")
-
-
-class CamelCaseStyle(NamingStyle):
- """Regex rules for camelCase naming style."""
-
- CLASS_NAME_RGX = re.compile(r"[^\W\dA-Z][^\W_]+$")
- MOD_NAME_RGX = re.compile(r"[^\W\dA-Z][^\W_]*$")
- CONST_NAME_RGX = re.compile(r"([^\W\dA-Z][^\W_]*|__.*__)$")
- COMP_VAR_RGX = re.compile(r"[^\W\dA-Z][^\W_]*$")
- DEFAULT_NAME_RGX = re.compile(r"([^\W\dA-Z][^\W_]{2,}|__[^\W\dA-Z_]\w+__)$")
- CLASS_ATTRIBUTE_RGX = re.compile(r"([^\W\dA-Z][^\W_]{2,}|__.*__)$")
-
-
-class PascalCaseStyle(NamingStyle):
- """Regex rules for PascalCase naming style."""
-
- CLASS_NAME_RGX = re.compile(r"[^\W\da-z][^\W_]+$")
- MOD_NAME_RGX = re.compile(r"[^\W\da-z][^\W_]+$")
- CONST_NAME_RGX = re.compile(r"([^\W\da-z][^\W_]*|__.*__)$")
- COMP_VAR_RGX = re.compile(r"[^\W\da-z][^\W_]+$")
- DEFAULT_NAME_RGX = re.compile(r"([^\W\da-z][^\W_]{2,}|__[^\W\dA-Z_]\w+__)$")
- CLASS_ATTRIBUTE_RGX = re.compile(r"[^\W\da-z][^\W_]{2,}$")
-
-
-class UpperCaseStyle(NamingStyle):
- """Regex rules for UPPER_CASE naming style."""
-
- CLASS_NAME_RGX = re.compile(r"[^\W\da-z][^\Wa-z]+$")
- MOD_NAME_RGX = re.compile(r"[^\W\da-z][^\Wa-z]+$")
- CONST_NAME_RGX = re.compile(r"([^\W\da-z][^\Wa-z]*|__.*__)$")
- COMP_VAR_RGX = re.compile(r"[^\W\da-z][^\Wa-z]+$")
- DEFAULT_NAME_RGX = re.compile(r"([^\W\da-z][^\Wa-z]{2,}|__[^\W\dA-Z_]\w+__)$")
- CLASS_ATTRIBUTE_RGX = re.compile(r"[^\W\da-z][^\Wa-z]{2,}$")
-
-
-class AnyStyle(NamingStyle):
- pass
-
-
-NAMING_STYLES = {
- "snake_case": SnakeCaseStyle,
- "camelCase": CamelCaseStyle,
- "PascalCase": PascalCaseStyle,
- "UPPER_CASE": UpperCaseStyle,
- "any": AnyStyle,
-}
-
-# Default patterns for name types that do not have styles
-DEFAULT_PATTERNS = {
- "typevar": re.compile(
- r"^_{0,2}(?:[^\W\da-z_]+|(?:[^\W\da-z_][^\WA-Z_]+)+T?(?<!Type))(?:_co(?:ntra)?)?$"
- )
-}
-
REVERSED_PROTOCOL_METHOD = "__reversed__"
SEQUENCE_PROTOCOL_METHODS = ("__getitem__", "__len__")
REVERSED_METHODS = (SEQUENCE_PROTOCOL_METHODS, (REVERSED_PROTOCOL_METHOD,))
UNITTEST_CASE = "unittest.case"
ABC_METACLASSES = {"_py_abc.ABCMeta", "abc.ABCMeta"} # Python 3.7+,
-# Name categories that are always consistent with all naming conventions.
-EXEMPT_NAME_CATEGORIES = {"exempt", "ignore"}
# A mapping from qname -> symbol, to be used when generating messages
# about dangerous default values as arguments
@@ -162,29 +86,6 @@ DEFAULT_ARGUMENT_SYMBOLS = dict(
# List of methods which can be redefined
REDEFINABLE_METHODS = frozenset(("__module__",))
TYPING_FORWARD_REF_QNAME = "typing.ForwardRef"
-TYPING_TYPE_VAR_QNAME = "typing.TypeVar"
-
-
-def _redefines_import(node):
- """Detect that the given node (AssignName) is inside an
- exception handler and redefines an import from the tryexcept body.
-
- Returns True if the node redefines an import, False otherwise.
- """
- current = node
- while current and not isinstance(current.parent, nodes.ExceptHandler):
- current = current.parent
- if not current or not utils.error_of_type(current.parent, ImportError):
- return False
- try_block = current.parent.parent
- for import_node in try_block.nodes_of_class((nodes.ImportFrom, nodes.Import)):
- for name, alias in import_node.names:
- if alias:
- if alias == node.name:
- return True
- elif name == node.name:
- return True
- return False
LOOPLIKE_NODES = (
@@ -258,71 +159,6 @@ def _loop_exits_early(loop):
)
-def _is_multi_naming_match(match, node_type, confidence):
- return (
- match is not None
- and match.lastgroup is not None
- and match.lastgroup not in EXEMPT_NAME_CATEGORIES
- and (node_type != "method" or confidence != interfaces.INFERENCE_FAILURE)
- )
-
-
-BUILTIN_PROPERTY = "builtins.property"
-
-
-def _get_properties(config):
- """Returns a tuple of property classes and names.
-
- Property classes are fully qualified, such as 'abc.abstractproperty' and
- property names are the actual names, such as 'abstract_property'.
- """
- property_classes = {BUILTIN_PROPERTY}
- property_names = set() # Not returning 'property', it has its own check.
- if config is not None:
- property_classes.update(config.property_classes)
- property_names.update(
- prop.rsplit(".", 1)[-1] for prop in config.property_classes
- )
- return property_classes, property_names
-
-
-def _determine_function_name_type(node: nodes.FunctionDef, config=None):
- """Determine the name type whose regex the function's name should match.
-
- :param node: A function node.
- :param config: Configuration from which to pull additional property classes.
- :type config: :class:`optparse.Values`
-
- :returns: One of ('function', 'method', 'attr')
- :rtype: str
- """
- property_classes, property_names = _get_properties(config)
- if not node.is_method():
- return "function"
-
- if is_property_setter(node) or is_property_deleter(node):
- # If the function is decorated using the prop_method.{setter,getter}
- # form, treat it like an attribute as well.
- return "attr"
-
- decorators = node.decorators.nodes if node.decorators else []
- for decorator in decorators:
- # If the function is a property (decorated with @property
- # or @abc.abstractproperty), the name type is 'attr'.
- if isinstance(decorator, nodes.Name) or (
- isinstance(decorator, nodes.Attribute)
- and decorator.attrname in property_names
- ):
- inferred = utils.safe_infer(decorator)
- if (
- inferred
- and hasattr(inferred, "qname")
- and inferred.qname() in property_classes
- ):
- return "attr"
- return "method"
-
-
def _has_abstract_methods(node):
"""Determine if the given `node` has abstract methods.
@@ -1597,549 +1433,6 @@ class BasicChecker(_BasicChecker):
self._check_redeclared_assign_name([node.target])
-# Name types that have a style option
-KNOWN_NAME_TYPES_WITH_STYLE = {
- "module",
- "const",
- "class",
- "function",
- "method",
- "attr",
- "argument",
- "variable",
- "class_attribute",
- "class_const",
- "inlinevar",
-}
-
-# Name types that have a 'rgx' option
-KNOWN_NAME_TYPES = {
- *KNOWN_NAME_TYPES_WITH_STYLE,
- "typevar",
-}
-
-DEFAULT_NAMING_STYLES = {
- "module": "snake_case",
- "const": "UPPER_CASE",
- "class": "PascalCase",
- "function": "snake_case",
- "method": "snake_case",
- "attr": "snake_case",
- "argument": "snake_case",
- "variable": "snake_case",
- "class_attribute": "any",
- "class_const": "UPPER_CASE",
- "inlinevar": "any",
-}
-
-
-def _create_naming_options():
- name_options = []
- for name_type in sorted(KNOWN_NAME_TYPES):
- human_readable_name = constants.HUMAN_READABLE_TYPES[name_type]
- name_type_hyphened = name_type.replace("_", "-")
-
- help_msg = f"Regular expression matching correct {human_readable_name} names. "
- if name_type in KNOWN_NAME_TYPES_WITH_STYLE:
- help_msg += f"Overrides {name_type_hyphened}-naming-style. "
- help_msg += f"If left empty, {human_readable_name} names will be checked with the set naming style."
-
- # Add style option for names that support it
- if name_type in KNOWN_NAME_TYPES_WITH_STYLE:
- default_style = DEFAULT_NAMING_STYLES[name_type]
- name_options.append(
- (
- f"{name_type_hyphened}-naming-style",
- {
- "default": default_style,
- "type": "choice",
- "choices": list(NAMING_STYLES.keys()),
- "metavar": "<style>",
- "help": f"Naming style matching correct {human_readable_name} names.",
- },
- )
- )
-
- name_options.append(
- (
- f"{name_type_hyphened}-rgx",
- {
- "default": None,
- "type": "regexp",
- "metavar": "<regexp>",
- "help": help_msg,
- },
- )
- )
- return tuple(name_options)
-
-
-class NameChecker(_BasicChecker):
- msgs = {
- "C0103": (
- '%s name "%s" doesn\'t conform to %s',
- "invalid-name",
- "Used when the name doesn't conform to naming rules "
- "associated to its type (constant, variable, class...).",
- ),
- "C0104": (
- 'Disallowed name "%s"',
- "disallowed-name",
- "Used when the name matches bad-names or bad-names-rgxs- (unauthorized names).",
- {
- "old_names": [
- ("C0102", "blacklisted-name"),
- ]
- },
- ),
- "C0105": (
- 'Type variable "%s" is %s, use "%s" instead',
- "typevar-name-incorrect-variance",
- "Emitted when a TypeVar name doesn't reflect its type variance. "
- "According to PEP8, it is recommended to add suffixes '_co' and "
- "'_contra' to the variables used to declare covariant or "
- "contravariant behaviour respectively. Invariant (default) variables "
- "do not require a suffix. The message is also emitted when invariant "
- "variables do have a suffix.",
- ),
- "W0111": (
- "Name %s will become a keyword in Python %s",
- "assign-to-new-keyword",
- "Used when assignment will become invalid in future "
- "Python release due to introducing new keyword.",
- ),
- }
-
- options = (
- (
- "good-names",
- {
- "default": ("i", "j", "k", "ex", "Run", "_"),
- "type": "csv",
- "metavar": "<names>",
- "help": "Good variable names which should always be accepted,"
- " separated by a comma.",
- },
- ),
- (
- "good-names-rgxs",
- {
- "default": "",
- "type": "regexp_csv",
- "metavar": "<names>",
- "help": "Good variable names regexes, separated by a comma. If names match any regex,"
- " they will always be accepted",
- },
- ),
- (
- "bad-names",
- {
- "default": ("foo", "bar", "baz", "toto", "tutu", "tata"),
- "type": "csv",
- "metavar": "<names>",
- "help": "Bad variable names which should always be refused, "
- "separated by a comma.",
- },
- ),
- (
- "bad-names-rgxs",
- {
- "default": "",
- "type": "regexp_csv",
- "metavar": "<names>",
- "help": "Bad variable names regexes, separated by a comma. If names match any regex,"
- " they will always be refused",
- },
- ),
- (
- "name-group",
- {
- "default": (),
- "type": "csv",
- "metavar": "<name1:name2>",
- "help": (
- "Colon-delimited sets of names that determine each"
- " other's naming style when the name regexes"
- " allow several styles."
- ),
- },
- ),
- (
- "include-naming-hint",
- {
- "default": False,
- "type": "yn",
- "metavar": "<y or n>",
- "help": "Include a hint for the correct naming format with invalid-name.",
- },
- ),
- (
- "property-classes",
- {
- "default": ("abc.abstractproperty",),
- "type": "csv",
- "metavar": "<decorator names>",
- "help": "List of decorators that produce properties, such as "
- "abc.abstractproperty. Add to this list to register "
- "other decorators that produce valid properties. "
- "These decorators are taken in consideration only for invalid-name.",
- },
- ),
- ) + _create_naming_options()
-
- KEYWORD_ONSET = {(3, 7): {"async", "await"}}
-
- def __init__(self, linter):
- super().__init__(linter)
- self._name_category = {}
- self._name_group = {}
- self._bad_names = {}
- self._name_regexps = {}
- self._name_hints = {}
- self._good_names_rgxs_compiled = []
- self._bad_names_rgxs_compiled = []
-
- def open(self):
- self.linter.stats.reset_bad_names()
- for group in self.config.name_group:
- for name_type in group.split(":"):
- self._name_group[name_type] = f"group_{group}"
-
- regexps, hints = self._create_naming_rules()
- self._name_regexps = regexps
- self._name_hints = hints
- self._good_names_rgxs_compiled = [
- re.compile(rgxp) for rgxp in self.config.good_names_rgxs
- ]
- self._bad_names_rgxs_compiled = [
- re.compile(rgxp) for rgxp in self.config.bad_names_rgxs
- ]
-
- def _create_naming_rules(self) -> Tuple[Dict[str, Pattern[str]], Dict[str, str]]:
- regexps: Dict[str, Pattern[str]] = {}
- hints: Dict[str, str] = {}
-
- for name_type in KNOWN_NAME_TYPES:
- if name_type in KNOWN_NAME_TYPES_WITH_STYLE:
- naming_style_name = getattr(self.config, f"{name_type}_naming_style")
- regexps[name_type] = NAMING_STYLES[naming_style_name].get_regex(
- name_type
- )
- else:
- naming_style_name = "predefined"
- regexps[name_type] = DEFAULT_PATTERNS[name_type]
-
- custom_regex_setting_name = f"{name_type}_rgx"
- custom_regex = getattr(self.config, custom_regex_setting_name, None)
- if custom_regex is not None:
- regexps[name_type] = custom_regex
-
- if custom_regex is not None:
- hints[name_type] = f"{custom_regex.pattern!r} pattern"
- else:
- hints[name_type] = f"{naming_style_name} naming style"
-
- return regexps, hints
-
- @utils.check_messages("disallowed-name", "invalid-name")
- def visit_module(self, node: nodes.Module) -> None:
- self._check_name("module", node.name.split(".")[-1], node)
- self._bad_names = {}
-
- def leave_module(self, _: nodes.Module) -> None:
- for all_groups in self._bad_names.values():
- if len(all_groups) < 2:
- continue
- groups = collections.defaultdict(list)
- min_warnings = sys.maxsize
- prevalent_group, _ = max(all_groups.items(), key=lambda item: len(item[1]))
- for group in all_groups.values():
- groups[len(group)].append(group)
- min_warnings = min(len(group), min_warnings)
- if len(groups[min_warnings]) > 1:
- by_line = sorted(
- groups[min_warnings],
- key=lambda group: min(warning[0].lineno for warning in group),
- )
- warnings = itertools.chain(*by_line[1:])
- else:
- warnings = groups[min_warnings][0]
- for args in warnings:
- self._raise_name_warning(prevalent_group, *args)
-
- @utils.check_messages("disallowed-name", "invalid-name", "assign-to-new-keyword")
- def visit_classdef(self, node: nodes.ClassDef) -> None:
- self._check_assign_to_new_keyword_violation(node.name, node)
- self._check_name("class", node.name, node)
- for attr, anodes in node.instance_attrs.items():
- if not any(node.instance_attr_ancestors(attr)):
- self._check_name("attr", attr, anodes[0])
-
- @utils.check_messages("disallowed-name", "invalid-name", "assign-to-new-keyword")
- def visit_functiondef(self, node: nodes.FunctionDef) -> None:
- # Do not emit any warnings if the method is just an implementation
- # of a base class method.
- self._check_assign_to_new_keyword_violation(node.name, node)
- confidence = interfaces.HIGH
- if node.is_method():
- if utils.overrides_a_method(node.parent.frame(future=True), node.name):
- return
- confidence = (
- interfaces.INFERENCE
- if utils.has_known_bases(node.parent.frame(future=True))
- else interfaces.INFERENCE_FAILURE
- )
-
- self._check_name(
- _determine_function_name_type(node, config=self.config),
- node.name,
- node,
- confidence,
- )
- # Check argument names
- args = node.args.args
- if args is not None:
- self._recursive_check_names(args)
-
- visit_asyncfunctiondef = visit_functiondef
-
- @utils.check_messages("disallowed-name", "invalid-name")
- def visit_global(self, node: nodes.Global) -> None:
- for name in node.names:
- self._check_name("const", name, node)
-
- @utils.check_messages(
- "disallowed-name",
- "invalid-name",
- "assign-to-new-keyword",
- "typevar-name-incorrect-variance",
- )
- def visit_assignname(self, node: nodes.AssignName) -> None:
- """Check module level assigned names."""
- self._check_assign_to_new_keyword_violation(node.name, node)
- frame = node.frame(future=True)
- assign_type = node.assign_type()
-
- # Check names defined in comprehensions
- if isinstance(assign_type, nodes.Comprehension):
- self._check_name("inlinevar", node.name, node)
-
- # Check names defined in module scope
- elif isinstance(frame, nodes.Module):
- # Check names defined in Assign nodes
- if isinstance(assign_type, nodes.Assign):
- inferred_assign_type = utils.safe_infer(assign_type.value)
-
- # Check TypeVar's assigned alone or in tuple assignment
- if isinstance(node.parent, nodes.Assign) and self._assigns_typevar(
- assign_type.value
- ):
- self._check_name("typevar", assign_type.targets[0].name, node)
- elif (
- isinstance(node.parent, nodes.Tuple)
- and isinstance(assign_type.value, nodes.Tuple)
- and self._assigns_typevar(
- assign_type.value.elts[node.parent.elts.index(node)]
- )
- ):
- self._check_name(
- "typevar",
- assign_type.targets[0].elts[node.parent.elts.index(node)].name,
- node,
- )
-
- # Check classes (TypeVar's are classes so they need to be excluded first)
- elif isinstance(inferred_assign_type, nodes.ClassDef):
- self._check_name("class", node.name, node)
-
- # Don't emit if the name redefines an import in an ImportError except handler.
- elif not _redefines_import(node) and isinstance(
- inferred_assign_type, nodes.Const
- ):
- self._check_name("const", node.name, node)
- # Check names defined in AnnAssign nodes
- elif isinstance(
- assign_type, nodes.AnnAssign
- ) and utils.is_assign_name_annotated_with(node, "Final"):
- self._check_name("const", node.name, node)
-
- # Check names defined in function scopes
- elif isinstance(frame, nodes.FunctionDef):
- # global introduced variable aren't in the function locals
- if node.name in frame and node.name not in frame.argnames():
- if not _redefines_import(node):
- self._check_name("variable", node.name, node)
-
- # Check names defined in class scopes
- elif isinstance(frame, nodes.ClassDef):
- if not list(frame.local_attr_ancestors(node.name)):
- for ancestor in frame.ancestors():
- if (
- ancestor.name == "Enum"
- and ancestor.root().name == "enum"
- or utils.is_assign_name_annotated_with(node, "Final")
- ):
- self._check_name("class_const", node.name, node)
- break
- else:
- self._check_name("class_attribute", node.name, node)
-
- def _recursive_check_names(self, args):
- """Check names in a possibly recursive list <arg>."""
- for arg in args:
- if isinstance(arg, nodes.AssignName):
- self._check_name("argument", arg.name, arg)
- else:
- self._recursive_check_names(arg.elts)
-
- def _find_name_group(self, node_type):
- return self._name_group.get(node_type, node_type)
-
- def _raise_name_warning(
- self,
- prevalent_group: Optional[str],
- node: nodes.NodeNG,
- node_type: str,
- name: str,
- confidence,
- warning: str = "invalid-name",
- ) -> None:
- type_label = constants.HUMAN_READABLE_TYPES[node_type]
- hint = self._name_hints[node_type]
- if prevalent_group:
- # This happens in the multi naming match case. The expected
- # prevalent group needs to be spelled out to make the message
- # correct.
- hint = f"the `{prevalent_group}` group in the {hint}"
- if self.config.include_naming_hint:
- hint += f" ({self._name_regexps[node_type].pattern!r} pattern)"
- args = (
- (type_label.capitalize(), name, hint)
- if warning == "invalid-name"
- else (type_label.capitalize(), name)
- )
-
- self.add_message(warning, node=node, args=args, confidence=confidence)
- self.linter.stats.increase_bad_name(node_type, 1)
-
- def _name_allowed_by_regex(self, name: str) -> bool:
- return name in self.config.good_names or any(
- pattern.match(name) for pattern in self._good_names_rgxs_compiled
- )
-
- def _name_disallowed_by_regex(self, name: str) -> bool:
- return name in self.config.bad_names or any(
- pattern.match(name) for pattern in self._bad_names_rgxs_compiled
- )
-
- def _check_name(self, node_type, name, node, confidence=interfaces.HIGH):
- """Check for a name using the type's regexp."""
-
- def _should_exempt_from_invalid_name(node):
- if node_type == "variable":
- inferred = utils.safe_infer(node)
- if isinstance(inferred, nodes.ClassDef):
- return True
- return False
-
- if self._name_allowed_by_regex(name=name):
- return
- if self._name_disallowed_by_regex(name=name):
- self.linter.stats.increase_bad_name(node_type, 1)
- self.add_message("disallowed-name", node=node, args=name)
- return
- regexp = self._name_regexps[node_type]
- match = regexp.match(name)
-
- if _is_multi_naming_match(match, node_type, confidence):
- name_group = self._find_name_group(node_type)
- bad_name_group = self._bad_names.setdefault(name_group, {})
- warnings = bad_name_group.setdefault(match.lastgroup, [])
- warnings.append((node, node_type, name, confidence))
-
- if match is None and not _should_exempt_from_invalid_name(node):
- self._raise_name_warning(None, node, node_type, name, confidence)
-
- # Check TypeVar names for variance suffixes
- if node_type == "typevar":
- self._check_typevar_variance(name, node)
-
- def _check_assign_to_new_keyword_violation(self, name, node):
- keyword_first_version = self._name_became_keyword_in_version(
- name, self.KEYWORD_ONSET
- )
- if keyword_first_version is not None:
- self.add_message(
- "assign-to-new-keyword",
- node=node,
- args=(name, keyword_first_version),
- confidence=interfaces.HIGH,
- )
-
- @staticmethod
- def _name_became_keyword_in_version(name, rules):
- for version, keywords in rules.items():
- if name in keywords and sys.version_info < version:
- return ".".join(str(v) for v in version)
- return None
-
- @staticmethod
- def _assigns_typevar(node: Optional[nodes.NodeNG]) -> bool:
- """Check if a node is assigning a TypeVar."""
- if isinstance(node, astroid.Call):
- inferred = utils.safe_infer(node.func)
- if (
- isinstance(inferred, astroid.ClassDef)
- and inferred.qname() == TYPING_TYPE_VAR_QNAME
- ):
- return True
- return False
-
- def _check_typevar_variance(self, name: str, node: nodes.AssignName) -> None:
- """Check if a TypeVar has a variance and if it's included in the name.
-
- Returns the args for the message to be displayed.
- """
- if isinstance(node.parent, nodes.Assign):
- keywords = node.assign_type().value.keywords
- elif isinstance(node.parent, nodes.Tuple):
- keywords = (
- node.assign_type().value.elts[node.parent.elts.index(node)].keywords
- )
-
- for kw in keywords:
- if kw.arg == "covariant" and kw.value.value:
- if not name.endswith("_co"):
- suggest_name = f"{re.sub('_contra$', '', name)}_co"
- self.add_message(
- "typevar-name-incorrect-variance",
- node=node,
- args=(name, "covariant", suggest_name),
- confidence=interfaces.INFERENCE,
- )
- return
-
- if kw.arg == "contravariant" and kw.value.value:
- if not name.endswith("_contra"):
- suggest_name = f"{re.sub('_co$', '', name)}_contra"
- self.add_message(
- "typevar-name-incorrect-variance",
- node=node,
- args=(name, "contravariant", suggest_name),
- confidence=interfaces.INFERENCE,
- )
- return
-
- if name.endswith("_co") or name.endswith("_contra"):
- suggest_name = re.sub("_contra$|_co$", "", name)
- self.add_message(
- "typevar-name-incorrect-variance",
- node=node,
- args=(name, "invariant", suggest_name),
- confidence=interfaces.INFERENCE,
- )
-
-
def register(linter: "PyLinter") -> None:
linter.register_checker(BasicErrorChecker(linter))
linter.register_checker(BasicChecker(linter))
diff --git a/pylint/checkers/base/name_checker/__init__.py b/pylint/checkers/base/name_checker/__init__.py
new file mode 100644
index 000000000..3d6818b7a
--- /dev/null
+++ b/pylint/checkers/base/name_checker/__init__.py
@@ -0,0 +1,25 @@
+# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
+# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt
+
+__all__ = [
+ "NameChecker",
+ "NamingStyle",
+ "KNOWN_NAME_TYPES_WITH_STYLE",
+ "SnakeCaseStyle",
+ "CamelCaseStyle",
+ "UpperCaseStyle",
+ "PascalCaseStyle",
+ "AnyStyle",
+]
+
+from pylint.checkers.base.name_checker.checker import NameChecker
+from pylint.checkers.base.name_checker.naming_style import (
+ KNOWN_NAME_TYPES_WITH_STYLE,
+ AnyStyle,
+ CamelCaseStyle,
+ NamingStyle,
+ PascalCaseStyle,
+ SnakeCaseStyle,
+ UpperCaseStyle,
+)
diff --git a/pylint/checkers/base/name_checker/checker.py b/pylint/checkers/base/name_checker/checker.py
new file mode 100644
index 000000000..5943640c4
--- /dev/null
+++ b/pylint/checkers/base/name_checker/checker.py
@@ -0,0 +1,588 @@
+# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
+# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt
+
+"""Basic checker for Python code."""
+import collections
+import itertools
+import re
+import sys
+from typing import Dict, Optional, Pattern, Tuple
+
+import astroid
+from astroid import nodes
+
+from pylint import constants, interfaces
+from pylint.checkers import utils
+from pylint.checkers.base.basic_checker import _BasicChecker
+from pylint.checkers.base.name_checker.naming_style import (
+ KNOWN_NAME_TYPES,
+ KNOWN_NAME_TYPES_WITH_STYLE,
+ NAMING_STYLES,
+ _create_naming_options,
+)
+from pylint.checkers.utils import is_property_deleter, is_property_setter
+
+# Default patterns for name types that do not have styles
+DEFAULT_PATTERNS = {
+ "typevar": re.compile(
+ r"^_{0,2}(?:[^\W\da-z_]+|(?:[^\W\da-z_][^\WA-Z_]+)+T?(?<!Type))(?:_co(?:ntra)?)?$"
+ )
+}
+
+BUILTIN_PROPERTY = "builtins.property"
+TYPING_TYPE_VAR_QNAME = "typing.TypeVar"
+
+
+def _get_properties(config):
+ """Returns a tuple of property classes and names.
+
+ Property classes are fully qualified, such as 'abc.abstractproperty' and
+ property names are the actual names, such as 'abstract_property'.
+ """
+ property_classes = {BUILTIN_PROPERTY}
+ property_names = set() # Not returning 'property', it has its own check.
+ if config is not None:
+ property_classes.update(config.property_classes)
+ property_names.update(
+ prop.rsplit(".", 1)[-1] for prop in config.property_classes
+ )
+ return property_classes, property_names
+
+
+def _redefines_import(node):
+ """Detect that the given node (AssignName) is inside an
+ exception handler and redefines an import from the tryexcept body.
+
+ Returns True if the node redefines an import, False otherwise.
+ """
+ current = node
+ while current and not isinstance(current.parent, nodes.ExceptHandler):
+ current = current.parent
+ if not current or not utils.error_of_type(current.parent, ImportError):
+ return False
+ try_block = current.parent.parent
+ for import_node in try_block.nodes_of_class((nodes.ImportFrom, nodes.Import)):
+ for name, alias in import_node.names:
+ if alias:
+ if alias == node.name:
+ return True
+ elif name == node.name:
+ return True
+ return False
+
+
+def _determine_function_name_type(node: nodes.FunctionDef, config=None):
+ """Determine the name type whose regex the function's name should match.
+
+ :param node: A function node.
+ :param config: Configuration from which to pull additional property classes.
+ :type config: :class:`optparse.Values`
+
+ :returns: One of ('function', 'method', 'attr')
+ :rtype: str
+ """
+ property_classes, property_names = _get_properties(config)
+ if not node.is_method():
+ return "function"
+
+ if is_property_setter(node) or is_property_deleter(node):
+ # If the function is decorated using the prop_method.{setter,getter}
+ # form, treat it like an attribute as well.
+ return "attr"
+
+ decorators = node.decorators.nodes if node.decorators else []
+ for decorator in decorators:
+ # If the function is a property (decorated with @property
+ # or @abc.abstractproperty), the name type is 'attr'.
+ if isinstance(decorator, nodes.Name) or (
+ isinstance(decorator, nodes.Attribute)
+ and decorator.attrname in property_names
+ ):
+ inferred = utils.safe_infer(decorator)
+ if (
+ inferred
+ and hasattr(inferred, "qname")
+ and inferred.qname() in property_classes
+ ):
+ return "attr"
+ return "method"
+
+
+# Name categories that are always consistent with all naming conventions.
+EXEMPT_NAME_CATEGORIES = {"exempt", "ignore"}
+
+
+def _is_multi_naming_match(match, node_type, confidence):
+ return (
+ match is not None
+ and match.lastgroup is not None
+ and match.lastgroup not in EXEMPT_NAME_CATEGORIES
+ and (node_type != "method" or confidence != interfaces.INFERENCE_FAILURE)
+ )
+
+
+class NameChecker(_BasicChecker):
+ msgs = {
+ "C0103": (
+ '%s name "%s" doesn\'t conform to %s',
+ "invalid-name",
+ "Used when the name doesn't conform to naming rules "
+ "associated to its type (constant, variable, class...).",
+ ),
+ "C0104": (
+ 'Disallowed name "%s"',
+ "disallowed-name",
+ "Used when the name matches bad-names or bad-names-rgxs- (unauthorized names).",
+ {
+ "old_names": [
+ ("C0102", "blacklisted-name"),
+ ]
+ },
+ ),
+ "C0105": (
+ 'Type variable "%s" is %s, use "%s" instead',
+ "typevar-name-incorrect-variance",
+ "Emitted when a TypeVar name doesn't reflect its type variance. "
+ "According to PEP8, it is recommended to add suffixes '_co' and "
+ "'_contra' to the variables used to declare covariant or "
+ "contravariant behaviour respectively. Invariant (default) variables "
+ "do not require a suffix. The message is also emitted when invariant "
+ "variables do have a suffix.",
+ ),
+ "W0111": (
+ "Name %s will become a keyword in Python %s",
+ "assign-to-new-keyword",
+ "Used when assignment will become invalid in future "
+ "Python release due to introducing new keyword.",
+ ),
+ }
+
+ options = (
+ (
+ "good-names",
+ {
+ "default": ("i", "j", "k", "ex", "Run", "_"),
+ "type": "csv",
+ "metavar": "<names>",
+ "help": "Good variable names which should always be accepted,"
+ " separated by a comma.",
+ },
+ ),
+ (
+ "good-names-rgxs",
+ {
+ "default": "",
+ "type": "regexp_csv",
+ "metavar": "<names>",
+ "help": "Good variable names regexes, separated by a comma. If names match any regex,"
+ " they will always be accepted",
+ },
+ ),
+ (
+ "bad-names",
+ {
+ "default": ("foo", "bar", "baz", "toto", "tutu", "tata"),
+ "type": "csv",
+ "metavar": "<names>",
+ "help": "Bad variable names which should always be refused, "
+ "separated by a comma.",
+ },
+ ),
+ (
+ "bad-names-rgxs",
+ {
+ "default": "",
+ "type": "regexp_csv",
+ "metavar": "<names>",
+ "help": "Bad variable names regexes, separated by a comma. If names match any regex,"
+ " they will always be refused",
+ },
+ ),
+ (
+ "name-group",
+ {
+ "default": (),
+ "type": "csv",
+ "metavar": "<name1:name2>",
+ "help": (
+ "Colon-delimited sets of names that determine each"
+ " other's naming style when the name regexes"
+ " allow several styles."
+ ),
+ },
+ ),
+ (
+ "include-naming-hint",
+ {
+ "default": False,
+ "type": "yn",
+ "metavar": "<y or n>",
+ "help": "Include a hint for the correct naming format with invalid-name.",
+ },
+ ),
+ (
+ "property-classes",
+ {
+ "default": ("abc.abstractproperty",),
+ "type": "csv",
+ "metavar": "<decorator names>",
+ "help": "List of decorators that produce properties, such as "
+ "abc.abstractproperty. Add to this list to register "
+ "other decorators that produce valid properties. "
+ "These decorators are taken in consideration only for invalid-name.",
+ },
+ ),
+ ) + _create_naming_options()
+
+ KEYWORD_ONSET = {(3, 7): {"async", "await"}}
+
+ def __init__(self, linter):
+ super().__init__(linter)
+ self._name_category = {}
+ self._name_group = {}
+ self._bad_names = {}
+ self._name_regexps = {}
+ self._name_hints = {}
+ self._good_names_rgxs_compiled = []
+ self._bad_names_rgxs_compiled = []
+
+ def open(self):
+ self.linter.stats.reset_bad_names()
+ for group in self.config.name_group:
+ for name_type in group.split(":"):
+ self._name_group[name_type] = f"group_{group}"
+
+ regexps, hints = self._create_naming_rules()
+ self._name_regexps = regexps
+ self._name_hints = hints
+ self._good_names_rgxs_compiled = [
+ re.compile(rgxp) for rgxp in self.config.good_names_rgxs
+ ]
+ self._bad_names_rgxs_compiled = [
+ re.compile(rgxp) for rgxp in self.config.bad_names_rgxs
+ ]
+
+ def _create_naming_rules(self) -> Tuple[Dict[str, Pattern[str]], Dict[str, str]]:
+ regexps: Dict[str, Pattern[str]] = {}
+ hints: Dict[str, str] = {}
+
+ for name_type in KNOWN_NAME_TYPES:
+ if name_type in KNOWN_NAME_TYPES_WITH_STYLE:
+ naming_style_name = getattr(self.config, f"{name_type}_naming_style")
+ regexps[name_type] = NAMING_STYLES[naming_style_name].get_regex(
+ name_type
+ )
+ else:
+ naming_style_name = "predefined"
+ regexps[name_type] = DEFAULT_PATTERNS[name_type]
+
+ custom_regex_setting_name = f"{name_type}_rgx"
+ custom_regex = getattr(self.config, custom_regex_setting_name, None)
+ if custom_regex is not None:
+ regexps[name_type] = custom_regex
+
+ if custom_regex is not None:
+ hints[name_type] = f"{custom_regex.pattern!r} pattern"
+ else:
+ hints[name_type] = f"{naming_style_name} naming style"
+
+ return regexps, hints
+
+ @utils.check_messages("disallowed-name", "invalid-name")
+ def visit_module(self, node: nodes.Module) -> None:
+ self._check_name("module", node.name.split(".")[-1], node)
+ self._bad_names = {}
+
+ def leave_module(self, _: nodes.Module) -> None:
+ for all_groups in self._bad_names.values():
+ if len(all_groups) < 2:
+ continue
+ groups = collections.defaultdict(list)
+ min_warnings = sys.maxsize
+ prevalent_group, _ = max(all_groups.items(), key=lambda item: len(item[1]))
+ for group in all_groups.values():
+ groups[len(group)].append(group)
+ min_warnings = min(len(group), min_warnings)
+ if len(groups[min_warnings]) > 1:
+ by_line = sorted(
+ groups[min_warnings],
+ key=lambda group: min(warning[0].lineno for warning in group),
+ )
+ warnings = itertools.chain(*by_line[1:])
+ else:
+ warnings = groups[min_warnings][0]
+ for args in warnings:
+ self._raise_name_warning(prevalent_group, *args)
+
+ @utils.check_messages("disallowed-name", "invalid-name", "assign-to-new-keyword")
+ def visit_classdef(self, node: nodes.ClassDef) -> None:
+ self._check_assign_to_new_keyword_violation(node.name, node)
+ self._check_name("class", node.name, node)
+ for attr, anodes in node.instance_attrs.items():
+ if not any(node.instance_attr_ancestors(attr)):
+ self._check_name("attr", attr, anodes[0])
+
+ @utils.check_messages("disallowed-name", "invalid-name", "assign-to-new-keyword")
+ def visit_functiondef(self, node: nodes.FunctionDef) -> None:
+ # Do not emit any warnings if the method is just an implementation
+ # of a base class method.
+ self._check_assign_to_new_keyword_violation(node.name, node)
+ confidence = interfaces.HIGH
+ if node.is_method():
+ if utils.overrides_a_method(node.parent.frame(future=True), node.name):
+ return
+ confidence = (
+ interfaces.INFERENCE
+ if utils.has_known_bases(node.parent.frame(future=True))
+ else interfaces.INFERENCE_FAILURE
+ )
+
+ self._check_name(
+ _determine_function_name_type(node, config=self.config),
+ node.name,
+ node,
+ confidence,
+ )
+ # Check argument names
+ args = node.args.args
+ if args is not None:
+ self._recursive_check_names(args)
+
+ visit_asyncfunctiondef = visit_functiondef
+
+ @utils.check_messages("disallowed-name", "invalid-name")
+ def visit_global(self, node: nodes.Global) -> None:
+ for name in node.names:
+ self._check_name("const", name, node)
+
+ @utils.check_messages(
+ "disallowed-name",
+ "invalid-name",
+ "assign-to-new-keyword",
+ "typevar-name-incorrect-variance",
+ )
+ def visit_assignname(self, node: nodes.AssignName) -> None:
+ """Check module level assigned names."""
+ self._check_assign_to_new_keyword_violation(node.name, node)
+ frame = node.frame(future=True)
+ assign_type = node.assign_type()
+
+ # Check names defined in comprehensions
+ if isinstance(assign_type, nodes.Comprehension):
+ self._check_name("inlinevar", node.name, node)
+
+ # Check names defined in module scope
+ elif isinstance(frame, nodes.Module):
+ # Check names defined in Assign nodes
+ if isinstance(assign_type, nodes.Assign):
+ inferred_assign_type = utils.safe_infer(assign_type.value)
+
+ # Check TypeVar's assigned alone or in tuple assignment
+ if isinstance(node.parent, nodes.Assign) and self._assigns_typevar(
+ assign_type.value
+ ):
+ self._check_name("typevar", assign_type.targets[0].name, node)
+ elif (
+ isinstance(node.parent, nodes.Tuple)
+ and isinstance(assign_type.value, nodes.Tuple)
+ and self._assigns_typevar(
+ assign_type.value.elts[node.parent.elts.index(node)]
+ )
+ ):
+ self._check_name(
+ "typevar",
+ assign_type.targets[0].elts[node.parent.elts.index(node)].name,
+ node,
+ )
+
+ # Check classes (TypeVar's are classes so they need to be excluded first)
+ elif isinstance(inferred_assign_type, nodes.ClassDef):
+ self._check_name("class", node.name, node)
+
+ # Don't emit if the name redefines an import in an ImportError except handler.
+ elif not _redefines_import(node) and isinstance(
+ inferred_assign_type, nodes.Const
+ ):
+ self._check_name("const", node.name, node)
+ # Check names defined in AnnAssign nodes
+ elif isinstance(
+ assign_type, nodes.AnnAssign
+ ) and utils.is_assign_name_annotated_with(node, "Final"):
+ self._check_name("const", node.name, node)
+
+ # Check names defined in function scopes
+ elif isinstance(frame, nodes.FunctionDef):
+ # global introduced variable aren't in the function locals
+ if node.name in frame and node.name not in frame.argnames():
+ if not _redefines_import(node):
+ self._check_name("variable", node.name, node)
+
+ # Check names defined in class scopes
+ elif isinstance(frame, nodes.ClassDef):
+ if not list(frame.local_attr_ancestors(node.name)):
+ for ancestor in frame.ancestors():
+ if (
+ ancestor.name == "Enum"
+ and ancestor.root().name == "enum"
+ or utils.is_assign_name_annotated_with(node, "Final")
+ ):
+ self._check_name("class_const", node.name, node)
+ break
+ else:
+ self._check_name("class_attribute", node.name, node)
+
+ def _recursive_check_names(self, args):
+ """Check names in a possibly recursive list <arg>."""
+ for arg in args:
+ if isinstance(arg, nodes.AssignName):
+ self._check_name("argument", arg.name, arg)
+ else:
+ self._recursive_check_names(arg.elts)
+
+ def _find_name_group(self, node_type):
+ return self._name_group.get(node_type, node_type)
+
+ def _raise_name_warning(
+ self,
+ prevalent_group: Optional[str],
+ node: nodes.NodeNG,
+ node_type: str,
+ name: str,
+ confidence,
+ warning: str = "invalid-name",
+ ) -> None:
+ type_label = constants.HUMAN_READABLE_TYPES[node_type]
+ hint = self._name_hints[node_type]
+ if prevalent_group:
+ # This happens in the multi naming match case. The expected
+ # prevalent group needs to be spelled out to make the message
+ # correct.
+ hint = f"the `{prevalent_group}` group in the {hint}"
+ if self.config.include_naming_hint:
+ hint += f" ({self._name_regexps[node_type].pattern!r} pattern)"
+ args = (
+ (type_label.capitalize(), name, hint)
+ if warning == "invalid-name"
+ else (type_label.capitalize(), name)
+ )
+
+ self.add_message(warning, node=node, args=args, confidence=confidence)
+ self.linter.stats.increase_bad_name(node_type, 1)
+
+ def _name_allowed_by_regex(self, name: str) -> bool:
+ return name in self.config.good_names or any(
+ pattern.match(name) for pattern in self._good_names_rgxs_compiled
+ )
+
+ def _name_disallowed_by_regex(self, name: str) -> bool:
+ return name in self.config.bad_names or any(
+ pattern.match(name) for pattern in self._bad_names_rgxs_compiled
+ )
+
+ def _check_name(self, node_type, name, node, confidence=interfaces.HIGH):
+ """Check for a name using the type's regexp."""
+
+ def _should_exempt_from_invalid_name(node):
+ if node_type == "variable":
+ inferred = utils.safe_infer(node)
+ if isinstance(inferred, nodes.ClassDef):
+ return True
+ return False
+
+ if self._name_allowed_by_regex(name=name):
+ return
+ if self._name_disallowed_by_regex(name=name):
+ self.linter.stats.increase_bad_name(node_type, 1)
+ self.add_message("disallowed-name", node=node, args=name)
+ return
+ regexp = self._name_regexps[node_type]
+ match = regexp.match(name)
+
+ if _is_multi_naming_match(match, node_type, confidence):
+ name_group = self._find_name_group(node_type)
+ bad_name_group = self._bad_names.setdefault(name_group, {})
+ warnings = bad_name_group.setdefault(match.lastgroup, [])
+ warnings.append((node, node_type, name, confidence))
+
+ if match is None and not _should_exempt_from_invalid_name(node):
+ self._raise_name_warning(None, node, node_type, name, confidence)
+
+ # Check TypeVar names for variance suffixes
+ if node_type == "typevar":
+ self._check_typevar_variance(name, node)
+
+ def _check_assign_to_new_keyword_violation(self, name, node):
+ keyword_first_version = self._name_became_keyword_in_version(
+ name, self.KEYWORD_ONSET
+ )
+ if keyword_first_version is not None:
+ self.add_message(
+ "assign-to-new-keyword",
+ node=node,
+ args=(name, keyword_first_version),
+ confidence=interfaces.HIGH,
+ )
+
+ @staticmethod
+ def _name_became_keyword_in_version(name, rules):
+ for version, keywords in rules.items():
+ if name in keywords and sys.version_info < version:
+ return ".".join(str(v) for v in version)
+ return None
+
+ @staticmethod
+ def _assigns_typevar(node: Optional[nodes.NodeNG]) -> bool:
+ """Check if a node is assigning a TypeVar."""
+ if isinstance(node, astroid.Call):
+ inferred = utils.safe_infer(node.func)
+ if (
+ isinstance(inferred, astroid.ClassDef)
+ and inferred.qname() == TYPING_TYPE_VAR_QNAME
+ ):
+ return True
+ return False
+
+ def _check_typevar_variance(self, name: str, node: nodes.AssignName) -> None:
+ """Check if a TypeVar has a variance and if it's included in the name.
+
+ Returns the args for the message to be displayed.
+ """
+ if isinstance(node.parent, nodes.Assign):
+ keywords = node.assign_type().value.keywords
+ elif isinstance(node.parent, nodes.Tuple):
+ keywords = (
+ node.assign_type().value.elts[node.parent.elts.index(node)].keywords
+ )
+
+ for kw in keywords:
+ if kw.arg == "covariant" and kw.value.value:
+ if not name.endswith("_co"):
+ suggest_name = f"{re.sub('_contra$', '', name)}_co"
+ self.add_message(
+ "typevar-name-incorrect-variance",
+ node=node,
+ args=(name, "covariant", suggest_name),
+ confidence=interfaces.INFERENCE,
+ )
+ return
+
+ if kw.arg == "contravariant" and kw.value.value:
+ if not name.endswith("_contra"):
+ suggest_name = f"{re.sub('_co$', '', name)}_contra"
+ self.add_message(
+ "typevar-name-incorrect-variance",
+ node=node,
+ args=(name, "contravariant", suggest_name),
+ confidence=interfaces.INFERENCE,
+ )
+ return
+
+ if name.endswith("_co") or name.endswith("_contra"):
+ suggest_name = re.sub("_contra$|_co$", "", name)
+ self.add_message(
+ "typevar-name-incorrect-variance",
+ node=node,
+ args=(name, "invariant", suggest_name),
+ confidence=interfaces.INFERENCE,
+ )
diff --git a/pylint/checkers/base/name_checker/naming_style.py b/pylint/checkers/base/name_checker/naming_style.py
new file mode 100644
index 000000000..98bdaf737
--- /dev/null
+++ b/pylint/checkers/base/name_checker/naming_style.py
@@ -0,0 +1,175 @@
+# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
+# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt
+
+import re
+from typing import Pattern
+
+from pylint import constants
+
+
+class NamingStyle:
+ """It may seem counterintuitive that single naming style has multiple "accepted"
+ forms of regular expressions, but we need to special-case stuff like dunder names in method names.
+ """
+
+ ANY: Pattern[str] = re.compile(".*")
+ CLASS_NAME_RGX: Pattern[str] = ANY
+ MOD_NAME_RGX: Pattern[str] = ANY
+ CONST_NAME_RGX: Pattern[str] = ANY
+ COMP_VAR_RGX: Pattern[str] = ANY
+ DEFAULT_NAME_RGX: Pattern[str] = ANY
+ CLASS_ATTRIBUTE_RGX: Pattern[str] = ANY
+
+ @classmethod
+ def get_regex(cls, name_type):
+ return {
+ "module": cls.MOD_NAME_RGX,
+ "const": cls.CONST_NAME_RGX,
+ "class": cls.CLASS_NAME_RGX,
+ "function": cls.DEFAULT_NAME_RGX,
+ "method": cls.DEFAULT_NAME_RGX,
+ "attr": cls.DEFAULT_NAME_RGX,
+ "argument": cls.DEFAULT_NAME_RGX,
+ "variable": cls.DEFAULT_NAME_RGX,
+ "class_attribute": cls.CLASS_ATTRIBUTE_RGX,
+ "class_const": cls.CONST_NAME_RGX,
+ "inlinevar": cls.COMP_VAR_RGX,
+ }[name_type]
+
+
+class SnakeCaseStyle(NamingStyle):
+ """Regex rules for snake_case naming style."""
+
+ CLASS_NAME_RGX = re.compile(r"[^\W\dA-Z][^\WA-Z]+$")
+ MOD_NAME_RGX = re.compile(r"[^\W\dA-Z][^\WA-Z]*$")
+ CONST_NAME_RGX = re.compile(r"([^\W\dA-Z][^\WA-Z]*|__.*__)$")
+ COMP_VAR_RGX = re.compile(r"[^\W\dA-Z][^\WA-Z]*$")
+ DEFAULT_NAME_RGX = re.compile(
+ r"([^\W\dA-Z][^\WA-Z]{2,}|_[^\WA-Z]*|__[^\WA-Z\d_][^\WA-Z]+__)$"
+ )
+ CLASS_ATTRIBUTE_RGX = re.compile(r"([^\W\dA-Z][^\WA-Z]{2,}|__.*__)$")
+
+
+class CamelCaseStyle(NamingStyle):
+ """Regex rules for camelCase naming style."""
+
+ CLASS_NAME_RGX = re.compile(r"[^\W\dA-Z][^\W_]+$")
+ MOD_NAME_RGX = re.compile(r"[^\W\dA-Z][^\W_]*$")
+ CONST_NAME_RGX = re.compile(r"([^\W\dA-Z][^\W_]*|__.*__)$")
+ COMP_VAR_RGX = re.compile(r"[^\W\dA-Z][^\W_]*$")
+ DEFAULT_NAME_RGX = re.compile(r"([^\W\dA-Z][^\W_]{2,}|__[^\W\dA-Z_]\w+__)$")
+ CLASS_ATTRIBUTE_RGX = re.compile(r"([^\W\dA-Z][^\W_]{2,}|__.*__)$")
+
+
+class PascalCaseStyle(NamingStyle):
+ """Regex rules for PascalCase naming style."""
+
+ CLASS_NAME_RGX = re.compile(r"[^\W\da-z][^\W_]+$")
+ MOD_NAME_RGX = re.compile(r"[^\W\da-z][^\W_]+$")
+ CONST_NAME_RGX = re.compile(r"([^\W\da-z][^\W_]*|__.*__)$")
+ COMP_VAR_RGX = re.compile(r"[^\W\da-z][^\W_]+$")
+ DEFAULT_NAME_RGX = re.compile(r"([^\W\da-z][^\W_]{2,}|__[^\W\dA-Z_]\w+__)$")
+ CLASS_ATTRIBUTE_RGX = re.compile(r"[^\W\da-z][^\W_]{2,}$")
+
+
+class UpperCaseStyle(NamingStyle):
+ """Regex rules for UPPER_CASE naming style."""
+
+ CLASS_NAME_RGX = re.compile(r"[^\W\da-z][^\Wa-z]+$")
+ MOD_NAME_RGX = re.compile(r"[^\W\da-z][^\Wa-z]+$")
+ CONST_NAME_RGX = re.compile(r"([^\W\da-z][^\Wa-z]*|__.*__)$")
+ COMP_VAR_RGX = re.compile(r"[^\W\da-z][^\Wa-z]+$")
+ DEFAULT_NAME_RGX = re.compile(r"([^\W\da-z][^\Wa-z]{2,}|__[^\W\dA-Z_]\w+__)$")
+ CLASS_ATTRIBUTE_RGX = re.compile(r"[^\W\da-z][^\Wa-z]{2,}$")
+
+
+class AnyStyle(NamingStyle):
+ pass
+
+
+NAMING_STYLES = {
+ "snake_case": SnakeCaseStyle,
+ "camelCase": CamelCaseStyle,
+ "PascalCase": PascalCaseStyle,
+ "UPPER_CASE": UpperCaseStyle,
+ "any": AnyStyle,
+}
+
+# Name types that have a style option
+KNOWN_NAME_TYPES_WITH_STYLE = {
+ "module",
+ "const",
+ "class",
+ "function",
+ "method",
+ "attr",
+ "argument",
+ "variable",
+ "class_attribute",
+ "class_const",
+ "inlinevar",
+}
+
+
+DEFAULT_NAMING_STYLES = {
+ "module": "snake_case",
+ "const": "UPPER_CASE",
+ "class": "PascalCase",
+ "function": "snake_case",
+ "method": "snake_case",
+ "attr": "snake_case",
+ "argument": "snake_case",
+ "variable": "snake_case",
+ "class_attribute": "any",
+ "class_const": "UPPER_CASE",
+ "inlinevar": "any",
+}
+
+
+# Name types that have a 'rgx' option
+KNOWN_NAME_TYPES = {
+ *KNOWN_NAME_TYPES_WITH_STYLE,
+ "typevar",
+}
+
+
+def _create_naming_options():
+ name_options = []
+ for name_type in sorted(KNOWN_NAME_TYPES):
+ human_readable_name = constants.HUMAN_READABLE_TYPES[name_type]
+ name_type_hyphened = name_type.replace("_", "-")
+
+ help_msg = f"Regular expression matching correct {human_readable_name} names. "
+ if name_type in KNOWN_NAME_TYPES_WITH_STYLE:
+ help_msg += f"Overrides {name_type_hyphened}-naming-style. "
+ help_msg += f"If left empty, {human_readable_name} names will be checked with the set naming style."
+
+ # Add style option for names that support it
+ if name_type in KNOWN_NAME_TYPES_WITH_STYLE:
+ default_style = DEFAULT_NAMING_STYLES[name_type]
+ name_options.append(
+ (
+ f"{name_type_hyphened}-naming-style",
+ {
+ "default": default_style,
+ "type": "choice",
+ "choices": list(NAMING_STYLES.keys()),
+ "metavar": "<style>",
+ "help": f"Naming style matching correct {human_readable_name} names.",
+ },
+ )
+ )
+
+ name_options.append(
+ (
+ f"{name_type_hyphened}-rgx",
+ {
+ "default": None,
+ "type": "regexp",
+ "metavar": "<regexp>",
+ "help": help_msg,
+ },
+ )
+ )
+ return tuple(name_options)
diff --git a/tests/checkers/base/unittest_base.py b/tests/checkers/base/unittest_base.py
index b8eb2f350..05a7271cd 100644
--- a/tests/checkers/base/unittest_base.py
+++ b/tests/checkers/base/unittest_base.py
@@ -4,267 +4,7 @@
"""Unittest for the base checker."""
-import re
import unittest
-from typing import Type
-
-import astroid
-
-from pylint.checkers import base
-from pylint.interfaces import HIGH
-from pylint.testutils import CheckerTestCase, MessageTest, set_config
-
-
-class TestMultiNamingStyle(CheckerTestCase):
- CHECKER_CLASS = base.NameChecker
-
- MULTI_STYLE_RE = re.compile("(?:(?P<UP>[A-Z]+)|(?P<down>[a-z]+))$")
-
- @set_config(class_rgx=MULTI_STYLE_RE)
- def test_multi_name_detection_majority(self) -> None:
- classes = astroid.extract_node(
- """
- class classb(object): #@
- pass
- class CLASSA(object): #@
- pass
- class CLASSC(object): #@
- pass
- """
- )
- message = MessageTest(
- "invalid-name",
- node=classes[0],
- args=(
- "Class",
- "classb",
- "the `UP` group in the '(?:(?P<UP>[A-Z]+)|(?P<down>[a-z]+))$' pattern",
- ),
- confidence=HIGH,
- line=2,
- col_offset=0,
- end_line=2,
- end_col_offset=12,
- )
- with self.assertAddsMessages(message):
- cls = None
- for cls in classes:
- self.checker.visit_classdef(cls)
- if cls:
- self.checker.leave_module(cls.root)
-
- @set_config(class_rgx=MULTI_STYLE_RE)
- def test_multi_name_detection_first_invalid(self) -> None:
- classes = astroid.extract_node(
- """
- class class_a(object): #@
- pass
- class classb(object): #@
- pass
- class CLASSC(object): #@
- pass
- """
- )
- messages = [
- MessageTest(
- "invalid-name",
- node=classes[0],
- args=(
- "Class",
- "class_a",
- "'(?:(?P<UP>[A-Z]+)|(?P<down>[a-z]+))$' pattern",
- ),
- confidence=HIGH,
- line=2,
- col_offset=0,
- end_line=2,
- end_col_offset=13,
- ),
- MessageTest(
- "invalid-name",
- node=classes[2],
- args=(
- "Class",
- "CLASSC",
- "the `down` group in the '(?:(?P<UP>[A-Z]+)|(?P<down>[a-z]+))$' pattern",
- ),
- confidence=HIGH,
- line=6,
- col_offset=0,
- end_line=6,
- end_col_offset=12,
- ),
- ]
- with self.assertAddsMessages(*messages):
- cls = None
- for cls in classes:
- self.checker.visit_classdef(cls)
- if cls:
- self.checker.leave_module(cls.root)
-
- @set_config(
- method_rgx=MULTI_STYLE_RE,
- function_rgx=MULTI_STYLE_RE,
- name_group=("function:method",),
- )
- def test_multi_name_detection_group(self):
- function_defs = astroid.extract_node(
- """
- class First(object):
- def func(self): #@
- pass
-
- def FUNC(): #@
- pass
- """,
- module_name="test",
- )
- message = MessageTest(
- "invalid-name",
- node=function_defs[1],
- args=(
- "Function",
- "FUNC",
- "the `down` group in the '(?:(?P<UP>[A-Z]+)|(?P<down>[a-z]+))$' pattern",
- ),
- confidence=HIGH,
- line=6,
- col_offset=0,
- end_line=6,
- end_col_offset=8,
- )
- with self.assertAddsMessages(message):
- func = None
- for func in function_defs:
- self.checker.visit_functiondef(func)
- if func:
- self.checker.leave_module(func.root)
-
- @set_config(
- function_rgx=re.compile("(?:(?P<ignore>FOO)|(?P<UP>[A-Z]+)|(?P<down>[a-z]+))$")
- )
- def test_multi_name_detection_exempt(self) -> None:
- function_defs = astroid.extract_node(
- """
- def FOO(): #@
- pass
- def lower(): #@
- pass
- def FOO(): #@
- pass
- def UPPER(): #@
- pass
- """
- )
- message = MessageTest(
- "invalid-name",
- node=function_defs[3],
- args=(
- "Function",
- "UPPER",
- "the `down` group in the '(?:(?P<ignore>FOO)|(?P<UP>[A-Z]+)|(?P<down>[a-z]+))$' pattern",
- ),
- confidence=HIGH,
- line=8,
- col_offset=0,
- end_line=8,
- end_col_offset=9,
- )
- with self.assertAddsMessages(message):
- func = None
- for func in function_defs:
- self.checker.visit_functiondef(func)
- if func:
- self.checker.leave_module(func.root)
-
-
-class TestNamePresets(unittest.TestCase):
- SNAKE_CASE_NAMES = {"tést_snake_case", "test_snake_case11", "test_https_200"}
- CAMEL_CASE_NAMES = {"téstCamelCase", "testCamelCase11", "testHTTP200"}
- UPPER_CASE_NAMES = {"TÉST_UPPER_CASE", "TEST_UPPER_CASE11", "TEST_HTTP_200"}
- PASCAL_CASE_NAMES = {"TéstPascalCase", "TestPascalCase11", "TestHTTP200"}
- ALL_NAMES = (
- SNAKE_CASE_NAMES | CAMEL_CASE_NAMES | UPPER_CASE_NAMES | PASCAL_CASE_NAMES
- )
-
- def _test_name_is_correct_for_all_name_types(
- self, naming_style: Type[base.NamingStyle], name: str
- ) -> None:
- for name_type in base.KNOWN_NAME_TYPES_WITH_STYLE:
- self._test_is_correct(naming_style, name, name_type)
-
- def _test_name_is_incorrect_for_all_name_types(
- self, naming_style: Type[base.NamingStyle], name: str
- ) -> None:
- for name_type in base.KNOWN_NAME_TYPES_WITH_STYLE:
- self._test_is_incorrect(naming_style, name, name_type)
-
- def _test_should_always_pass(self, naming_style: Type[base.NamingStyle]) -> None:
- always_pass_data = [
- ("__add__", "method"),
- ("__set_name__", "method"),
- ("__version__", "const"),
- ("__author__", "const"),
- ]
- for name, name_type in always_pass_data:
- self._test_is_correct(naming_style, name, name_type)
-
- @staticmethod
- def _test_is_correct(
- naming_style: Type[base.NamingStyle], name: str, name_type: str
- ) -> None:
- rgx = naming_style.get_regex(name_type)
- fail = f"{name!r} does not match pattern {rgx!r} (style: {naming_style}, type: {name_type})"
- assert rgx.match(name), fail
-
- @staticmethod
- def _test_is_incorrect(
- naming_style: Type[base.NamingStyle], name: str, name_type: str
- ) -> None:
- rgx = naming_style.get_regex(name_type)
- fail = f"{name!r} not match pattern {rgx!r} (style: {naming_style}, type: {name_type})"
- assert not rgx.match(name), fail
-
- def test_snake_case(self) -> None:
- naming_style = base.SnakeCaseStyle
-
- for name in self.SNAKE_CASE_NAMES:
- self._test_name_is_correct_for_all_name_types(naming_style, name)
- for name in self.ALL_NAMES - self.SNAKE_CASE_NAMES:
- self._test_name_is_incorrect_for_all_name_types(naming_style, name)
-
- self._test_should_always_pass(naming_style)
-
- def test_camel_case(self) -> None:
- naming_style = base.CamelCaseStyle
-
- for name in self.CAMEL_CASE_NAMES:
- self._test_name_is_correct_for_all_name_types(naming_style, name)
- for name in self.ALL_NAMES - self.CAMEL_CASE_NAMES:
- self._test_name_is_incorrect_for_all_name_types(naming_style, name)
-
- self._test_should_always_pass(naming_style)
-
- def test_upper_case(self) -> None:
- naming_style = base.UpperCaseStyle
-
- for name in self.UPPER_CASE_NAMES:
- self._test_name_is_correct_for_all_name_types(naming_style, name)
- for name in self.ALL_NAMES - self.UPPER_CASE_NAMES:
- self._test_name_is_incorrect_for_all_name_types(naming_style, name)
- self._test_name_is_incorrect_for_all_name_types(naming_style, "UPPERcase")
-
- self._test_should_always_pass(naming_style)
-
- def test_pascal_case(self) -> None:
- naming_style = base.PascalCaseStyle
-
- for name in self.PASCAL_CASE_NAMES:
- self._test_name_is_correct_for_all_name_types(naming_style, name)
- for name in self.ALL_NAMES - self.PASCAL_CASE_NAMES:
- self._test_name_is_incorrect_for_all_name_types(naming_style, name)
-
- self._test_should_always_pass(naming_style)
class TestNoSix(unittest.TestCase):
diff --git a/tests/checkers/base/unittest_multi_naming_style.py b/tests/checkers/base/unittest_multi_naming_style.py
new file mode 100644
index 000000000..b9215a1e0
--- /dev/null
+++ b/tests/checkers/base/unittest_multi_naming_style.py
@@ -0,0 +1,176 @@
+# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
+# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt
+
+"""Unittest for the NameChecker."""
+
+import re
+
+import astroid
+
+from pylint.checkers import base
+from pylint.interfaces import HIGH
+from pylint.testutils import CheckerTestCase, MessageTest, set_config
+
+
+class TestMultiNamingStyle(CheckerTestCase):
+ CHECKER_CLASS = base.NameChecker
+
+ MULTI_STYLE_RE = re.compile("(?:(?P<UP>[A-Z]+)|(?P<down>[a-z]+))$")
+
+ @set_config(class_rgx=MULTI_STYLE_RE)
+ def test_multi_name_detection_majority(self) -> None:
+ classes = astroid.extract_node(
+ """
+ class classb(object): #@
+ pass
+ class CLASSA(object): #@
+ pass
+ class CLASSC(object): #@
+ pass
+ """
+ )
+ message = MessageTest(
+ "invalid-name",
+ node=classes[0],
+ args=(
+ "Class",
+ "classb",
+ "the `UP` group in the '(?:(?P<UP>[A-Z]+)|(?P<down>[a-z]+))$' pattern",
+ ),
+ confidence=HIGH,
+ line=2,
+ col_offset=0,
+ end_line=2,
+ end_col_offset=12,
+ )
+ with self.assertAddsMessages(message):
+ cls = None
+ for cls in classes:
+ self.checker.visit_classdef(cls)
+ if cls:
+ self.checker.leave_module(cls.root)
+
+ @set_config(class_rgx=MULTI_STYLE_RE)
+ def test_multi_name_detection_first_invalid(self) -> None:
+ classes = astroid.extract_node(
+ """
+ class class_a(object): #@
+ pass
+ class classb(object): #@
+ pass
+ class CLASSC(object): #@
+ pass
+ """
+ )
+ messages = [
+ MessageTest(
+ "invalid-name",
+ node=classes[0],
+ args=(
+ "Class",
+ "class_a",
+ "'(?:(?P<UP>[A-Z]+)|(?P<down>[a-z]+))$' pattern",
+ ),
+ confidence=HIGH,
+ line=2,
+ col_offset=0,
+ end_line=2,
+ end_col_offset=13,
+ ),
+ MessageTest(
+ "invalid-name",
+ node=classes[2],
+ args=(
+ "Class",
+ "CLASSC",
+ "the `down` group in the '(?:(?P<UP>[A-Z]+)|(?P<down>[a-z]+))$' pattern",
+ ),
+ confidence=HIGH,
+ line=6,
+ col_offset=0,
+ end_line=6,
+ end_col_offset=12,
+ ),
+ ]
+ with self.assertAddsMessages(*messages):
+ cls = None
+ for cls in classes:
+ self.checker.visit_classdef(cls)
+ if cls:
+ self.checker.leave_module(cls.root)
+
+ @set_config(
+ method_rgx=MULTI_STYLE_RE,
+ function_rgx=MULTI_STYLE_RE,
+ name_group=("function:method",),
+ )
+ def test_multi_name_detection_group(self):
+ function_defs = astroid.extract_node(
+ """
+ class First(object):
+ def func(self): #@
+ pass
+
+ def FUNC(): #@
+ pass
+ """,
+ module_name="test",
+ )
+ message = MessageTest(
+ "invalid-name",
+ node=function_defs[1],
+ args=(
+ "Function",
+ "FUNC",
+ "the `down` group in the '(?:(?P<UP>[A-Z]+)|(?P<down>[a-z]+))$' pattern",
+ ),
+ confidence=HIGH,
+ line=6,
+ col_offset=0,
+ end_line=6,
+ end_col_offset=8,
+ )
+ with self.assertAddsMessages(message):
+ func = None
+ for func in function_defs:
+ self.checker.visit_functiondef(func)
+ if func:
+ self.checker.leave_module(func.root)
+
+ @set_config(
+ function_rgx=re.compile("(?:(?P<ignore>FOO)|(?P<UP>[A-Z]+)|(?P<down>[a-z]+))$")
+ )
+ def test_multi_name_detection_exempt(self) -> None:
+ function_defs = astroid.extract_node(
+ """
+ def FOO(): #@
+ pass
+ def lower(): #@
+ pass
+ def FOO(): #@
+ pass
+ def UPPER(): #@
+ pass
+ """
+ )
+ message = MessageTest(
+ "invalid-name",
+ node=function_defs[3],
+ args=(
+ "Function",
+ "UPPER",
+ "the `down` group in the '(?:(?P<ignore>FOO)|(?P<UP>[A-Z]+)|(?P<down>[a-z]+))$' pattern",
+ ),
+ confidence=HIGH,
+ line=8,
+ col_offset=0,
+ end_line=8,
+ end_col_offset=9,
+ )
+ with self.assertAddsMessages(message):
+ func = None
+ for func in function_defs:
+ self.checker.visit_functiondef(func)
+ if func:
+ self.checker.leave_module(func.root)
diff --git a/tests/checkers/base/unittest_name_preset.py b/tests/checkers/base/unittest_name_preset.py
new file mode 100644
index 000000000..9d4b748c6
--- /dev/null
+++ b/tests/checkers/base/unittest_name_preset.py
@@ -0,0 +1,99 @@
+# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
+# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
+# Copyright (c) https://github.com/PyCQA/pylint/blob/main/CONTRIBUTORS.txt
+
+"""Unittest for the NameChecker."""
+
+import unittest
+from typing import Type
+
+from pylint.checkers import base
+
+
+class TestNamePresets(unittest.TestCase):
+ SNAKE_CASE_NAMES = {"tést_snake_case", "test_snake_case11", "test_https_200"}
+ CAMEL_CASE_NAMES = {"téstCamelCase", "testCamelCase11", "testHTTP200"}
+ UPPER_CASE_NAMES = {"TÉST_UPPER_CASE", "TEST_UPPER_CASE11", "TEST_HTTP_200"}
+ PASCAL_CASE_NAMES = {"TéstPascalCase", "TestPascalCase11", "TestHTTP200"}
+ ALL_NAMES = (
+ SNAKE_CASE_NAMES | CAMEL_CASE_NAMES | UPPER_CASE_NAMES | PASCAL_CASE_NAMES
+ )
+
+ def _test_name_is_correct_for_all_name_types(
+ self, naming_style: Type[base.NamingStyle], name: str
+ ) -> None:
+ for name_type in base.KNOWN_NAME_TYPES_WITH_STYLE:
+ self._test_is_correct(naming_style, name, name_type)
+
+ def _test_name_is_incorrect_for_all_name_types(
+ self, naming_style: Type[base.NamingStyle], name: str
+ ) -> None:
+ for name_type in base.KNOWN_NAME_TYPES_WITH_STYLE:
+ self._test_is_incorrect(naming_style, name, name_type)
+
+ def _test_should_always_pass(self, naming_style: Type[base.NamingStyle]) -> None:
+ always_pass_data = [
+ ("__add__", "method"),
+ ("__set_name__", "method"),
+ ("__version__", "const"),
+ ("__author__", "const"),
+ ]
+ for name, name_type in always_pass_data:
+ self._test_is_correct(naming_style, name, name_type)
+
+ @staticmethod
+ def _test_is_correct(
+ naming_style: Type[base.NamingStyle], name: str, name_type: str
+ ) -> None:
+ rgx = naming_style.get_regex(name_type)
+ fail = f"{name!r} does not match pattern {rgx!r} (style: {naming_style}, type: {name_type})"
+ assert rgx.match(name), fail
+
+ @staticmethod
+ def _test_is_incorrect(
+ naming_style: Type[base.NamingStyle], name: str, name_type: str
+ ) -> None:
+ rgx = naming_style.get_regex(name_type)
+ fail = f"{name!r} not match pattern {rgx!r} (style: {naming_style}, type: {name_type})"
+ assert not rgx.match(name), fail
+
+ def test_snake_case(self) -> None:
+ naming_style = base.SnakeCaseStyle
+
+ for name in self.SNAKE_CASE_NAMES:
+ self._test_name_is_correct_for_all_name_types(naming_style, name)
+ for name in self.ALL_NAMES - self.SNAKE_CASE_NAMES:
+ self._test_name_is_incorrect_for_all_name_types(naming_style, name)
+
+ self._test_should_always_pass(naming_style)
+
+ def test_camel_case(self) -> None:
+ naming_style = base.CamelCaseStyle
+
+ for name in self.CAMEL_CASE_NAMES:
+ self._test_name_is_correct_for_all_name_types(naming_style, name)
+ for name in self.ALL_NAMES - self.CAMEL_CASE_NAMES:
+ self._test_name_is_incorrect_for_all_name_types(naming_style, name)
+
+ self._test_should_always_pass(naming_style)
+
+ def test_upper_case(self) -> None:
+ naming_style = base.UpperCaseStyle
+
+ for name in self.UPPER_CASE_NAMES:
+ self._test_name_is_correct_for_all_name_types(naming_style, name)
+ for name in self.ALL_NAMES - self.UPPER_CASE_NAMES:
+ self._test_name_is_incorrect_for_all_name_types(naming_style, name)
+ self._test_name_is_incorrect_for_all_name_types(naming_style, "UPPERcase")
+
+ self._test_should_always_pass(naming_style)
+
+ def test_pascal_case(self) -> None:
+ naming_style = base.PascalCaseStyle
+
+ for name in self.PASCAL_CASE_NAMES:
+ self._test_name_is_correct_for_all_name_types(naming_style, name)
+ for name in self.ALL_NAMES - self.PASCAL_CASE_NAMES:
+ self._test_name_is_incorrect_for_all_name_types(naming_style, name)
+
+ self._test_should_always_pass(naming_style)