diff options
author | Pierre Sassoulas <pierre.sassoulas@gmail.com> | 2022-03-24 22:03:35 +0100 |
---|---|---|
committer | Pierre Sassoulas <pierre.sassoulas@gmail.com> | 2022-03-24 22:40:30 +0100 |
commit | 3f11fe629a7b89d2a3b92dce09ac5818f3904cee (patch) | |
tree | daf3488946f26b184bf8fc850ba7e8f22c3e8ba0 | |
parent | 6940715ba15f81fbd7d9e8685c0a714a8b612f24 (diff) | |
download | pylint-git-3f11fe629a7b89d2a3b92dce09ac5818f3904cee.tar.gz |
[refactor] Create a package for the NameChecker in pylint.checker.base
-rw-r--r-- | pylint/checkers/base/__init__.py | 757 | ||||
-rw-r--r-- | pylint/checkers/base/name_checker/__init__.py | 25 | ||||
-rw-r--r-- | pylint/checkers/base/name_checker/checker.py | 588 | ||||
-rw-r--r-- | pylint/checkers/base/name_checker/naming_style.py | 175 | ||||
-rw-r--r-- | tests/checkers/base/unittest_base.py | 260 | ||||
-rw-r--r-- | tests/checkers/base/unittest_multi_naming_style.py | 176 | ||||
-rw-r--r-- | tests/checkers/base/unittest_name_preset.py | 99 |
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) |