| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691 |
- # Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
- # For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE
- # Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt
- """Basic checker for Python code."""
- from __future__ import annotations
- import argparse
- import collections
- import itertools
- import re
- import sys
- from collections.abc import Iterable
- from enum import Enum, auto
- from re import Pattern
- from typing import TYPE_CHECKING, 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
- from pylint.typing import Options
- if TYPE_CHECKING:
- from pylint.lint.pylinter import PyLinter
- _BadNamesTuple = Tuple[nodes.NodeNG, str, str, interfaces.Confidence]
- # Default patterns for name types that do not have styles
- DEFAULT_PATTERNS = {
- "typevar": re.compile(
- r"^_{0,2}(?!T[A-Z])(?:[A-Z]+|(?:[A-Z]+[a-z]+)+T?(?<!Type))(?:_co(?:ntra)?)?$"
- ),
- "typealias": re.compile(
- r"^_{0,2}(?!T[A-Z]|Type)[A-Z]+[a-z0-9]+(?:[A-Z][a-z0-9]+)*$"
- ),
- }
- BUILTIN_PROPERTY = "builtins.property"
- TYPE_VAR_QNAME = frozenset(
- (
- "typing.TypeVar",
- "typing_extensions.TypeVar",
- )
- )
- class TypeVarVariance(Enum):
- invariant = auto()
- covariant = auto()
- contravariant = auto()
- double_variant = auto()
- def _get_properties(config: argparse.Namespace) -> tuple[set[str], set[str]]:
- """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[str] = 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: nodes.AssignName) -> bool:
- """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: argparse.Namespace
- ) -> str:
- """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.
- :returns: One of ('function', 'method', 'attr')
- """
- 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: re.Match[str] | None, node_type: str, confidence: interfaces.Confidence
- ) -> bool:
- 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 name does not reflect variance%s",
- "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.",
- ),
- "C0131": (
- "TypeVar cannot be both covariant and contravariant",
- "typevar-double-variance",
- 'Emitted when both the "covariant" and "contravariant" '
- 'keyword arguments are set to "True" in a TypeVar.',
- ),
- "C0132": (
- 'TypeVar name "%s" does not match assigned variable name "%s"',
- "typevar-name-mismatch",
- "Emitted when a TypeVar is assigned to a variable "
- "that does not match its name argument.",
- ),
- }
- _options: 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.",
- },
- ),
- )
- options: Options = _options + _create_naming_options()
- def __init__(self, linter: PyLinter) -> None:
- super().__init__(linter)
- self._name_group: dict[str, str] = {}
- self._bad_names: dict[str, dict[str, list[_BadNamesTuple]]] = {}
- self._name_regexps: dict[str, re.Pattern[str]] = {}
- self._name_hints: dict[str, str] = {}
- self._good_names_rgxs_compiled: list[re.Pattern[str]] = []
- self._bad_names_rgxs_compiled: list[re.Pattern[str]] = []
- def open(self) -> None:
- self.linter.stats.reset_bad_names()
- for group in self.linter.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.linter.config.good_names_rgxs
- ]
- self._bad_names_rgxs_compiled = [
- re.compile(rgxp) for rgxp in self.linter.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.linter.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.linter.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.only_required_for_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[
- int, list[list[_BadNamesTuple]]
- ] = 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( # type: ignore[no-any-return]
- warning[0].lineno
- for warning in group
- if warning[0].lineno is not None
- ),
- )
- warnings: Iterable[_BadNamesTuple] = itertools.chain(*by_line[1:])
- else:
- warnings = groups[min_warnings][0]
- for args in warnings:
- self._raise_name_warning(prevalent_group, *args)
- @utils.only_required_for_messages("disallowed-name", "invalid-name")
- def visit_classdef(self, node: nodes.ClassDef) -> None:
- 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.only_required_for_messages("disallowed-name", "invalid-name")
- 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.
- 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.linter.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.only_required_for_messages(
- "disallowed-name",
- "invalid-name",
- "typevar-name-incorrect-variance",
- "typevar-double-variance",
- "typevar-name-mismatch",
- )
- def visit_assignname( # pylint: disable=too-many-branches
- self, node: nodes.AssignName
- ) -> None:
- """Check module level assigned names."""
- 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 and TypeAliases assigned alone or in tuple assignment
- if isinstance(node.parent, nodes.Assign):
- if self._assigns_typevar(assign_type.value):
- self._check_name("typevar", assign_type.targets[0].name, node)
- return
- if self._assigns_typealias(assign_type.value):
- self._check_name("typealias", assign_type.targets[0].name, node)
- return
- if (
- isinstance(node.parent, nodes.Tuple)
- and isinstance(assign_type.value, nodes.Tuple)
- # protect against unbalanced tuple unpacking
- and node.parent.elts.index(node) < len(assign_type.value.elts)
- ):
- assigner = assign_type.value.elts[node.parent.elts.index(node)]
- if self._assigns_typevar(assigner):
- self._check_name(
- "typevar",
- assign_type.targets[0]
- .elts[node.parent.elts.index(node)]
- .name,
- node,
- )
- return
- if self._assigns_typealias(assigner):
- self._check_name(
- "typealias",
- assign_type.targets[0]
- .elts[node.parent.elts.index(node)]
- .name,
- node,
- )
- return
- # 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)
- else:
- self._check_name(
- "variable", node.name, node, disallowed_check_only=True
- )
- # Check names defined in AnnAssign nodes
- elif isinstance(assign_type, nodes.AnnAssign):
- if utils.is_assign_name_annotated_with(node, "Final"):
- self._check_name("const", node.name, node)
- elif self._assigns_typealias(assign_type.annotation):
- self._check_name("typealias", 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 utils.is_enum(ancestor) 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: list[nodes.AssignName]) -> None:
- """Check names in a possibly recursive list <arg>."""
- for arg in args:
- self._check_name("argument", arg.name, arg)
- def _find_name_group(self, node_type: str) -> str:
- return self._name_group.get(node_type, node_type)
- def _raise_name_warning(
- self,
- prevalent_group: str | None,
- node: nodes.NodeNG,
- node_type: str,
- name: str,
- confidence: interfaces.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.linter.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.linter.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.linter.config.bad_names or any(
- pattern.match(name) for pattern in self._bad_names_rgxs_compiled
- )
- def _check_name(
- self,
- node_type: str,
- name: str,
- node: nodes.NodeNG,
- confidence: interfaces.Confidence = interfaces.HIGH,
- disallowed_check_only: bool = False,
- ) -> None:
- """Check for a name using the type's regexp."""
- def _should_exempt_from_invalid_name(node: nodes.NodeNG) -> bool:
- 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, confidence=interfaces.HIGH
- )
- 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, {})
- # Ignored because this is checked by the if statement
- warnings = bad_name_group.setdefault(match.lastgroup, []) # type: ignore[union-attr, arg-type]
- warnings.append((node, node_type, name, confidence))
- if (
- match is None
- and not disallowed_check_only
- 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(name, node)
- @staticmethod
- def _assigns_typevar(node: nodes.NodeNG | None) -> 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() in TYPE_VAR_QNAME
- ):
- return True
- return False
- @staticmethod
- def _assigns_typealias(node: nodes.NodeNG | None) -> bool:
- """Check if a node is assigning a TypeAlias."""
- inferred = utils.safe_infer(node)
- if isinstance(inferred, nodes.ClassDef):
- if inferred.qname() == ".Union":
- # Union is a special case because it can be used as a type alias
- # or as a type annotation. We only want to check the former.
- assert node is not None
- return not isinstance(node.parent, nodes.AnnAssign)
- elif isinstance(inferred, nodes.FunctionDef):
- if inferred.qname() == "typing.TypeAlias":
- return True
- return False
- def _check_typevar(self, name: str, node: nodes.AssignName) -> None:
- """Check for TypeVar lint violations."""
- if isinstance(node.parent, nodes.Assign):
- keywords = node.assign_type().value.keywords
- args = node.assign_type().value.args
- elif isinstance(node.parent, nodes.Tuple):
- keywords = (
- node.assign_type().value.elts[node.parent.elts.index(node)].keywords
- )
- args = node.assign_type().value.elts[node.parent.elts.index(node)].args
- variance = TypeVarVariance.invariant
- name_arg = None
- for kw in keywords:
- if variance == TypeVarVariance.double_variant:
- pass
- elif kw.arg == "covariant" and kw.value.value:
- variance = (
- TypeVarVariance.covariant
- if variance != TypeVarVariance.contravariant
- else TypeVarVariance.double_variant
- )
- elif kw.arg == "contravariant" and kw.value.value:
- variance = (
- TypeVarVariance.contravariant
- if variance != TypeVarVariance.covariant
- else TypeVarVariance.double_variant
- )
- if kw.arg == "name" and isinstance(kw.value, nodes.Const):
- name_arg = kw.value.value
- if name_arg is None and args and isinstance(args[0], nodes.Const):
- name_arg = args[0].value
- if variance == TypeVarVariance.double_variant:
- self.add_message(
- "typevar-double-variance",
- node=node,
- confidence=interfaces.INFERENCE,
- )
- self.add_message(
- "typevar-name-incorrect-variance",
- node=node,
- args=("",),
- confidence=interfaces.INFERENCE,
- )
- elif variance == TypeVarVariance.covariant and not name.endswith("_co"):
- suggest_name = f"{re.sub('_contra$', '', name)}_co"
- self.add_message(
- "typevar-name-incorrect-variance",
- node=node,
- args=(f'. "{name}" is covariant, use "{suggest_name}" instead'),
- confidence=interfaces.INFERENCE,
- )
- elif variance == TypeVarVariance.contravariant and not name.endswith("_contra"):
- suggest_name = f"{re.sub('_co$', '', name)}_contra"
- self.add_message(
- "typevar-name-incorrect-variance",
- node=node,
- args=(f'. "{name}" is contravariant, use "{suggest_name}" instead'),
- confidence=interfaces.INFERENCE,
- )
- elif variance == TypeVarVariance.invariant and (
- name.endswith("_co") or name.endswith("_contra")
- ):
- suggest_name = re.sub("_contra$|_co$", "", name)
- self.add_message(
- "typevar-name-incorrect-variance",
- node=node,
- args=(f'. "{name}" is invariant, use "{suggest_name}" instead'),
- confidence=interfaces.INFERENCE,
- )
- if name_arg is not None and name_arg != name:
- self.add_message(
- "typevar-name-mismatch",
- node=node,
- args=(name_arg, name),
- confidence=interfaces.INFERENCE,
- )
|